|
/* Copyright (c) 2000 by Network ICE Corporation. All rights reserved. The source code to this program is being publicly disclosed for purposes of discussion. However, this source code is not in the public domain (or open-source or GPL). However, we plan to GPL this code in the future. A license is hearby granted to use this software only in cases where an individual monitors his/her own network traffic. If you would like to purchase a license for other uses of this software, please contact <sales@altivore.com>. Using this software to surreptitiously monitor other people's data may be in violation of USC 18 2512. ALTIVORE v0.9.3 This is a sample program containing some of the features of the features of the FBI's "Carnivore" program. It is intended to serve as a point of discussion about Carnivore features. It has not been thoroughly tested and contains numerous bugs. This may also serve as an "alternative" for ISPs who do not wish to install a black-box from the FBI. Court orders demanding data from the ISP do not necessarily require that Carnivore must be used if the ISP is able to obtain the data in another manner. This software may also be useful in network management, such as backing up data or sniffing a consumer's dial-up connection when they are reporting problems to customer support. HOW TO USE THIS SOFTWARE This software must be compiled and linked with the libpcap library. Libpcap must likewise be installed on the system in order to for this to run. This software has been compiled and briefly tested under Windows and Linux. It should run on pretty much any system with some minor adjustments. Windows Compilation Download WINPCAP developers kit from: http://netgroup-serv.polito.it/winpcap/ Point the include directory to "WPdpack/include" and link with the libraries "libpcap.lib", "user32.lib", and "wsock32.lib". Linux Compilation gcc altivore.c -lpcap -Ipcap -o altivore Note: libpcap broken on RedHat 6.2 WHAT DATA IS COLLECTED? This module was written to match the FBI's solicitation for indepedent technical review of Carnivore that was published on August 25, 2000. Attachment 1 of that document describes several scenarios for Carnivore usage. Throughout this document, the term "Alice" refers to the criminal suspect whose email is being monitored. The term "Bob" refers to the person Alice is communicating with. The sections below can be copied/pasted into the file "altivore.ini", which this program uses to store its configuration information. [1.1 email header pen-register] ;Monitors all the email headers to and from Alice's ;account. This does not capture the "Subject:" field, ;which is considered by courts to be part of the "data" ;rather than the "call records". This should be ;deployed on or near the email server that processes ;Alice's mail. mode = email-headers email.address = alice@example.com logfile = alice.txt [1.2 HTTP pen-register from dial-up user] ;Monitors the IP address of web-sites the user visits. ;A complication in this is that the user dials-up and ;receives a unique IP address each time. We monitor ;the dial-up password protocol known as "RADIUS" in ;order to trigger when Alice logs on and in order to ;find out what IP address she is using. This should be ;deployed on a segment behind the bank of dialup servers ;as well as where it can sniff the RADIUS packets. This ;version of Carnivore only monitors Accounting packets; ;you may have to enable this feature in order to get ;this to work right. mode = server-access radius.account = alice@example.com server.port = 80 logfile = alice.csv [1.3 FTP pen-register from dial-up user] ;Same as above, but monitors FTP instead of HTTP. mode = server-access radius.account = alice@example.com server.port = 80 logfile = alice.csv [2.1 email content-wiretap] ;Instead of capturing just the headers, this scenario ;captures the full contents of the email mode = email-content email.address = alice@example.com tracefile = alice.tcp [2.2 email content-wiretap] ;Captures the full content to/from a specific IP ;address. This is the same as running the freeware ;product called TCPDUMP. Example: ; tcpdump -w tracefile.tcp host 192.0.2.189 mode = ip-content ip.address = 192.0.2.189 tracefile = alice.tcp DESIGN No reassembly/reordering This software does not support IP fragmentation or TCP segment reordering. As a result, it may miss some emails or accidentally include segments from other people's emails. This is a crucial area of discussion; fragmentation issues are an important flaw in many products, and is likely a flaw of Carnivore as well. Little SMTP server state Altivore only monitors a little bit of SMTP server state (it is impossible to fully support SMTP state without reassembly and re-ording of fragments). As a result, it may indvertently capture email not belonging to Alice (the suspect). For example, if the system is unable to determine when an email message ends, it may accidentally capture subsequent emails transfered across the same SMTP connection. It is believed that this is a problem with the FBI's Carnivore as well. RADIUS incomplete This RADIUS parsing code has only been tested at a few ISPs. This is a concern in some deployments because it won't work. One way arround this is to force RADIUS Accounting during deployment. More work on RADIUS decoding needs to be done with Altivore. Evidence Authentication Evidence handling is a big concern. Altivore and Carnivore really should support MD5, PGP, or X.509 private-key signing in order to fully authenticate files. This would detect later unauthorized tampering of the evidence. ALTIVORE VS. NETWORK ICE Network ICE is a leading software vendor of products similar to this technology. The "sniff" network traffic looking for signs of hacker activity in order to protect customer networks. Our primary competitive advantages are our stateful protocol decoding features and high-speed sniffing. This means we can monitor gigabit networks with full packet reassembly and application protocol state. In contrast, Carnivore was probably written using many of the same short-cuts that our competitors have taken. We've written Altivore using similar short-cuts in order to demonstrate the problems with this approach. We've included a small amount of state in order to show why stateful inspection is needed in this class of products. */ #include <string.h> #include <malloc.h> #include <stdlib.h> #include <ctype.h> #include <stdarg.h> #include <stdio.h> #include <sys/stat.h> #include <errno.h> /* Links to the libpcap library, standard library on Windows and * UNIX for sniffing.*/ #include <pcap.h> #define HASH_ENTRIES 1024 #define ADD_IF_NOT_FOUND 1 #define IGNORE_IF_NOT_FOUND 0 #define TCP_TO_SERVER 0x100 #define TCP_FROM_SERVER 0x000 /** Maximum length of an email address. Portions of the address * longer than this length are ignored. */ #define MAX_ADDRESS_LENGTH 1024 /** Maximum number of recipients. More recipients than this are * ignored. */ #define MAX_RECIPIENTS 100 #undef TRUE #define TRUE 1 #undef FALSE #define FALSE 0 /** For pretty printing IP addresses */ #define _XIP(a,n) (int)(((a)>>(n))&0xFF) #define P_IP_ADDR(a) _XIP(a,24), _XIP(a,16), _XIP(a,8), _XIP(a,0) /** * TCP/IP protocol extraction stuff. */ #define ex8(p,f) ((p)[f]) #define ex16(p,f) ((p)[f] << 8 | (p)[f+1]) #define ex32(p,f) ((ex16(p,f)<<16) | ex16(p,f+2)) #define IP_VERSION(p,f) ((ex8(p,f+0) >> 4) & 0x0F) #define IP_SIZEOF_HDR(p,f) ((ex8(p,f+0) & 0x0F) * 4) #define IP_TOTALLENGTH(p,f) ex16(p,f+2) #define IP_PROTOCOL(p,f) ex8(p,f+9) #define IP_SRC(p,f) ex32(p,f+12) #define IP_DST(p,f) ex32(p,f+16) #define TCP_SRC(p,f) ex16(p,f+0) #define TCP_DST(p,f) ex16(p,f+2) #define TCP_SEQNO(p,f) ex32(p,f+4) #define TCP_ACKNO(p,f) ex32(p,f+8) #define TCP_FLAGS(p,f) (ex8(p,f+13)&0x3F) #define TCP_SIZEOF_HDR(p,f) (((ex8(p,f+12)>>4) & 0x0f)*4) #define TCP_FIN 1 #define TCP_SYN 2 #define TCP_RST 4 #define FREE(x) if (x) free(x) /** * A utility function for assigning strings. It solves several * string handling issues, such as copying over "counted strings" * rather than NUL-terminated strings. */ static void setString(char **r_str, const void *vstr, int offset, int len) { const char *str = vstr; /*kludge: avoid warnings*/ if (*r_str) free(*r_str); if (str == NULL) { *r_str = NULL; return; } if (len == -1) len = strlen((const char*)str); *r_str = (char*)malloc(len+1); memcpy(*r_str, str+offset, len); (*r_str)[len] = '\0'; } /** Case-insensitive memcmp() */ static int memcasecmp(const void *lhs, const void *rhs, int length) { int i; for (i=0; i<length; i++) { if (tolower(((char*)lhs)[i]) != tolower(((char*)rhs)[i])) return -1; } return 0; } /** Utility for case-insensitive comparisons*/ static int startsWith(const char lhs[], const char rhs[]) { int len = strlen(lhs); if ((int)strlen(rhs) < len) len = strlen(rhs); return memcmp(lhs, rhs, len) == 0; } /** * Encapsulates the idea of an array of nul terminated strings. Use * straXXXX() functions. */ struct stringarray { char **str; int length; int max; }; typedef struct stringarray stringarray; /** stringarray.straAddElement() * Appends a string onto the end of an array of strings. */ void straAddElement(stringarray *lhs, const char rhs[]) { if (lhs->length + 1 >= lhs->max) { int new_max = lhs->max * 2 + 1; char **new_array = (char**)malloc(sizeof(char*)*(new_max)); if (lhs->str) { memcpy( new_array, lhs->str, sizeof(new_array[0])*lhs->length); free(lhs->str); } lhs->str = new_array; lhs->max = new_max; } lhs->str[lhs->length] = strdup(rhs); lhs->length++; } /** * These are the several modes that Carnivore/Altivore can run as. * See the explanation above for more information on how to * configure these modes. */ enum { /** Capture the headers of email to a text file */ mode_email_headers = 1, /** Capture just the addresses to/from Alice */ mode_email_addresses, /** Record accesses to servers with a specific TCP port. */ mode_server_access, /** Record the full email content for Alice's email*/ mode_email_content, /** Record a full sniffer trace for the indicated * IP address. */ mode_ip_content }; #define MODE(carn, m) ((carn)->mode == m) static const char *modeNames[] = {"unspecified", "email-headers", "email-addresses", "server-access", "email-content", "ip-content", 0}; int parseMode(const char modeName[]) { int i; for (i=0; modeNames[i]; i++) { if (strcmp(modeName, modeNames[i]) == 0) return i; } return 0; } struct intlist { int list[32]; int count; }; typedef struct intlist intlist; /** * The root object for the Carnivore system. */ struct Carnivore { /** What mode of operation we are in */ int mode; /** The name of the sniffer compatible tracefile that data will * be copied to (when doing full-content wiretaps).*/ char *tracefile; FILE *fp_trace; /** Logfile for text information. */ char *logfile; /** A list of IP addresses to filter for. This is used when a * court order specifies IP addresses. TODO: allow ranges and * more IP addresses.*/ intlist ip; /** Contains a list of ports that we will use in order to * monitor when a certain type of server has been accessed. */ intlist port; /** TCP/IP connection table for maintaining session state*/ struct TcpCnxn *cxTable[HASH_ENTRIES]; int cxId; /** Whether or not we should save the last frame to a file */ int do_filter; /** Whether or not we should remove this connection from * our list. */ int do_remove; /** A list of email addresses. We compare these addresses to * emails as they go by in order to determine if we need to * make a copy. */ stringarray email_addresses; /** A list of RADIUS account names that we should monitor * when doing IP wiretaps. */ stringarray radius_accounts; /** An array of tracefiles that we will read in order to test * the system. They must be in tcpdump/libpcap format. */ stringarray testinput; /** An array of adapter names that we need to open in * promiscuous mode. */ stringarray interfaces; }; typedef struct Carnivore Carnivore; /** * Test to see if either the source or destination IP address is * being filtered for. If we are filtering for this IP address, * then we'll likely save it to a file. Note that we are doing a * linear search through the array on the assumption that we are * filtering only a few IP addresses, often just a single one. */ int has_integer(intlist *ip, int ip1, int ip2) { int i; for (i=0; i<ip->count; i++) { if (ip->list[i] == ip1 || ip->list[i] == ip2) return 1; } return 0; } /** Adds the specified IP address to the list of addresses that we * are filtering for. This may be a configured IP address or one * that is auto-configured by the RADIUS parsing. */ void add_integer(intlist *ip, int ip_address) { if (ip_address == 0) return; /*ignore empty IP addresses*/ if (has_integer(ip, ip_address, ip_address)) return; /*ignore duplicates*/ if (ip->count < sizeof(ip->list)/sizeof(int)) { ip->list[ip->count] = ip_address; ip->count++; } } /** Delete an IP address from the list of filters. This is called * when the RADIUS parsing determines that the monitored user has * hung up. */ void del_integer(intlist *ip, int ip_address) { int i; for (i=0; i<ip->count; i++) { if (ip->list[i] == ip_address) { memmove(ip->list+i, ip->list+i+1, (ip->count - i - 1)*sizeof(int)); ip->count--; } } } /** matchName() * Tests to see if the desired email address should be filtered * for. This is presumably the email address of somebody that we * have a court-order to monitor. */ int matchName(const char addr[], int addr_len, stringarray *list) { int i; if (addr == NULL) return 0; for (i=0; i<list->length; i++) { int lhs_len = strlen(list->str[i]); if (list->str[i][0] == '*') { /*match end of string, e.g. allow specification of * "*@suspect.com" to match any emails for a domain*/ if (addr_len >= lhs_len - 1) { const char *new_lhs = list->str[i]+1; const char *new_addr = addr+addr_len-lhs_len+1; if (memcasecmp(new_lhs, new_addr, lhs_len-1) == 0) return TRUE; } } else if (addr_len == lhs_len && memcasecmp(list->str[i], addr, addr_len) == 0) return TRUE; } return FALSE; } /** * A TCP connection entry. We maintain one of these for every * outstanding connection that we might be tracking. This contains * the basic TCP info, as well as some higher level protocol info * for SMTP. */ struct TcpCnxn { /** Each new connection is identified with a unique ID */ int msg_id; int server_ip; int client_ip; int server_port; int client_port; int server_seqno; int client_seqno; struct TcpCnxn *next; time_t creation_time; char *sender; int sender_matches; char *recipient; stringarray recipients; /** Whether or not we should save the email message for this * connection. */ int do_filter; /** Whether we should filter this one frame. We need this in * order to capture the trailing dot that ends an email message. */ int filter_one_frame; /** Whether or not we should remove this connection entry at * the next opportunity. */ int do_remove; /** Whether we are parsing the 'envelope' or the message * itself. */ int state; }; typedef struct TcpCnxn TcpCnxn; /** * Create a hash entry for our table. The hash entry is based * only on the IP addresses and port numbers. The exact * hash algorithm is unimportant, and should be adjusted over * time to produce the best results. Note that since we've * already converted the (src,dst) to (srvr,clnt), we don't * need to make the hash symmetric. */ int cxHash(TcpCnxn *cx) { int result = 0; result = abs((cx->server_ip ^ (cx->client_ip*2)) ^ ((cx->server_port<<16) | cx->client_port)); return result % HASH_ENTRIES; } /** * Compares two connection objects in order to see if they are the * same one. Only IP address and TCP port info is used in this * comparison. */ int cxEquals(TcpCnxn *lhs, TcpCnxn *rhs) { if (lhs->server_ip != rhs->server_ip) return 0; if (lhs->client_ip != rhs->client_ip) return 0; if (lhs->server_port != rhs->server_port) return 0; if (lhs->client_port != rhs->client_port) return 0; return 1; } /** * Looks up a TCP connection object within our table. If not found, * it may add it (depending upon a parameter). * @param carn * This object. * @param rhs * A copy of the connection object we are looking up (we simply * pull out the address/ports from this to compare them). * @param add_if_not_found * Whether we should add a new connection object if we cannot * find an existing one. It is important that we only add * connection objects during a SYN/SYN-ACK in order to avoid * accidentally getting state in the middle of the connection. */ TcpCnxn * cxLookup(Carnivore *carn, TcpCnxn *rhs, int add_if_not_found) { int h = cxHash(rhs); TcpCnxn **r_cx = &carn->cxTable[h]; for (;;) { if (*r_cx == NULL) { /* The connection object wasn't found. If this was * a SYN or SYN-ACK, then we'll need to add this * connection. */ if (add_if_not_found) { *r_cx = (TcpCnxn*)malloc(sizeof(TcpCnxn)); memset(*r_cx, 0, sizeof(**r_cx)); (*r_cx)->server_ip = rhs->server_ip; (*r_cx)->client_ip = rhs->client_ip; (*r_cx)->server_port = rhs->server_port; (*r_cx)->client_port = rhs->client_port; (*r_cx)->server_seqno = rhs->server_seqno; (*r_cx)->client_seqno = rhs->client_seqno; (*r_cx)->creation_time = time(0); } return *r_cx; } if (cxEquals(*r_cx, rhs)) return *r_cx; else r_cx = &(*r_cx)->next; } } /** * Resets the SMTP protocol info back to a known state. It is * important that this be as delicate as possible: it should reset * data at the slightest provocation in order to avoid accidentally * capturing somebody else's email. */ void cxResetMsg(TcpCnxn *cx) { cx->do_filter = FALSE; /*don't capture these emails*/ if (cx->sender) { free(cx->sender); cx->sender = NULL; } cx->sender_matches = FALSE; if (cx->recipients.length) { int i; for (i=0; i<cx->recipients.length; i++) free(cx->recipients.str[i]); free(cx->recipients.str); cx->recipients.str = NULL; memset(&cx->recipients, 0, sizeof(cx->recipients)); } } /** * Removes a TCP connection object from our table. This is called * whenever we reach the end of SMTP processing, the TCP connection * closes, or when we timeout and clean up a connection. */ void cxRemove(Carnivore *carn, TcpCnxn *rhs) { int h = cxHash(rhs); TcpCnxn **r_cx = &carn->cxTable[h]; for (;;) { if (*r_cx == NULL) break; /*not found*/ else if (cxEquals(*r_cx, rhs)) { TcpCnxn *cx = *r_cx; *r_cx = cx->next; cxResetMsg(cx); free(cx); break; } else r_cx = &(*r_cx)->next; } } /** Writes a little-endian integer to the buffer */ void writeint(unsigned char hdr[], int offset, int x) { hdr[offset+0] = (unsigned char)(x>>0); hdr[offset+1] = (unsigned char)(x>>8); hdr[offset+2] = (unsigned char)(x>>16); hdr[offset+3] = (unsigned char)(x>>24); } /** * Saves the current packet to a TCPDUMP compatible file. Note * that I could use the built-in libpcap file saving mechanism * but I want to eventually at digital-signatures, so I'll be * doing strange stuff with the file in the future. */ void carnSavePacket(Carnivore *carn, const unsigned char buf[], int orig_len, time_t timestamp, int usecs) { unsigned char hdr[16]; int snap_len = orig_len; /* We were triggered to save the frame, now turn this off. * The SMTP state engine will have to revalidate the next * packet in order to make sure we should be saving it. */ carn->do_filter = FALSE; /* Exit from this function (without saving content) if we * are not running in the appropriate mode.*/ switch (carn->mode) { case mode_email_content: case mode_ip_content: break; default: return; } if (carn->tracefile == NULL) return; /*no filename*/ /*Open the tracefile if need be*/ if (carn->fp_trace == NULL) { struct stat s = {0}; if (stat(carn->tracefile, &s) == 0) { /*Ooops, it already exists. Maybe we crashed before? * We should not put the header on the file if it * already exists */ carn->fp_trace = fopen(carn->tracefile, "a+b"); } else { /*Create a new one.*/ carn->fp_trace = fopen(carn->tracefile, "wb"); if (carn->fp_trace) { /*create a file header*/ static const char *foo = "\xD4\xC3\xB2\xA1" /*MAGIC*/ "\x02\x00\x04\x00" /*major/minor version*/ "\x00\x00\x00\x00" /*this timezone (GMT)*/ "\x00\x00\x00\x00" /*sig figs */ "\xDC\x05\x00\x00" /*snap length*/ "\x01\x00\x00\x00"; /*link type*/ if (fwrite(foo, 1, 24, carn->fp_trace) != 24) { int xxx = errno; fclose(carn->fp_trace); carn->fp_trace = NULL; errno = xxx; } } } if (carn->fp_trace == NULL) { perror(carn->tracefile); return; } } /* Write the frame to the file */ writeint(hdr, 0, ((int)timestamp)); writeint(hdr, 4, usecs); /*microseconds*/ writeint(hdr, 8, snap_len); /*snapped size of frame*/ writeint(hdr, 12, orig_len); /*original size of frame*/ fwrite(hdr, 1, 16, carn->fp_trace); fwrite(buf, 1, snap_len, carn->fp_trace); } /** * Prints some text to the logfile. */ void logprint(Carnivore *carn, const char fmt[], ...) { FILE *fp; struct stat s = {0}; va_list marker; if (carn->logfile == NULL) return; if (stat(carn->logfile,&s) == 0) fp = fopen(carn->logfile, "a"); else fp = fopen(carn->logfile, "w"); if (fp == NULL) { perror(carn->logfile); return; } va_start(marker, fmt); vfprintf(fp, fmt, marker); va_end(marker); fclose(fp); } /** * For logging purposes, we frequently need to grab the current * time. This function formats the current GMT time in ISO * format. BUG: the time should really be retrieved from the * packet, not the system time (in case we read from tracefiles * rather the live network). */ void formatNow(char tbuf[], int sizeof_tbuf) { time_t now = time(0); struct tm *tmptr = gmtime(&now); /*must be GMT*/ if (tmptr == NULL) strcpy(tbuf, "err"); else strftime(tbuf, sizeof_tbuf, "%Y-%m-%d %H:%M:%S", tmptr); } /** * This function captures just the email addresses. */ void carnPenEmail(Carnivore *carn, const char sender[], const unsigned char rcpt[], int offset, int length) { char tbuf[64]; if (!MODE(carn, mode_email_addresses)) return; /*not recording email addresses*/ if (carn->logfile == NULL) return; /*no logfile specified by user*/ if (sender == NULL) sender = "(nul)"; /*format time: eg. 2000-08-24 08:23:59*/ formatNow(tbuf, sizeof(tbuf)); logprint(carn, "%s, %s, %.*s\n", tbuf, sender, length, rcpt+offset); printf("%s, %s, %.*s\n", tbuf, sender, length, rcpt+offset); } enum { parsing_envelope, parsing_message }; /** * Tests to see if the TCP packet data starts with the specified * command. */ int SMTP_COMMAND(const unsigned char buf[], int offset, int max_offset, const char cmd[]) { int cmd_length = strlen(cmd); int line_length = max_offset-offset; if (line_length < cmd_length) return FALSE; if (memcasecmp(buf+offset, cmd, cmd_length) != 0) return FALSE; offset += cmd_length; /*TODO: test for some boundary conditions*/ return TRUE; } /** * Tests to see if the email body contains a dot '.' on a blank * line by itself. */ int SMTP_IS_DOT(const unsigned char buf[], int offset, int max_offset) { int i; char last_char = '\0'; for (i=offset; i<max_offset; i++) { char c = buf[i]; if (c == '.') { if (i+1 < max_offset && (buf[i+1] == '\n' || buf[i+1] == '\r') && (last_char == '\n' || last_char == '\r')) { return TRUE; } } last_char = c; } return FALSE; } static const char *MAIL_FROM = "MAIL FROM:"; static const char *RCPT_TO = "RCPT TO:"; /** * Processes the email address in a RCPT TO: or MAIL FROM: */ void match_email(Carnivore *carn, const char *cmd, const unsigned char buf[], int offset, int max_offset, TcpCnxn *cx) { int length = -1; int address_matched = FALSE; /* See if this starts with RCPT TO: or MAIL FROM:, and then * skip beyond it. */ if (!SMTP_COMMAND(buf, offset, max_offset, cmd)) return; offset += strlen(cmd); /* Skip beyond leading whitespace and the initial '<' character * (if they exist). */ while (offset < max_offset && (isspace(buf[offset]) || buf[offset] == '<')) offset++; /* Figure out how long the email address is */ for (length=0; offset+length<max_offset; length++) { char c = (char)buf[offset+length]; if (c == '\r' || c == '\n' || c == '>') break; } if (length < 0) return; if (MODE(carn, mode_email_addresses) && cmd == MAIL_FROM ) { /* If we are doing a pen-register style capturing of email * addresses, then save off the SOURCE email address. */ if (cx->sender) free(cx->sender); cx->sender = (char*)malloc(length+1); memcpy(cx->sender, buf+offset, length); cx->sender[length] = '\0'; } /* See if the email addresses match */ if (matchName((char*)buf+offset, length, &carn->email_addresses)) { cx->do_filter = TRUE; address_matched = TRUE; } if (cmd == MAIL_FROM) { if (address_matched) cx->sender_matches = TRUE; } else if (cmd == RCPT_TO) { if (address_matched || cx->sender_matches) carnPenEmail(carn, cx->sender, buf, offset, length); } } /** * Read the number of remaining characters in the line. */ int readLine(const unsigned char buf[], int offset, int max_offset) { int length = 0; while (offset + length < max_offset) { char c = buf[offset+length]; length++; if (c == '\n') break; } return length; } /** * Examine the line from the packet in order to determine whether * it constitutes a legal RFC822 email header. We stop processing * data at the end of the headers. */ int isEmailHeader(const unsigned char buf[], int offset, int max_offset) { int leading_space = 0; int saw_colon = 0; while (offset < max_offset && isspace(buf[offset])) { offset++; /*strip leading whitespace*/ leading_space++; } if (offset >= max_offset) return FALSE; /*empty lines are not a header*/ if (buf[offset] == '>') return FALSE; while (offset < max_offset) { if (buf[offset] == ':') saw_colon = TRUE; offset++; } if (saw_colon) return TRUE; if (leading_space) return TRUE; return FALSE; } /** * This function processes a single TCP segment sent by the client * to the SMTP server. */ int sniffSmtp(Carnivore *carn, TcpCnxn *rhs, int tcp_flags, const unsigned char buf[], int offset, int max_offset) { TcpCnxn *cx; int length; /* Lookup the TCP connection record to see if we are saving * packets on the indicated TCP connection. */ cx = cxLookup(carn, rhs, IGNORE_IF_NOT_FOUND); /* Process data within this TCP segment */ length = max_offset - offset; if (length > 0) { if (cx == NULL) { /* Add a record for this connection whenever we see a * an address in an envelope. */ if (SMTP_COMMAND(buf, offset, max_offset, "RCPT TO:")) cx = cxLookup(carn, rhs, ADD_IF_NOT_FOUND); if (SMTP_COMMAND(buf, offset, max_offset, "MAIL FROM:")) cx = cxLookup(carn, rhs, ADD_IF_NOT_FOUND); } if (cx != NULL) { switch (cx->state) { case parsing_envelope: match_email(carn, MAIL_FROM, buf, offset, max_offset, cx); match_email(carn, RCPT_TO, buf, offset, max_offset, cx); if (SMTP_COMMAND(buf, offset, max_offset, "DATA")) { if (cx->do_filter) cx->state = parsing_message; else cx->do_remove = TRUE; } if (SMTP_COMMAND(buf, offset, max_offset, "QUIT")) cx->do_remove = TRUE; if (SMTP_COMMAND(buf, offset, max_offset, "RSET")) cx->do_remove = TRUE; if (SMTP_COMMAND(buf, offset, max_offset, "ERST")) cx->do_remove = TRUE; break; case parsing_message: if (MODE(carn, mode_email_headers)) { int i; char tbuf[64]; formatNow(tbuf, sizeof(tbuf)); logprint(carn, "--- %08X->%08X %s ---\n", cx->client_ip, cx->server_ip, tbuf); /*Parse just the headers from the first packet*/ for (i=offset; i<max_offset; i++) { int len; len = readLine(buf, offset, max_offset); if (!isEmailHeader(buf, offset, offset+len)) break; if (len > 8 && startsWith((char*)buf+offset, "Subject:")) logprint(carn, "Subject: <removed>\n"); else { /*Write line to log file*/ logprint(carn, "%.*s", len, buf+offset); } offset += len; } logprint(carn,"---EOM---\n"); cx->do_remove = TRUE; cx->do_filter = FALSE; carn->do_filter = FALSE; } if (SMTP_IS_DOT(buf, offset, max_offset)) cx->do_remove = TRUE; break; } } } if (cx) { if (cx->do_filter) carn->do_filter = TRUE; if (cx->filter_one_frame) { carn->do_filter = TRUE; cx->filter_one_frame = FALSE; } if (cx->do_remove || (tcp_flags & TCP_RST) || (tcp_flags & TCP_FIN)) cxRemove(carn, rhs); } return 0; } /** * RADIUS protocol information we parse out of a packet. In the * future versions of this software, we are going to need to * store these records over time; for now, we just parse the * protocol into this normalized structure. */ struct RadiusRecord { int radius_client; int radius_server; int nas_ip; int nas_port; int direction; int code; int xid; int status; char *user_name; char *caller_id; char *called_phone; char *session_id; int ip_address; int session_duration; }; typedef struct RadiusRecord RadiusRecord; /** Frees the allocated information */ void radFree(RadiusRecord *rad) { FREE(rad->user_name); FREE(rad->caller_id); FREE(rad->called_phone); FREE(rad->session_id); } /** * Process a single RADIUS command that we saw on the network. * For right now, we are primarily going to process radius * accounting packets, as these are the ones most likely to give * us solid information. */ void radProcess(Carnivore *carn, RadiusRecord *rad) { enum {account_start=1, account_stop=2}; if (rad->code == 4 || rad->code == 5) { /* ACCOUNTING packet: This packet contains an accounting * record. Accounting records will often contains IP address * assignments that normal authentication packets won't.*/ if (rad->user_name && matchName(rad->user_name, strlen(rad->user_name), &carn->radius_accounts)) { /* Found Alice! Therefore, we going add add Alice's * IP address to the list of IP addresses currently * being filtered. Conversely, if this is a stop * packet, then we will delete the IP address from * our list. */ if (rad->status == account_start) add_integer(&carn->ip, rad->ip_address); else { /* Default: any unknown accounting message should * trigger us to stop capturing data. If we make a * mistake, we should err on the side of not * collecting data. */ del_integer(&carn->ip, rad->ip_address); } carn->do_filter = TRUE; /*capture this packet*/ } /* Double-check: Look to see if the IP address belongs to * another person.*/ else if (has_integer(&carn->ip, rad->ip_address, 0)) { /* The names did not match, yet we have seen some sort * of packet dealing with the account that we are * monitoring. This is bad -- it indicates that we might * have dropped a packet somewhere. Therefore, we are * going to immediately drop this packet.*/ del_integer(&carn->ip, rad->ip_address); carn->do_filter = TRUE; /*capture this packet*/ } } } /** * This function sniffs RADIUS packets off the network, then passes * the processed RADIUS information to another function that * deals with the content. */ int sniffRadius(Carnivore *carn, int ip_src, int ip_dst, const unsigned char buf[], int offset, int max_offset) { RadiusRecord recx = {0}; RadiusRecord *rad = &recx; const static int minimum_length = 20; int code; int xid; int radius_length; int i; if (carn->radius_accounts.length == 0) return 0; /*not scanning radius*/ if (max_offset - offset <= minimum_length) return 0; /*corrupt*/ /* Parse the RADIUS header info and verify */ code = ex8(buf, offset+0); if (code < 1 || code > 5) return 0; /*unknown command/operationg*/ xid = ex8(buf, offset+1); radius_length = ex16(buf, offset+2); if (offset + radius_length > max_offset) return 0; /*packet corrupt*/ else if (offset + radius_length < minimum_length) return 0; /*packet corrupt*/ else if (max_offset > offset + radius_length) max_offset = offset + radius_length; /*ignore padding*/ /* Verify the attributes field */ for (i=offset+minimum_length; i<max_offset-2; /*nul*/) { /*int type = buf[i];*/ int len = buf[i+1]; if (i+len > max_offset) return 0; i += len; } /* Grab the IP addresses of the client (the Network Access * Server like Livingston) and the RADIUS authentication * server. */ if (code == 1 || code == 4) { rad->radius_client = ip_src; rad->radius_server = ip_dst; } else { rad->radius_client = ip_dst; rad->radius_server = ip_src; } rad->code = code; rad->xid = xid; /* Parse the attributes field */ for (i=offset+minimum_length; i<max_offset-2; ) { int type = buf[i]; int len = buf[i+1]; int data_offset = i+2; if (i+len > max_offset) break; i += len; len -= 2; switch (type) { case 1: /*User-Name*/ /*Lots of names appear to have a trailing nul that we *should strip from the end of the name.*/ if (len > 1 && buf[data_offset+len-1] == '\0') len--; setString(&rad->user_name, buf, data_offset, len); break; case 2: /*User-Password*/ break; case 4: /*NAS-IP-Address*/ rad->nas_ip = ex32(buf,data_offset); break; case 5: /*NAS-Port*/ rad->nas_port = ex32(buf,data_offset); break; case 8: /*Framed-IP-Address*/ rad->ip_address = ex32(buf,data_offset); break; case 19: /*Callback-Number*/ case 20: /*Callback-Id*/ /*TODO: sounds like something we might want to record*/ break; case 30: /*Called-Station-Id*/ /*Find out the phone number of the NAS. This could be *important in cases where the evidence will later be *correlated with phone records.*/ setString(&rad->called_phone, buf, data_offset, len); break; case 31: /*Calling-Station-Id*/ /*True "trap-and-trace"! Assuming that caller-id is *enabled, this will reveal the phone number of the *person dialing in.*/ setString(&rad->caller_id, buf, data_offset, len); break; case 40: /*Acct-Status-Type*/ /*When scanning accounting packets, this is critical in *order to be able to detect when the service starts and *stops.*/ rad->status = ex32(buf,data_offset); if (rad->status < 1 || 8 < rad->status) rad->status = 2; /*STOP if unknown*/ break; case 44: /*Acct-Session-Id*/ setString(&rad->session_id, buf, data_offset, len); break; case 46: /*Acct-Session-Time*/ /*Could be interesting information to collect*/ if (len == 4) rad->session_duration = ex32(buf,data_offset); break; } } /* The data was parsed from the RADIUS packet, now process that * data in order to trigger on the suspect.*/ radProcess(carn, rad); radFree(rad); return 0; } struct iphdr { int offset; int proto; int src; int dst; int data_offset; int max_offset; }; struct tcphdr { int offset; int src; int dst; int seqno; int ackno; int flags; int data_offset; }; /** * This packet is called for each packet received from the wire * (or from test input). This function will parse the packet into * the consituent IP and TCP headers, then find which stream the * packet belongs to, then parse the remaining data according * to that stream. */ int sniffPacket(Carnivore *carn, const unsigned char buf[], int max_offset, time_t timestamp, int usecs) { struct iphdr ip; struct tcphdr tcp; TcpCnxn cn; /* Make sure that we have a frame long enough to hold the * Ethernet(14), IP(20), and UDP(8) or TCP(20) headers */ if (max_offset < 14 + 20 + 20) return 1; /* packet fragment too small */ if (ex16(buf,12) != 0x0800) return 1; /*not IP ethertype */ /*IP*/ ip.offset = 14; /*sizeof ethernet_header*/ if (IP_VERSION(buf,ip.offset) != 4) return 1; ip.proto = IP_PROTOCOL(buf,ip.offset); ip.src = IP_SRC(buf,ip.offset); ip.dst = IP_DST(buf,ip.offset); ip.data_offset = ip.offset + IP_SIZEOF_HDR(buf,ip.offset); if (max_offset > IP_TOTALLENGTH(buf,ip.offset) + ip.offset) ip.max_offset = IP_TOTALLENGTH(buf,ip.offset) + ip.offset; else ip.max_offset = max_offset; /* If sniffing somebody's IP address, then sift for it */ if (MODE(carn, mode_ip_content) && has_integer(&carn->ip, ip.src, ip.dst)) carn->do_filter = TRUE; if (ip.proto == 6) { /*TCP*/ tcp.offset = ip.data_offset; tcp.dst = TCP_DST(buf,tcp.offset); tcp.src = TCP_SRC(buf,tcp.offset); tcp.flags = TCP_FLAGS(buf,tcp.offset); tcp.seqno = TCP_SEQNO(buf,tcp.offset); tcp.ackno = TCP_ACKNO(buf,tcp.offset); tcp.data_offset = tcp.offset + TCP_SIZEOF_HDR(buf,tcp.offset); if (MODE(carn, mode_server_access)) { /* We are watching for when the user attempts to access * servers of a specific type (HTTP, FTP, etc.). This * only tracks SYNs; though we could change the code * to track all packets. */ if ((tcp.flags & TCP_SYN) && has_integer(&carn->ip, ip.src, ip.src) && has_integer(&carn->ip, tcp.dst, tcp.dst)) { char tbuf[64]; formatNow(tbuf, sizeof(tbuf)); logprint(carn, "%s, %d.%d.%d.%d, %d.%d.%d.%d, %d\n", tbuf, P_IP_ADDR(ip.src), P_IP_ADDR(ip.dst), tcp.dst); } } else switch (tcp.dst) { case 25: cn.server_ip = ip.dst; cn.client_ip = ip.src; cn.server_port = tcp.dst; cn.client_port = tcp.src; cn.server_seqno = tcp.ackno; cn.client_seqno = tcp.seqno; sniffSmtp(carn, &cn, tcp.flags | TCP_TO_SERVER, buf, tcp.data_offset, ip.max_offset); break; } } else if (ip.proto == 17) { /*UDP*/ tcp.offset = ip.data_offset; tcp.dst = TCP_DST(buf,tcp.offset); tcp.src = TCP_SRC(buf,tcp.offset); tcp.data_offset = tcp.offset + 8; if (tcp.dst == 1812 || tcp.dst == 1813 || tcp.dst == 1645 || tcp.dst == 1646 || tcp.src == 1812 || tcp.src == 1813 || tcp.src == 1645 || tcp.src == 1646) { /* This looks like a RADIUS packet, either using the * old port number or the new one. We are going to * track both RADIUS authentication packets as well * as accounting packets (depending upon whwere we * are tapped into the network, we might see one, * the other, or both).*/ sniffRadius(carn, ip.src, ip.dst, buf, tcp.data_offset, ip.max_offset); } } /* If one of the filters was successful, then save this packet * to the tracefile. This is only done*/ if (carn->do_filter) carnSavePacket(carn, buf, max_offset, timestamp, usecs); return 0; } /** * A callback function that handles each packet as the 'libpcap' * subsystem receives it from the network. */ void pcapHandlePacket(unsigned char *carn, const struct pcap_pkthdr *framehdr, const unsigned char *buf) { int max_offset = framehdr->caplen; sniffPacket((Carnivore*)carn, buf, max_offset, framehdr->ts.tv_sec, framehdr->ts.tv_usec); } /** * Sets the mode of operation according to the input parameter. */ void carnSetMode(Carnivore *carn, const char *value) { if (startsWith(value, "email-head")) carn->mode = mode_email_headers; else if (startsWith(value, "email-addr")) carn->mode = mode_email_headers; else if (startsWith(value, "server-access")) carn->mode = mode_server_access; else if (startsWith(value, "email-content")) carn->mode = mode_email_content; else if (startsWith(value, "ip-content")) carn->mode = mode_ip_content; else carn->mode = -1; } /** * Parses the IP address. I use this rather than the sockets * inet_addr() for portability reasons. */ int my_inet_addr(const char addr[]) { int num = 0; int offset=0; while (addr[offset] && !isalnum(addr[offset])) offset++; for (; addr[offset]; offset++) { char c = addr[offset]; if (isdigit(c)) num = (num&0xFFFFFF00) | (((num&0xFF)*10) + (c - '0')); else if (c == '.') num <<= 8; else break; } return num; } /** * Reads in the configuration from a a file such as "altivore.ini". */ void carnReadConfiguration(Carnivore *carn, const char filename[]) { FILE *fp; fp = fopen(filename, "r"); if (fp == NULL) perror(filename); else { char line[1024]; /* For all lines within the file */ while (fgets(line, sizeof(line), fp)) { char *name = line; char *value; while (*name && isspace(*name)) name++; /*strip leading whitespace*/ if (*name == '\0' || ispunct(*name)) continue;/*ignore blank lines and comments*/ value = strchr(name, '='); if (value == NULL) continue; /*ignore when no equals sign*/ else value++; /*skip the equals itself*/ while (*value && isspace(*value)) value++; /*strip leading whitespace*/ while (*value && isspace(value[strlen(value)-1])) value[strlen(value)-1] = '\0'; /*strip trailing WS*/ if (startsWith(name, "mode")) carn->mode = parseMode(value); else if (startsWith(name, "email.address")) straAddElement(&carn->email_addresses, value); else if (startsWith(name, "radius.account")) straAddElement(&carn->radius_accounts, value); else if (startsWith(name, "ip.address")) add_integer(&carn->ip, my_inet_addr(value)); else if (startsWith(name, "tracefile")) setString(&carn->tracefile, value, 0, -1); else if (startsWith(name, "logfile")) setString(&carn->logfile, value, 0, -1); else if (startsWith(name, "testinput")) straAddElement(&carn->testinput, value); else if (startsWith(name, "interface")) straAddElement(&carn->interfaces, value); else if (startsWith(name, "server.port")) add_integer(&carn->ip, strtol(value,0,0)); else fprintf(stderr, "bad param: %s\n", line); } fclose(fp); } } /** * Process a test input file. */ void processFile(Carnivore *carn, const char filename[]) { char errbuf[1024]; /*TODO: how long should this be?*/ pcap_t *hPcap; /* Open the file */ hPcap = pcap_open_offline(filename, errbuf); if (hPcap == NULL) { fprintf(stderr, "%s: %s\n", filename, errbuf); return; /*ignore this file and go onto next*/ } /* Pump packets through it */ for (;;) { int packets_read = pcap_dispatch( hPcap, /*handle to PCAP*/ 10, /*next 10 packets*/ pcapHandlePacket, /*callback*/ (unsigned char*)carn /*canivore*/ ); if (packets_read == 0) break; } /* Close the file and go onto the next one */ pcap_close(hPcap); } /** * Sniff the wire for packets and process them using the libpcap * interface */ void processPackets(Carnivore *carn, const char devicename[]) { int traffic_seen = FALSE; int total_packets_processed = 0; pcap_t *hPcap; char errbuf[1024]; hPcap = pcap_open_live( (char*)devicename, 2000, /*snap len*/ 1, /*promiscuous*/ 10, /*10-ms read timeout*/ errbuf ); if (hPcap == NULL) { fprintf(stderr, "%s: %s\n", devicename, errbuf); return; } /* Pump packets through it */ for (;;) { int packets_read; packets_read = pcap_dispatch( hPcap, /*handle to PCAP*/ 10, /*next 10 packets*/ pcapHandlePacket, /*callback*/ (unsigned char*)carn /*canivore*/ ); total_packets_processed += packets_read; if (!traffic_seen && total_packets_processed > 0) { fprintf(stderr, "Traffic seen\n"); traffic_seen = TRUE; } } /* Close the file and go onto the next one */ pcap_close(hPcap); } /*----------------------------------------------------------------*/ int main(int argc, char *argv[]) { int i; Carnivore *carn; printf("--- ALTIVORE ---\n"); printf("Copyright (c) 2000 by Network ICE Corporation\n"); printf("Public disclosure of the source code does not\n"); printf("constitute a license to use this software.\n"); printf("Use \"altivore -?\" for help.\n"); /* Create the carnivore subsystem */ carn = (Carnivore*)malloc(sizeof(Carnivore)); memset(carn, 0, sizeof(*carn)); /* Read configuration info from "altivore.ini". */ carnReadConfiguration(carn, "altivore.ini"); /* Parse all the options from the command-line. Normally, * you wouldn't have any command-line options, you would * simply use the configuration file above. */ for (i=1; i<argc; i++) { if (argv[i][0] != '-') straAddElement(&carn->email_addresses, argv[i]); else switch (argv[i][1]) { case 'h': add_integer(&carn->ip, my_inet_addr(argv[i]+2)); break; case 'i': straAddElement(&carn->interfaces, argv[i]+2); break; case 'l': setString(&carn->logfile, argv[i]+2, 0, -1); break; case 'm': carn->mode = parseMode(argv[i]+2); break; case 'p': add_integer(&carn->port, strtol(argv[i]+2,0,0)); break; case 'r': straAddElement(&carn->testinput, argv[i]+2); break; case 'w': setString(&carn->tracefile, argv[i]+2, 0, -1); break; case '?': printf("Options:\n" "<email-address> address to filter for, e.g.:\n" " rob@altivore.com (exact match)\n" " *@altivore.com (partial match)\n" " * (match all emails)\n" ); printf("-h<ip-address>\n" "\tIP of host to sniff\n"); printf("-i<devicename>\n" "\tNetwork interface to sniff on\n"); printf("-l<logfile>\n" "\tText-output logging\n"); printf("-m<mode>\n" "\tMode to run in, see docs\n"); printf("-p<port>\n" "\tServer port to filter on\n"); printf("-r<tracefile>\n" "\tTest input\n"); printf("-w<tracefile>\n" "\tEvidence tracefile to write packets to\n"); return 1; default: fprintf(stderr, "Unknown parm: %s\n", argv[i]); break; } } /* Print the configuration for debugging purposes */ printf("\tmode = %s\n", modeNames[carn->mode]); if (carn->tracefile) printf("\ttracefile = %s\n", carn->tracefile); if (carn->logfile) printf("\tlogfile = %s\n", carn->logfile); for (i=0; i<carn->ip.count; i++) printf("\tip = %d.%d.%d.%d\n", P_IP_ADDR(carn->ip.list[i])); for (i=0; i<carn->port.count; i++) printf("\tport = %d\n", carn->port.list); for (i=0; i<carn->email_addresses.length; i++) printf("\temail.address = %s\n", carn->email_addresses.str[i]); for (i=0; i<carn->radius_accounts.length; i++) printf("\tradius.accounts = %s\n", carn->radius_accounts.str[i]); for (i=0; i<carn->testinput.length; i++) printf("\ttestinput = %s\n", carn->testinput.str[i]); for (i=0; i<carn->interfaces.length; i++) printf("\tinterface = %s\n", carn->interfaces.str[i]); /* Testing only: user can specify tracefiles containing network * traffic for test purposes. */ if (carn->testinput.length > 0) { int i; for (i=0; i<carn->testinput.length; i++) processFile(carn, carn->testinput.str[i]); return 0; } /* Open adapters and rea*/ if (carn->interfaces.length > 0) { /*TODO: allow multiple adapters to be opened*/ char *devicename = carn->interfaces.str[0]; processPackets(carn, devicename); } else { char *devicename; char errbuf[1024]; devicename = pcap_lookupdev(errbuf); if (devicename == NULL) fprintf(stderr, "%s\n", errbuf); else processPackets(carn, devicename); } return 0; }