Thursday, June 19, 2008

Bro's Signature Engine

Bro relies primarily on its extensive scripting language for the definition of detection policies. In addition, however, Bro also provides an independent signature language for doing low-level, Snort-style pattern matching. While signatures are not Bro's preferred detection tool, they sometimes come in handy and are closer to what many people are familiar with from using other NIDS. This posting gives a brief overview on Bro's signatures and covers some of their technical subtleties.

Here, state contains more information
on the connection that triggered the match (see policy/bro.init for the definition of signature_state); msg is the string specified by the signature's event statement (Found root!), and data is the last piece of payload which triggered the pattern match.

To turn such signature_match events into actual alarms, you need to load signature.bro. This script contains a default event handler which raises SensitiveSignaturenotices (as well as others; see the beginning of the script).

Things to keep in mind when writing signatures

Each signature is reported at most once for every connection, further matches of the same signature are ignored.

Header conditions are only evaluated for the first packet from each endpoint. If a header condition does not match the initial packets, the signature will not trigger. Bro optimizes for the most common application here, which is header conditions picking the connection to be examined closer with payload statements.

A number of analyzer-specific content conditions perform pattern matching on elements extracted from an application protocol dialogue. For example, http /.*passwd/ scans URLs requested within HTTP sessions. The thing to keep in mind here is that these conditions only perform any matching when the corresponding application analyzer is actually active for a connection. Note that by default, analyzers are not enabled if the corresponding Bro script has not been loaded.

A good way to double-check whether an analyzer "sees" a connection is checking its log file for corresponding entries. If you cannot find the connection in the analyzer's log, very likely the signature engine has also not seen any application data.

As the name indicates, the payload keyword matches on packet payload only. You cannot use it to match on packet headers; use header conditions for that.

For UDP and ICMP flows, the payload matching is done on a per-packet basis; i.e., any content crossing packet boundaries will not be found.
For TCP connections, the matching semantics depend on whether Bro is reassembling the connection (i.e., putting all of a connection's packets in sequence). By default, Bro is reassembling the first 1K of every TCP connection, which means that within this window, matches will be found without regards to packet order or boundaries (i.e., stream-wise matching).

For performance reasons, by default Bro stops matching on a connection after seeing 1K of payload; see the section on options below for how to change this behaviour. The default was chosen with Bro's main user of signatures in mind: the dynamic protocol detection works well even when examining just connection heads.

Regular expressions are implicitly anchored, i.e., they work as if prefixed with the ^ operator. For reassembled TCP connections, they are anchored at the first byte of the payload stream. For all other connections, they are anchored at the first payload byte of each packet. To match at arbitrary positions, you can prefix the regular expression with .*, as done in the example above.

To match on non-ASCII characters, Bro's regular expressions support the \x<hex> operator. CRs/LFs are not treated specially by the signature engine and can be matched on with \r and \n, respectively. Generally, Bro follows flex's regular expression syntax.
See the DPD signatures in policy/sigs/dpd.bro for some examples of fairly complex payload patterns.

The data argument of the signature_match handler might not carry the full text matched by the regular expression. Bro performs the matching incrementally as packets come in; when
the signature eventually fires, it can only pass on the most recent chunk of data.

Options

The following options control details of Bro's matching process:

dpd_reassemble_first_packets: bool (default: T)

If true, Bro reassembles the beginning of every TCP connection (of up to dpd_buffer_size bytes, see below), to facilitate reliable matching across packet boundaries. If false, only connections are reassembled for which an application-layer analyzer gets activated (e.g., by Bro's dynamic protocol detection).

dpd_match_only_beginning : bool (default: T)

If true, Bro performs packet matching only within the initial payload window of dpd_buffer_size. If false, it keeps matching on subsequent payload as well.

dpd_buffer_size: count (default: 1024)

Defines the window size for the two preceding options. In addition, this value determines the amount of bytes Bro buffers for each connection in order to activate application analyzers even after parts of the payload have already passed through. This is needed by the dynamic protocol detection capability to defer the decision which analyzers to use.

So, how about using Snort signatures with Bro?

There was once a script, snort2bro, which converted Snort signatures automatically into Bro's signature syntax. However, in our experience this didn't turn out to be very useful because by simply using Snort signatures, one can't benefit from the additional capabilities Bro provides; the approaches of the two systems are too different. We therefore stopped maintaining the snort2bro script, and there are now many newer Snort options which it doesn't support. While the script is still part of the Bro distribution, I don't recommend using it anymore, and we will likely remove it in future versions. (If, however, anybody feels like updating the script to support the new Snort options, that would certainly still be appreciated ... It won't be exactly trivial though.)