There are some general concepts to keep in mind when developing Snort rules to maximize efficiency and speed.
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 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:
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:
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;