Customize Snort for your own needs quickly and easily by leveraging its flexible rule engine and language.
One of the best features of Snort [Hack #106] is its rule engine and language. Snort’s rule engine provides an extensive language that enables you to write your own rules, allowing you to extend it to meet the needs of your network.
A Snort rule can be broken down into two basic parts: the rule header and options for the rule. The rule header contains the action to perform, the protocol that the rule applies to, and the source and destination addresses and ports. The rule options allow you to create a descriptive message to associate with the rule, as well as check a variety of other packet attributes by making use of Snort’s extensive library of plug-ins.
Here’s the general form of a Snort rule:
action
proto
src_ip
src_port
direction
dst_ip
dst_port
(options
)
When a packet comes in, its source and destination IP addresses and ports are compared to the rules in the ruleset. If any of the rules is applicable to the packet, its options are then compared to the packet. If all of these comparisons return a match, the specified action is taken.
Snort provides several built-in actions you can use when crafting your rules. To simply log the packet that matches a rule, use the log
action. The alert
action generates an alert using the method specified in your configuration file or on the command line, in addition to logging the packet.
One nice feature is that you can establish very general rules and then create exceptions by writing rules that use the pass
action. This works especially well when you are using the rules distributed with Snort but are getting frequent false positives for some of them. If it’s not a security risk to ignore them, you can simply write pass
rules that will exclude the packets in question.
The last two built-in rule actions, activate
and dynamic
, are used together to dynamically modify Snort’s ruleset at runtime. Rules that use the dynamic
action are just like log
rules, except they will be considered only after they have been enabled by an activate
rule. To determine what dynamic
rules to enable once an activate
rule has been triggered, Snort enforces the use of the activates
and activated_by
rule options. In addition, dynamic
rules are required to specify a count
option so that Snort can limit how many packets the rules will record.
For instance, if you want to start recording packets after an exploit of an SSH daemon on 192.168.1.21 has been noticed, use a couple of rules similar to these:
activate tcp any any -> 192.168.1.21 22 (content:"/bin/sh"; activates:1; \ msg:"Possible SSH buffer overflow"; ) dynamic tcp any any -> 192.168.1.21 22 (activated_by:1; count:100;)
These two rules aren’t completely foolproof, but if someone were to run an exploit with shell code against an SSH daemon, it would most likely send the string /bin/sh in the clear in order to spawn a shell on the system being attacked.
In addition, since SSH is encrypted, strings like that wouldn’t be sent to the daemon under normal circumstances. Once the first rule is triggered, it will activate the second one, which will record 100 packets and then stop. This is useful, since you might be able to catch the intruder downloading or installing a rootkit within those first few packets, and recording them will help you to analyze the compromised system much more quickly.
You can also define custom rule actions, in addition to Snort’s built-in actions. This is done with the
ruletype
keyword:
ruletype redalert { type alert output alert_syslog: LOG_AUTH LOG_ALERT output database: log, mysql, user=snort dbname=snort host=localhost }
This custom rule action tells Snort that it behaves like the alert
rule action, but specifies that the alerts should be sent to the syslog daemon, while the packets will be logged to a database. When defining a custom action, you can use any of Snort’s output plug-ins, just as you would if you were configuring them as your primary output method.
Snort’s detection engine supports several protocols. The proto
field is used to specify the protocol to which your rule applies. Valid values for this field are ip
, icmp
, tcp
, and udp
.
The next fields in a Snort rule are used to specify the source and destination IP addresses and ports of the packet, as well as the direction in which the packet is traveling. Snort can accept a single IP address or a list of addresses. When specifying a list of IP address, you should separate each one with a comma and then enclose the list within square brackets, like this:
[192.168.1.1,192.168.1.45,10.1.1.24]
When doing this, be careful not to use any whitespace. You can also specify ranges of IP addresses using CIDR notation, or even include CIDR ranges within lists. Snort also allows you to apply the logical NOT operator (!
) to an IP address or CIDR range to specify that the rule should match all but that address or range of addresses.
As with IP addresses, Snort can accept single ports as well as ranges. To specify a range, use a colon character to separate the lower bound from the upper bound. For example, if you want to specify all ports from 1 to 1024, do it like this:
1:1024
You can also apply the NOT operator to a port, and you can specify a range of ports without an upper or lower bound.
For instance, if you want to examine only ports greater than 1024, do it this way:
1024:
Similarly, you can specify ports less than 1024 by doing this:
:1024
If you do not care about the IP address or port, you can simply specify any
.
Moving on, the direction
field is used to tell Snort which are the source IP address and port and which are the destination. In earlier versions of Snort, you could use either ->
or <-
to specify the direction. However, the <-
operator has been removed, since you can make either one equivalent to the other by just switching the IP addresses and port numbers. Snort does have another direction operator in addition to ->
, though. Specifying <>
as the direction tells Snort that you want the rule to apply bidirectionally. This is especially useful when using log
rules or dynamic
rules, since it enables you to log both sides of the TCP stream rather than just one.
The next part of the rule includes the options. This part lets you specify many other attributes to check against. Each option is implemented through a Snort plug-in. When a rule that specifies an option is triggered, Snort runs through the option’s corresponding plug-in to perform the check against the packet. Snort has over 40 plug-ins—too many to cover in detail in this hack—but we will look at some of the more useful ones.
The most useful option is
msg
. This option allows you to specify a custom message that will be logged in the alert when a packet matching the rule is detected. Without it, most alerts wouldn’t make much sense at first glance. This option takes a string enclosed in quotes as its argument.
For example, this specifies a logical message whenever Snort notices any traffic that is sent from 192.168.1.35:
alert tcp 192.168.1.35 any -> any any (msg:"Traffic from 192.168.1.35";)
Be sure not to include any escaped quotes within the string. Snort’s parser is a simple one and does not support escaping characters.
Another useful option is content
, which allows you to search a packet for a sequence of characters or hexadecimal values. If you are searching for a string, you can just put it in quotes; to specify a case-insensitive search, add nocase;
to the end of all your options. If you are looking for a sequence of hexadecimal digits, you must enclose them in |
characters. For example, this rule will trigger when the digit 0x90
is spotted in a packet’s data payload:
alert tcp any any -> any any (msg:"Possible exploit"; content:"|90|";)
This digit is the hexadecimal equivalent of the NOP instruction on the x86 architecture and is often seen in exploit code because it can be used to make buffer overflow exploits easier to write.
The offset
and depth
options can be used in conjunction with the content
option to limit the searched portion of the data payload to a specific range of bytes. For example, if you want to limit content matches for NOP instructions to between bytes 40 and 75 of the data portion of a packet, you can modify the previously shown rule to look like this:
alert tcp any any -> any any (msg:"Possible exploit"; content:"|90|"; \ offset:40; depth:75;)
You can also match against packets that do not contain the specified sequence by prefixing it with a !
.
Another thing you might want to check is the size of a packet’s data payload. Many shell code payloads can be large compared to the normal amount of data carried in a packet sent to a particular service. You can check the size of a packet’s data payload by using the dsize
option. This option takes a number as an argument. In addition, you can specify an upper bound by using the <
operator, or you can choose a lower bound by using the >
operator. Upper and lower bounds can be expressed with <>
. For example, the following line modifies the previous rule to match only if the data payload’s size is greater than 6000 bytes, in addition to the other options criteria:
alert tcp any any -> any any (msg:"Possible exploit"; content:"|90|"; \ offset:40; depth:75; dsize: >6000;)
To check the
TCP flags of a packet, Snort provides the flags
option. This option is especially useful for detecting port scans that employ various invalid flag combinations.
For example, this rule will detect when the SYN
and FIN
flags are set at the same time:
alert any any -> any any (flags:SF,12; msg:"Possible SYN FIN scan";)
Valid flags are S
for SYN
, F
for FIN
, R
for RST
, P
for PSH
, A
for ACK
, and U
for URG
. In addition, Snort lets you check the values of the two reserved flag bits. You can specify these by using either 1
or 2
. You can also match packets that have no flags set by using 0
. The flags
option will accept several operators. You can prepend either a +
,
*
, or !
to the flags, to match on all the flags plus any others, any of the flags, or only if none of the flags are set, respectively.
In practice, you might find that some of your rules are a bit noisy and trigger alerts too often to be useful. A way to overcome this is to use Snort’s thresholding feature. This feature allows you to specify a minimum number of times a rule needs to be matched for a particular IP address before it actually generates an alert, or limit the number of alerts a rule can generate during an arbitrary interval of time.
You can use thresholding in two different ways.
You can specify a threshold for a rule separately by referencing its ID. Threshold statements take the following form and are usually put in threshold.conf (located in the same directory as your snort.conf):
threshold gen_id<generator ID>
, sig_id<signature ID>
, \ type<limit | threshold | both>
, \ track<by_src | by_dest>
, count<n>
, seconds<m>
The <generator ID>
is the portion of Snort that generates the alert you want to threshold. This is used to track which preprocessor an alert came from. Since all alerts for signatures are generated by Snort’s signature engine, this should always be set to 1
. The <signature ID>
corresponds to the signature’s ID. This, of course, means that you’ll need to specify IDs when writing your own rules. This is done with the sid
option. You’ll also want to specify the rule’s revision with the rev
option.
For example, here’s a rule that we looked at before, but with sid
and rev
options added:
alert tcp any any -> any any (msg:"Possible exploit"; content:"|90|"; \ offset:40; depth:75; dsize: >6000; sid:1000001; rev:1;)
Note that only signature IDs greater than one million can be used for local rules.
When specifying thresholds, you can choose between three types.
limit
thresholds cause an alert to be generated up until the signature has been matched a set number of times (specified with the count
option) during the chosen interval (specified with the seconds
parameter). The threshold
type is used to prevent an alert from being generated unless the signature has been matched at least count
times during the specified time interval. Specifying both
as the type produces a mixture of the two techniques: it will cause one alert to be generated only after count
has been reached during the specified time period. To indicate whether the thresholding should apply to the source or destination IP address, use by_src
or by_dest
, respectively.
The other way to use thresholding is to include the thresholding parameters in the rule itself. For instance, if someone were to actually send a bunch of packets toward your IDS with the SYN
and FIN
flags set, the previously shown rule that would match them would generate far too many alerts to be useful. So, let’s add some thresholding to it:
alert any any -> any any (flags:SF,12; msg:"Possible SYN FIN scan"; \ threshold: type both, track by_dest, count 100, seconds 60)
Now, the alert will trigger only once every minute for each IP address that it sees receiving at least 100 SYN
/FIN
packets during that time period.
If you find that a rule is still too noisy, you can disable it either altogether or for specific IP addresses by using Snort’s suppression feature. Suppression statements take the following form and are usually also kept in threshold.conf:
suppress gen_id<generator ID>
, sig_id<sid>
, [track <by_src
|by_dest
>, ip[/mask]]
The IDs are specified just like in a threshold statement; however, the track
and ip
parameters can be omitted to completely suppress alerts generated by the signature. Use them if you want to limit the suppression to a specific source or destination IP address (or range of addresses). Ranges of IP addresses and networks can be entered in CIDR format.
One of the best features of Snort is that it provides many plug-ins that can be used in the options
field of a rule. The options discussed here should get you off to a good start. However, if you want to write more complex rules, consult Snort’s excellent rule documentation, which contains full descriptions and examples for each of Snort’s rule options. The Snort User’s Manual is available at http://www.snort.org/docs/writing_rules/
.