Next: 4. Making Snort Faster
Up: 3. Writing Snort Rules
Previous: 3.7 Post-Detection Rule Options
  Contents
Subsections
There are some general concepts to keep in mind when developing Snort
rules to maximize efficiency and speed.
The 2.0 detection engine changes the way Snort works slightly by having the
first phase be a setwise pattern match. The longer a content option is, the
more exact the match. Rules without content (or
uricontent) slow the entire system down.
While some detection options, such as pcre and byte_test,
perform detection in the payload section of the packet, they do not use the
setwise pattern matching engine. If at all possible, try and have at least one
content option if at all possible.
Try to write rules that target the vulnerability, instead of a specific exploit.
For example, look for a the vulnerable command with an argument that is too
large, instead of shellcode that binds a shell.
By writing rules for the vulnerability, the rule is less vulnerable to evasion
when an attacker changes the exploit slightly.
Many services typically send the commands in upper case letters. FTP is a good example. In FTP, to send the username, the client sends:
user username_here
A simple rule to look for FTP root login attempts could be:
alert tcp any any -> any any 21 (content:"user root";)
While it may seem trivial to write a rule that looks for the username
root, a good rule will handle all of the odd things that the protocol might
handle when accepting the user command.
For example, each of the following are accepted by most FTP servers:
user root
user root
user root
user root
user<tab>root
To handle all of the cases that the FTP server might handle, the rule needs
more smarts than a simple string match.
A good rule that looks for root login on ftp would be:
alert tcp any any -> any 21 (flow:to_server,established; content:"root";
pcre:"/user\s+root/i";)
There are a few important things to note in this rule:
- The rule has a flow option, verifying this is traffic going to the server on an enstablished session.
- The rule has a content option, looking for root, which is
the longest, most unique string in the attack. This option is added to allow
Snort's setwise pattern match detection engine to give Snort a boost in speed.
- The rule has a pcre option, looking for user, followed at least one space character (which includes tab), followed by root, ignoring case.
The content matching portion of the detection engine has recursion to handle a
few evasion cases. Rules that are not properly written can cause Snort to
waste time duplicating checks.
The way the recursion works now is if a pattern matches, and if any of the
detection options after that pattern fail, then look for the pattern again
after where it was found the previous time. Repeat until the pattern is not
found again or the opt functions all succeed.
On first read, that may not sound like a smart idea, but it is needed. For example, take the following rule:
alert ip any any -> any any (content:"a"; content:"b"; within:1;)
This rule would look for ``a'', immediately followed by ``b''. Without recursion,
the payload ``aab'' would fail, even though it is obvious that the payload ``aab''
has ``a'' immediately followed by ``b'', because the first "a" is not
immediately followed by ``b''.
While recursion is important for detection, the recursion implementation is not
very smart.
For example, the following rule options are not optimized:
content:"|13|"; dsize:1;
By looking at this rule snippit, it is obvious the rule looks for a packet with
a single byte of 0x13. However, because of recursion, a packet with 1024 bytes
of 0x13 could cause 1023 too many pattern match attempts and 1023 too many
dsize checks. Why? The content 0x13 would be found in the first byte, then
the dsize option would fail, and because of recursion, the content 0x13 would
be found again starting after where the previous 0x13 was found, once it is
found, then check the dsize again, repeating until 0x13 is not found in the
payload again.
Reordering the rule options so that discrete checks (such as dsize) are moved to the begining of the rule speed up Snort.
The optimized rule snipping would be:
dsize:1; content:"|13|";
A packet of 1024 bytes of 0x13 would fail immediately, as the dsize check is
the first option checked and dsize is a discrete check without recursion.
The following rule options are discrete and should generally be placed at the
begining of any rule:
- dsize
- flags
- flow
- fragbits
- icmp_id
- icmp_seq
- icode
- id
- ipopts
- ip_proto
- itype
- seq
- session
- tos
- ttl
- ack
- window
- resp
- sameip
3.8.5 testing numerical values
The rule options byte_test and byte_jump were written to
support writing rules for protocols that have length encoded data. RPC was the
protocol that spawned the requirement for these two rule options, as RPC uses simple
length based encoding for passing data.
In order to understand why byte_test and byte_jump are useful, let's go
through an exploit attempt against the sadmind service.
This is the payload of the exploit:
89 09 9c e2 00 00 00 00 00 00 00 02 00 01 87 88 ................
00 00 00 0a 00 00 00 01 00 00 00 01 00 00 00 20 ...............
40 28 3a 10 00 00 00 0a 4d 45 54 41 53 50 4c 4f @(:.....metasplo
49 54 00 00 00 00 00 00 00 00 00 00 00 00 00 00 it..............
00 00 00 00 00 00 00 00 40 28 3a 14 00 07 45 df ........@(:...e.
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00 00 00 00 00 00 00 06 00 00 00 00 00 00 00 00 ................
00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 04 ................
7f 00 00 01 00 01 87 88 00 00 00 0a 00 00 00 04 ................
7f 00 00 01 00 01 87 88 00 00 00 0a 00 00 00 11 ................
00 00 00 1e 00 00 00 00 00 00 00 00 00 00 00 00 ................
00 00 00 00 00 00 00 3b 4d 45 54 41 53 50 4c 4f .......;metasplo
49 54 00 00 00 00 00 00 00 00 00 00 00 00 00 00 it..............
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00 00 00 00 00 00 00 06 73 79 73 74 65 6d 00 00 ........system..
00 00 00 15 2e 2e 2f 2e 2e 2f 2e 2e 2f 2e 2e 2f ....../../../../
2e 2e 2f 62 69 6e 2f 73 68 00 00 00 00 00 04 1e ../bin/sh.......
<snip>
Let's break this up, describe each of the fields, and figure out
how to write a rule to catch this exploit.
There are a few things to note with RPC:
- Numbers are written as uint32s, taking four bytes. The number 26 would show up as 0x0000001a.
- Strings are written as a uint32 specifying the length of the string, the string, and then null bytes to pad the length of the string to end on a 4 byte boundary. The string ``bob'' would show up as 0x00000003626f6200.
89 09 9c e2 - the request id, a random uint32, unique to each request
00 00 00 00 - rpc type (call = 0, response = 1)
00 00 00 02 - rpc version (2)
00 01 87 88 - rpc program (0x00018788 = 100232 = sadmind)
00 00 00 0a - rpc program version (0x0000000a = 10)
00 00 00 01 - rpc procedure (0x00000001 = 1)
00 00 00 01 - credential flavor (1 = auth\_unix)
00 00 00 20 - length of auth\_unix data (0x20 = 32
## the next 32 bytes are the auth\_unix data
40 28 3a 10 - unix timestamp (0x40283a10 = 1076378128 = feb 10 01:55:28 2004 gmt)
00 00 00 0a - length of the client machine name (0x0a = 10)
4d 45 54 41 53 50 4c 4f 49 54 00 00 - metasploit
00 00 00 00 - uid of requesting user (0)
00 00 00 00 - gid of requesting user (0)
00 00 00 00 - extra group ids (0)
00 00 00 00 - verifier flavor (0 = auth\_null, aka none)
00 00 00 00 - length of verifier (0, aka none)
The rest of the packet is the request that gets passed to procedure 1 of sadmind.
However, we know the vulnerability is that sadmind trusts the uid coming from the client. sadmind runs any request where the client's uid is 0 as root. As such, we have decoded enough of the request to write our rule.
First, we need to make sure that our packet is an RPC call.
content:"|00 00 00 00|"; offset:4; depth:4;
Then, we need to make sure that our packet is a call to sadmind.
content:"|00 01 87 88|"; offset:12; depth:4;
Then, we need to make sure that our packet is a call to the procedure 1, the vulnerable procedure.
content:"|00 00 00 01|"; offset:16; depth:4;
Then, we need to make sure that our packet has auth_unix credentials.
content:"|00 00 00 01|"; offset:20; depth:4;
We don't care about the hostname, but we want to skip over it and check a
number value after the hostname. This is where byte_test is useful. Starting
at the length of the hostname, the data we have is:
00 00 00 0a 4d 45 54 41 53 50 4c 4f 49 54 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00
We want to read 4 bytes, turn it into a number, and jump that many bytes
forward, making sure to account for the padding that RPC requires on strings.
If we do that, we are now at:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00
which happens to be the exact location of the uid, the value we want to check.
In english, we want to read 4 bytes, 36 bytes from the beginning of the packet, and turn those 4 bytes into an integer and jump that many bytes forward, aligning on the 4 byte boundary. To do that in a Snort rule, we use:
byte_jump:4,36,align;
then we want to look for the uid of 0.
content:"|00 00 00 00|"; within:4;
Now that we have all the detection capabilities for our rule, let's put them all together.
content:"|00 00 00 00|"; offset:4; depth:4;
content:"g00 01 87 88|"; offset:12; depth:4;
content:"|00 00 00 01|"; offset:16; depth:4;
content:"|00 00 00 01|"; offset:20; depth:4;
byte_jump:4,36,align;
content:"|00 00 00 00|"; within:4;
The 3rd and fourth string match are right next to each other, so we should combine those patterns. We end up with:
content:"|00 00 00 00|"; offset:4; depth:4;
content:"|00 01 87 88|"; offset:12; depth:4;
content:"|00 00 00 01 00 00 00 01|"; offset:16; depth:8;
byte_jump:4,36,align;
content:"|00 00 00 00|"; within:4;
If the sadmind service was vulnerable to a buffer overflow when reading the client's hostname, instead of reading the length of the hostname and jumping that many bytes forward, we would check the length of the hostname to make sure it is not too large.
To do that, we would read 4 bytes, starting 36 bytes into the packet, turn it into a number, and then make sure it is not too large (let's say bigger than 200 bytes). In Snort, we do:
byte_test:4,>,200,36;
Our full rule would be:
content:"|00 00 00 00|"; offset:4; depth:4;
content:"|00 01 87 88|"; offset:12; depth:4;
content:"|00 00 00 01 00 00 00 01|"; offset:16; depth:8;
byte_test:4,>,200,36;
Next: 4. Making Snort Faster
Up: 3. Writing Snort Rules
Previous: 3.7 Post-Detection Rule Options
  Contents
|