Packet Analysis

The Packet Analysis plugin architecture handles parsing of packet headers at layers below Zeek’s existing Session analysis. In particular, this allows to add new link and network layer protocols to Zeek. This document provides an overview of the underlying architecture as well as an example-based walk-through. For further details, consider to take a look at the built-in packet analyzers as well as the packet analyzer tests.

The Flow of Packets

The basic packet flow through Zeek is as follows. First, an IOSource deals with getting the packets into Zeek. While an IOSource can be used to interface all sorts of capturing mechanisms, the default source makes use of libpcap to either read PCAP files or sniff an interface. Once acquired, a packet is handed into the packet analysis and processed layer by layer.

Nesting of Protocol Data Units (PDUs)

Nesting of Protocol Data Units (PDUs).

At the lower layers, Protocol Data Units (PDUs) typically consist of a header and a payload, where the payload is the next layer’s PDU and the header carries a numeric identifier that determines the encapsulated protocol (see figure above, where “ID” denotes the location of such a numeric protocol identifier within the header).

Each packet analyzer parses the packet’s header according to the implemented protocol, determines a suitable analyzer for the encapsulated protocol and hands its payload to that next analyzer. Once the IP layer is reached, packet analysis is finished and Zeek continues by contructing a session for the observed connection. After session analysis, which includes processing of TCP and UDP, the packet continues its journey into the land of application layer analyzers. There, Dynamic Protocol Detection is used to determine the application layer protocol and continue the analysis.

Packet Analyzer Configuration

Packet analyzers can be configured using script-land constants. The following script shows configuration of the Ethernet packet analyzer:

packet-analysis-1-ethernet.zeek
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
module PacketAnalyzer::ETHERNET;

export {
    ## Default analyzer
    const default_analyzer: PacketAnalyzer::Tag = PacketAnalyzer::ANALYZER_IP &redef;

    ## IEEE 802.2 SNAP analyzer
    const snap_analyzer: PacketAnalyzer::Tag &redef;
    ## Novell raw IEEE 802.3 analyzer
    const novell_raw_analyzer: PacketAnalyzer::Tag &redef;
    ## IEEE 802.2 LLC analyzer
    const llc_analyzer: PacketAnalyzer::Tag &redef;

    ## Identifier mappings based on EtherType
    const dispatch_map: PacketAnalyzer::DispatchMap = {} &redef;
}

redef dispatch_map += {
    [0x8847] = PacketAnalyzer::DispatchEntry($analyzer=PacketAnalyzer::ANALYZER_MPLS),
    [0x0800] = PacketAnalyzer::DispatchEntry($analyzer=PacketAnalyzer::ANALYZER_IPV4),
    [0x86DD] = PacketAnalyzer::DispatchEntry($analyzer=PacketAnalyzer::ANALYZER_IPV6),
    [0x0806] = PacketAnalyzer::DispatchEntry($analyzer=PacketAnalyzer::ANALYZER_ARP),
    [0x8035] = PacketAnalyzer::DispatchEntry($analyzer=PacketAnalyzer::ANALYZER_ARP),
    [0x8100] = PacketAnalyzer::DispatchEntry($analyzer=PacketAnalyzer::ANALYZER_VLAN),
    [0x88A8] = PacketAnalyzer::DispatchEntry($analyzer=PacketAnalyzer::ANALYZER_VLAN),
    [0x9100] = PacketAnalyzer::DispatchEntry($analyzer=PacketAnalyzer::ANALYZER_VLAN),
    [0x8864] = PacketAnalyzer::DispatchEntry($analyzer=PacketAnalyzer::ANALYZER_PPPOE)
};

The dispatch_map of type PacketAnalyzer::DispatchMap is defined with mappings from EtherTypes to packet analyzers. For example, for EtherType 0x8864, the packet’s payload is passed to the PPPoE analyzer.

The default_analyzer analyzer specifies which packet analyzer to use if none of the mappings matched. In case of Ethernet, we try to fall back to IP.

Furthermore, Ethernet needs to handle different types of frames, with three of them identified using the first payload bytes (see Wikipedia). As the EtherType needs to be interpreted with respect to the frame type in these cases, the Ethernet analyzer provides three additional constants, snap_analyzer, novell_raw_analyzer, and llc_analyzer. to configure analyzers that handle the different frame types.

Note

There are a few conventions involved here:

  • The name of the module is expected to be PacketAnalyzer::<analyzer's canonical name>.
  • The map for dispatching is expected to be named dispatch_map.
  • The default analyzer is expected to be named default_analyzer.

Packet analysis starts at a root analyzer that dispatches based on the link types obtained from the IOSource. Accordingly base/packet-protocols/root/main.zeek contains the following line to integrate the Ethernet analyzer:

[DLT_EN10MB] = PacketAnalyzer::DispatchEntry($analyzer=PacketAnalyzer::ANALYZER_ETHERNET)

Packet Analyzer API

Just like for other parts of Zeek, a plugin may provide a packet analyzer by adding a packet analysis component that instantiates an analyzer. The packet analyzer itself is implemented by inheriting from zeek::packet_analysis::Analyzer and overriding the AnalyzePacket() method. The following is an excerpt from a test case that shows the exemplary analysis of LLC:

packet-analysis-2-llc.cc
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool LLCDemo::AnalyzePacket(size_t len, const uint8_t* data, Packet* packet)
    {
    // Rudimentary parsing of 802.2 LLC
    if ( 17 >= len )
        {
        packet->Weird("truncated_llc_header");
        return false;
        }

    if ( ! llc_demo_message )
        return true;

    auto dsap = data[14];
    auto ssap = data[15];
    auto control = data[16];

    event_mgr.Enqueue(llc_demo_message,
        val_mgr->Count(dsap),
        val_mgr->Count(ssap),
        val_mgr->Count(control));

    return true;
    }

First, we verify that the size of the packet matches what we expect. If that is not the case, we create a weird using the Packet object that is passed along the chain of analyzers. To signal that the analysis failed, the method returns false. For valid packets, we just read some protocol-specific values. As of now, there is no mechanism to pass extracted meta data on to other analyzers. While it is possible to trigger events that receive these values as parameters, keep in mind that handling events for every packet can be extremely expensive. However, for our test case we defined an event as follows in a separate .bif file:

event llc_demo_message%(dsap: count, ssap: count, control: count%);

Before we can expect the event to be generated, we need to integrate the analyzer. The configuration might be included in the scripts that are shipped with the packet analyzer. For example, one could add a new EtherType by adding a mapping to PacketAnalyzer::ETHERNET::dispatch_map. For the LLC example we redefine one of the additional constants:

redef PacketAnalyzer::ETHERNET::llc_analyzer = PacketAnalyzer::ANALYZER_LLC_DEMO;

In this example, packet analysis as well as all further analysis ends with the LLC analyzer. The ForwardPacket() method can be used to pass data to another packet analyzer. The method takes a pointer to the beginning of the data to process (usually the start of the payload in the current context), the length of the data to process, a pointer to the Packet object and an identifier. The identifier would be used to lookup the next analyzer based on the LLC analyzer’s dispatch_map. If there is no match, it will fall back to the default_analyzer if available.

In case a packet analyzer requires initialization, e.g., reading additional configuration values from script-land, this can be implemented by overriding the Initialize() method. When overriding this method, always make sure to call the base-class version to ensure proper initialization.

Note

When writing your own packet analyzer, take a look into the existing code to identify idiomatic ways to handle tasks like looking up configuration values.