Contents
Rather than selecting which application protocol analyzer to use based on a connection’s server port, Zeek’s dynamic analyzer framework associates an analyzer tree with every connection. This tree can contain an arbitrary number of analyzers in various constellations and can be modified during the whole lifetime of a connection, i.e., we can enable/disable analyzers on the fly. Most importantly, this gives us two key capabilities:
All analyzers derive from the class Analyzer. We associate an analyzer tree with each connection, which reflects the data-flow during packet analysis, in terms of which analyzers get to perform their analysis. Each packet is first passed to the root node of the tree which passes its (potentially transformed) input on to all of its children. Each child in turn passes the data on to its successors.
The root node must always be of type TransportLayerAnalyzer. There are such analyzers for TCP, UDP, and ICMP. Application-layer analyzers are either derived from TCP_ApplicationAnalyzer (for TCP protocols) or from the general Analyzer class (for all non-TCP protocols).
When a connection begins, the initial analyzer tree is instantiated by the global analyzer::Manager. The initial tree always contains a corresponding TransportLayerAnalyzer. For TCP and UDP it also contains an instance of class PIA_TCP or PIA_UDP, respectively. The PIAs are responsible for detecting protocols as the connection progresses. Most importantly, they perform the signature matching. Depending on whether any well-known port is in use, the initial tree may or may not contain any application-layer analyzers right away.
Analyzers can support one of two input methods (or both): packet-wise or stream-wise. An analyzer can accept input via one method (e.g., packet-wise) and pass it on to its children via the other (e.g., stream-wise). The TCP_Analyzer for example reassembles packets into a byte-stream and thus all TCP_ApplicationAnalyzers only see stream-wise input.
Any Analyzer-derived class can override the following virtual methods:
Interface for packet-wise input (or, more generally, chunk-wise in- order input as the parent analyzer does not necessarily need to pass full packets around).
- len
- Length of data.
- data
- Pointer to data.
- orig
- True if data is from connection originator, false for responder.
- seq
- >=0 if there’s a sequence number associated with the data. -1 if not.
- ip
- Pointer to packet header if there’s a packet associated with the data. 0 if not.
- caplen
- Length of the captured packed if ip is non-zero.
Interface for stream-wise input.
- len
- Length of data.
- data
- Pointer to data.
- orig
- True if data is from connection originator, false for responder.
Interface for input which is supposed to be stream-wise but could not be fitted into a continuous stream (e.g., parts of a TCP stream which could not be reassembled).
- seq
- Sequence number of not-continuous chunk.
- len
- Length of not-continuous chunk.
- orig
- True if from connection originator, false for responder.
In addition, analyzers need to have two static methods:
Any TCP_ApplicationAnalyzer-derived class can override the following virtual methods in addition to those of Analyzer :
Note: Whenever overriding one of these methods, call the parent class’s implementation first before doing anything else.
The classes Analyzer, TCP_ApplicationAnalyzer, and analyzer::Manager provide a couple of methods for passing data on to child analyzers, manipulating the analyzer trees, generating events, etc. See the source. :-)
There is one more thing: SupportAnalyzers, which encapsulate common but protocol-independent tasks (e.g., line-splitting for line-based ASCII protocols). While also derived from Analyzer, support analyzers are conceptually different in the sense that
All the support analyzers of a particular parent analyzer form a list (one list per direction). Every packet/stream-chunk which is handed to the parent first passes through this list. The output of the last support analyzer is then delivered via the parent’s Deliver{Packet,Stream}. The most important support analyzer currently is the ContentLine_Analyzer which performs the mentioned line- splitting in ASCII protocols. It ensures to pass only full lines to the parent’s DeliverStream().
These are the main steps to write an application analyzer for protocol Foo:
Foo_Analyzer::Foo_Analyzer(Connection* conn) : TCP_ApplicationAnalyzer(AnalyzerTag::Foo, conn) { AddSupportAnalyzer(new ContentLine_Analyzer(conn)); }
Analyzers can use one of three ways to be fed new connections:
We now explain how to do each in turn.
If the analyzer is primarily supposed to work on a fixed set of ports, then make a call to Analyzer::register_for_ports:
global foo_ports: set[port] = { 12345/tcp, 54321/tcp } &redef; event bro_init() { Analyzer::register_for_ports(Analyzer::ANALYZER_FTP, foo_ports); }
If you want to activate the analyzer via signatures (thus making it port-independent), create a signature file and load it in Zeek (e.g. via @load-sigs). Below is the signature pair used for HTTP as an example. It leverages the requires-reverse-signature condition to make the signature more reliable, and triggers the HTTP Analyzer via the enable "http" action. Here, "http" refers to the lower-case name the analyzer is registered under when an analyzer::Component is added via a plugin.
signature dpd_http_client { ip-proto == tcp payload /^ *(GET|HEAD|POST) */ tcp-state originator } signature dpd_http_server { ip-proto == tcp payload /^HTTP\/[0-9]/ tcp-state responder requires-reverse-signature dpd_http_client enable "http" }
If you want to activate the analyzer on all connections, you manually need to hook the analyzer into the analyzer tree, in analyzer::Manager::BuildInitialAnalyzerTree. For example, for a stream-based TCP content analyzer, you might use this…
if ( tcp ) { // ... if ( Foo_Analyzer::Available() ) tcp->AddChildAnalyzer(new Foo_Analyzer(conn));
… while for a packet-based one, you could use this:
if ( Foo_Analyzer::Available() ) tcp->AddChildPacketAnalyzer(new Foo_Analyzer(conn));
If your analyzer is not activated when you expect it to, try any of the below:
A TCP_ApplicationAnalyzer can access the state of the parent TCP_Analyzer by calling the method TCP. However, they should be coded in a way that they can also work without having a TCP parent (i.e., TCP() return 0). That will later allow us to use them with decapsulated tunnels. If that is not possible, they should at least do an assert(TCP()) so that one notices if the analyzer is used in the wrong way.
© 2014 The Bro Project.