ber_data and ber_skip

Snort 3 introduces two new rule options that are useful when writing detection for ASN.1 Basic Encoding Rules (BER), ber_data and ber_skip. Both options move the detection cursor with respect to BER data, but the difference lies in where the cursor is moved.

BER is a type-length-value (TLV) encoding format that encodes data elements in a stream containing (in this order):

  • Type: typically a single byte that identifies the kind of data being represented
  • Length: one or more bytes identifying the size of the value field
  • Value: length-number of bytes of the specified type's data

For example, consider the following BER-encoded element (assume the below bytes are hex):

02 03 01 00 01

The type is 0x02, which corresponds an INTEGER type. The length is 0x03, which tells us that the value is 3-bytes long. Then we just parse those three bytes after the length field to get the integer value:

01 00 01

Encoded length fields

As mentioned above, the length field can either be a single byte, or it can span multiple bytes. More specifically, the length field will span multiple bytes if the value field contains more than 128 bytes. If that's the case, then bit 7 of the length field will be set, and bits 0-6 will indicate how many length bytes follow. For example, consider the start of this element:

30 82 04 9A 41 41 41 41 …

The first byte tells us the type, which in this case is 0x30. However, notice that our length field is > 0x7f. This means that bit 7 of the length field is set to 1, which means that the length field requires more than one byte. So, we can look at the binary of 0x82 to figure out what bits 0-6 give us:

10000010

As we can see, we get 2 from reading bytes 0-6, and so we know that two length bytes follow. We then grab those two bytes ‒ 0x049A ‒ to get the length of the subsequent value. Then we can just parse length-number of bytes as we normally would to get the value.

Now let's look at how Snort 3's ber_data and ber_skip rule options are used to work with this kind of data.

ber_data

The ber_data option moves the detection cursor to the value portion of a specified BER element.

Using this option is done with ber_data: followed by the element's type code. The code can be written in decimal (e.g., 1, 2, 3, 4) or in a hexadecimal (e.g., 0x1, 0x2, 0x3, 0x4), and the valid range of codes is 0:255.

For example, let's assume we are looking at a packet starting with 02 03 01 00 01. We can move to the value portion of the element with ber_data:0x02;.

  • Detection cursor before the ber_data:0x02;:
    00000000  02 03 01 00 01
    
  • Detection cursor after the ber_data:0x02;:
    00000000 01 00 01 
    

Note that ber_data works relative to the cursor's current location in a given buffer. This means that if you don't move the cursor (for example with a content match) prior to using this option, then ber_data will try to pick up bytes at the start of the buffer.

Format:

ber_data:type;

Examples:

ber_data:0x02;
content:"|01 00 01|", within 3;

BER elements are commonly nested such that one element's value is the start of another element. Multiple ber_data options can be used to verify that a specific element is present in another:

ber_data:0x30;
# checks that 0x02 is the first element present in 0x30's value
ber_data:0x02;

ber_skip

The ber_skip option skips an entire BER element. It reads in the length value of the specified type, and jumps that many bytes, moving the cursor to the data immediately following the specified element.

Using this option is done with ber_skip: followed by the type code of the element to skip. The code can be written in decimal (e.g., 1, 2, 3, 4) or in hexadecimal (e.g., 0x1, 0x2, 0x3, 0x4), and the valid range of elements is 0:255.

For example, let's assume we are looking an INTEGER element (identified by 0x02) followed by a BOOLEAN element (identified by 0x01): 02 01 FF 01 01 05. We can skip the INTEGER element with ber_skip:0x02; to move the cursor to the start of the very next element, the BOOLEAN.

  • Detection cursor before the ber_skip:0x02;:
    00000000  02 01 FF 01 01 05
    
  • Detection cursor after the ber_skip:0x02;:
    00000000  01 01 05
    

By default, this rule option will return false if the specified BER element is not found. However, you can specify ,optional after the element number to tell Snort that the BER element you want to skip is optional.

Note that ber_skip works relative to the cursor's current location in a given buffer. This means that if you don't move the cursor (for example with a content match) prior to using this option, then ber_skip will try to pick up bytes at the start of the buffer.

Format:

ber_skip:type[,optional];

Examples:

ber_skip:0x02;
content:"|01 01 05|", within 3;
# will still continue evaluating even if the 0x02 is not present
ber_skip:0x02,optional;
content:"|01 01 05|", within 3;

Using ber_data and ber_skip together

ber_data and ber_skip will often be used together to iterate through BER-encoded structures. LDAP traffic is one common use case. For the below example, consider the following LDAP packet:

00000000  30 33 02 01 02 63 2E 04 00 0A 01 02 0A 01 00 02  03...c..........
00000010  01 00 02 01 00 01 01 00 A0 19 A4 0C 04 02 63 6E  ..............cn
00000020  30 06 81 04 66 72 65 64 A3 09 04 02 64 6E 04 03  0...fred....dn..
00000030  6A 6F 65 30 00                                   joe0.

The below example will iterate through the various elements to get to the data of the first 0x0A tag:

# LDAP is a common example
ber_data:0x30;
ber_skip:0x02,optional;
ber_data:0x63;
ber_skip:0x04;
ber_data:0x0a;
content:"|02|", within 1;