Snort and Applied Intrusion Detection

CS 462 Lecture, Dr. Lawlor

One standard trick for intrusion detection is just to watch ("sniff") all the network traffic, and then log or otherwise freak out when you see known-bad stuff go by.  One huge sniffer is called "snort".

Snort

Setting up snort means:
  1. Download the source code from snort.org/dl
  2. Build the source code with "tar xzvf snort-2.8.3.1.tar.gz; cd snort-2.8.3.1; ./configure; make; sudo make install"
  3. Copy the configure directory to a secure place, like /etc: "sudo cp snort-2.4.3/etc /etc/snort"
  4. Edit the "/etc/snort/snort.conf" config file, at least to set the HOME_NET address.
  5. Download snort rules from http://www.snort.org/pub-bin/downloads.cgi.  Free registration is required for the latest rules.
  6. Unpack snort rules using "cd /etc/snort; tar xzvf ~/snortrules*", or write your own rules.
  7. Check the snort config file, /etc/snort/snort.conf.  It's probably OK.
  8. Run snort as root, like: "snort -de  -i eth0 -K ascii -c /etc/snort/snort.conf"
Snort can be configured to send its output to:

Snort Rules

Snort rules use a really hideous syntax, and like me you can spend several hours reading the documentation and still not be able to write snort rules!  It's easier to start with an example.

Say you're part of the Ministry of Love (responsible for detention and torture) inside a paranoid dictatorship that hates our freedom.  You can sniff out dissidents by writing a rule that matches their traffic, like so:

alert tcp any any -> any any (msg:"Crimethink detected"; content:"freedom"; nocase; sid:9998;)

The format of this rule is:
This rule will alert on the word "freedom" inside any unencrypted TCP communication: web searches, emails, etc.

Here's a super-terse example of a more complicated snort rule.  The tighter you write the rule, the fewer false matches you get.

Interpreting Snort Alerts

Here's a typical alert, from the "/var/log/snort/alert" file:
[**] [1:9998:0] Crimethink detected [**]
[Priority: 0]
10/02-15:37:53.223238 0:1D:E0:99:A4:FF -> 0:0:5E:0:1:97 type:0x800 len:0x30D
137.229.48.118:47318 -> 209.85.173.127:80 TCP TTL:64 TOS:0x0 ID:5666 IpLen:20 DgmLen:767 DF
***A**** Seq: 0x73F5AA9D  Ack: 0xC3B83424  Win: 0x2E  TcpLen: 32
TCP Options (3) => NOP NOP TS: 5749972 1782851874

The important things there are shown in bold:
The IP Address:
The Port number. Source port numbers are either large (over 1000) and random, or small (under 1000) and always the same.  Low port numbers can only be used by root users, and are often used by system services (e.g., network daemons).

Buffer Overflow Details

Actual exploits of buffer overflow attacks are pretty useful to understand, because that helps you fight them.

First, the trick is just to fill up the stack beyond the end of the allocated buffer.  For example, the string "aaaabbbbccccdddd" overwrites the return function pointer with "dddd" in this program:
int happy_innocent_code(void) {
char str[8];
cin>>str;
cout<<"I just read a string: "<<str<<"! I'm a big boy!\n";
return 0;
}

void evil_bad_code(void) {
cout<<"Mwa ha ha ha...\n";
cout<<"...er, I can't return. Crashing.\n";
}

int foo(void) {
//void *p=(void *)evil_bad_code; /* address of the bad code */
//printf("evil code is at: '%4s'\n",(char *)&p);
happy_innocent_code();
cout<<"How nice!\n";
return 0;

}

(Try this in NetRun now!)

This crashes, which is bad (so don't use fixed-size char arrays as strings!), but it can do worse than crash; it can execute something malicious.  For example, the evil_bad_code is sitting at address 0x8048a44, or as characters, "DŠ" (those last two characters show up in various weird ways on different browsers), so if we input the string "aaaabbbbccccDŠ", the happy_innocent_code above will return directly to the evil_bad_code.

It's even worse if the return address is overwritten with the location of some data the attacker has control over, like the buffer on the stack where the string is stored.  Then the attacker can send some machine code, point the return pointer to that code, and then happy_innocent_code will return directly to the attacker's new remote code, usually "shellcode", since it opens a shell the attacker can use to finish off the vulnerable machine.

On Linux, modern kernels will randomly change the location of stuff on the stack, as a way to help defeat buffer overflow attacks.  For learning how buffer overflows work, you can disable this by doing (as root):
    echo 0 > /proc/sys/kernel/randomize_va_space

On my machine, with stack randomization on I got a different stack address every run:
olawlor@gwala:~/class/cs462/code/buffer_overflow/buffer_overflow_gdb$ ./vulnerable
Password base address=0xbfbfb4c0
olawlor@gwala:~/class/cs462/code/buffer_overflow/buffer_overflow_gdb$ ./vulnerable
Password base address=0xbf8bc980
olawlor@gwala:~/class/cs462/code/buffer_overflow/buffer_overflow_gdb$ ./vulnerable
Password base address=0xbfb9cc60
olawlor@gwala:~/class/cs462/code/buffer_overflow/buffer_overflow_gdb$ ./vulnerable
Password base address=0xbfecd790
olawlor@gwala:~/class/cs462/code/buffer_overflow/buffer_overflow_gdb$ ./vulnerable
Password base address=0xbfaeebb0
olawlor@gwala:~/class/cs462/code/buffer_overflow/buffer_overflow_gdb$ ./vulnerable
Password base address=0xbfcdada0

Without stack randomization, I get the same address, 0xbffff8c0, for a local variable every time the program is run.  If I encode this address into the return function pointer, any machine code loaded into that local variable will get executed.  So I can very carefully craft an attack string like this:
unsigned char exploit[exploit_len]={
/* Address Code Assembly // Purpose */
/* 0xbffff8c0 */ 0xba, 0x10,0,0,0, /* mov $16,%ecx // the string length, in characters */
0xb9, 0xe0,0xf8,0xff,0xbf, /* mov $exploit+20,%edx // the string's address */
0xbb, 0x01,0,0,0, /* mov $0x1,%ebx // 1 is the `fd' for stdout */
0xb8, 0x04,0,0,0, /* mov $0x4,%eax // 4 is the syscall number for `write' */
0xcd, 0x80, /* int $0x80 // makes the `write' syscall */

0x31, 0xdb, /* xor %ebx,%ebx // 0 is our exit code */
0xb8, 0x01,0,0,0, /* mov $0x1,%eax // 1 is the syscall number for `exit' */
0xcd, 0x80, /* int $0x80 // makes the `exit' syscall */

0, /* padding to 0x20 bytes */

/* 0xbffff8e0 */ 'E','V','I','L','_','D','E','E','D','S','_','H','E','R','E','!', /* String to print */

/* 0xbffff8f0 */ 0xE5,0,0,0, 0xE6,0,0,0, /* more padding */

/* 0xbffff... */ 0xE7,0,0,0, /* saved frame pointer-- not needed */
/* 0xbffff... */ 0xc0,0xf8,0xff,0xbf, /* saved program counter-- make address of our exploit! */
};
Here I'm making explicit Linux syscalls in order to print out my string.  Note that not only is this sort of attack code painfully annoying to write, it's also very brittle--if you change anything on the attacked machine, the attack will fail.  Things that can kill off a buffer overflow attack include:
Overall, weirdness is your friend here!

As a defender, you can pattern-match anything inside this chunk of code.  In particular, the bytes 0xCD 0x80 are needed to call Linux for anything, so they're a common pattern to match on.  Lots of non-ascii (nul and things) in what is supposed to be a string buffer is also a clear sign of an attack!