Contents
Traditionally, Bro decided with the very first packet of a connection which protocol analyzer to choose (based on the connection’s destination port), instantiated a corresponding connection object and started analyzing. This decision was fixed until the termination of the connection. With Bro’s new dynamic analyzer framework, we instead associate 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 new 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 DPM. 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 DPM 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:
static Analyzer* InstantiateAnalyzer(Connection* conn) { return new Foo_Analyzer(conn); } static bool Available() { return foo_event_1 || foo_event_1; }
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 add an entry to dpd_config in the analyzer’s policy script:
global foo_ports: set[port] = { 12345/tcp, 54321/tcp } &redef; redef dpd_config += { [ANALYZER_FTP] = [$ports = foo_ports] };
If you want to activate the analyzer via signatures (thus making it port-independent), add them to policy/sigs/dpd.sig. 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 textual name the analyzer is registered under in its entry in Analyzer::analyzer_configs in Analyzer.cc.
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 DPM::BuildInitialAnalyzerTree. For example, for a stream-based TCP content analyzer, you might use this…
// Around line 296: 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.
© 2011 The Bro Project. Logo design by DigiP.
