Bro relies primarily on its extensive scripting language for defining and analyzing 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 page gives a brief overview on Bro’s signatures and covers some of their technical subtleties.
Contents
Let’s look at an example signature first:
signature my-first-sig {
ip-proto == tcp
dst-port == 80
payload /.*root/
event "Found root!"
}
This signature asks Bro to match the regular expression .*root on all TCP connections going to port 80. When the signature triggers, Bro will raise an event signature_match of the form:
event signature_match(state: signature_state, msg: string, data: string)
Here, state contains more information on the connection that triggered the match, 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 can load Bro’s base/frameworks/signatures/main.bro script. This script contains a default event handler that raises Signatures::Sensitive_Signature Notices (as well as others; see the beginning of the script).
As signatures are independent of Bro’s policy scripts, they are put into their own file(s). There are two ways to specify which files contain signatures: By using the -s flag when you invoke Bro, or by extending the Bro variable signature_files using the += operator. If a signature file is given without a path, it is searched along the normal BROPATH. The default extension of the file name is .sig, and Bro appends that automatically when neccesary.
Let’s look at the format of a signature more closely. Each individual signature has the format signature <id> { <attributes> }. <id> is a unique label for the signature. There are two types of attributes: conditions and actions. The conditions define when the signature matches, while the actions declare what to do in the case of a match. Conditions can be further divided into four types: header, content, dependency, and context. We discuss these all in more detail in the following.
Header conditions limit the applicability of the signature to a subset of traffic that contains matching packet headers. For TCP, this match is performed only for the first packet of a connection. For other protocols, it is done on each individual packet.
There are pre-defined header conditions for some of the most used header fields. All of them generally have the format <keyword> <cmp> <value-list>, where <keyword> names the header field; cmp is one of ==, !=, <, <=, >, >=; and <value-list> is a list of comma-separated values to compare against. The following keywords are defined:
For lists of multiple values, they are sequentially compared against the corresponding header field. If at least one of the comparisons evaluates to true, the whole header condition matches (exception: with !=, the header condition only matches if all values differ).
In addition to these pre-defined header keywords, a general header condition can be defined either as
header <proto>[<offset>:<size>] [& <integer>] <cmp> <value-list>
This compares the value found at the given position of the packet header with a list of values. offset defines the position of the value within the header of the protocol defined by proto (which can be ip, tcp, udp or icmp). size is either 1, 2, or 4 and specifies the value to have a size of this many bytes. If the optional & <integer> is given, the packet’s value is first masked with the integer before it is compared to the value-list. cmp is one of ==, !=, <, <=, >, >=. value-list is a list of comma-separated integers similar to those described above. The integers within the list may be followed by an additional / mask where mask is a value from 0 to 32. This corresponds to the CIDR notation for netmasks and is translated into a corresponding bitmask applied to the packet’s value prior to the comparison (similar to the optional & integer).
Putting all together, this is an example condition that is equivalent to dst- ip == 1.2.3.4/16, 5.6.7.8/24:
header ip[16:4] == 1.2.3.4/16, 5.6.7.8/24
Internally, the predefined header conditions are in fact just short-cuts and mapped into a generic condition.
Content conditions are defined by regular expressions. We differentiate two kinds of content conditions: first, the expression may be declared with the payload statement, in which case it is matched against the raw payload of a connection (for reassembled TCP streams) or of a each packet (for ICMP, UDP, and non-reassembled TCP). Second, it may be prefixed with an analyzer-specific label, in which case the expression is matched against the data as extracted by the corresponding analyzer.
A payload condition has the form:
payload /<regular expression>/
Currently, the following analyzer-specific content conditions are defined (note that the corresponding analyzer has to be activated by loading its policy script):
For example, http-request /.*(etc/(passwd|shadow)/ matches any URI containing either etc/passwd or etc/shadow. To filter on request types, e.g. GET, use payload /GET /.
Note that HTTP pipelining (that is, multiple HTTP transactions in a single TCP connection) has some side effects on signature matches. If multiple conditions are specified within a single signature, this signature matches if all conditions are met by any HTTP transaction (not necessarily always the same!) in a pipelined connection.
To define dependencies between signatures, there are two conditions:
Context conditions pass the match decision on to other components of Bro. They are only evaluated if all other conditions have already matched. The following context conditions are defined:
The given policy function is called and has to return a boolean confirming the match. If false is returned, no signature match is going to be triggered. The function has to be of type function cond(state: signature_state, data: string): bool. Here, content may contain the most recent content chunk available at the time the signature was matched. If no such chunk is available, content will be the empty string. signature_state is defined as follows:
type signature_state: record { id: string; # ID of the signature conn: connection; # Current connection is_orig: bool; # True if current endpoint is originator payload_size: count; # Payload size of the first packet };
Actions define what to do if a signature matches. Currently, there are two actions defined:
Raises a signature_match event. The event handler has the following type:
event signature_match(state: signature_state, msg: string, data: string)
The given string is passed in as msg, and data is the current part of the payload that has eventually lead to the signature match (this may be empty for signatures without content conditions).
The following options control details of Bro’s matching process:
There was once a script, snort2bro, that converted Snort signatures automatically into Bro’s signature syntax. However, in our experience this didn’t turn out to be a very useful thing to do because by simply using Snort signatures, one can’t benefit from the additional capabilities that Bro provides; the approaches of the two systems are just too different. We therefore stopped maintaining the snort2bro script, and there are now many newer Snort options which it doesn’t support. The script is now no longer part of the Bro distribution.