...making Linux just a little more fun!
By Amit Saha
Sniffers capture byte-by-byte data being sent across the network; in effect, they examine the packets traversing the network in the rawest form. The captured packets can then be analyzed (a process known as packet analysis or protocol analysis) to reveal information about the packets: their protocol, source, destination, etc. This information can then be used to keep a watch on the incoming and outgoing packets, which can be used to troubleshoot network problems.
I am aware that there are other packet sniffers available, but it's easy to get overwhelmed by the huge lists of features in the existing products. This article is intended to show that anyone with the passion and zeal can develop their own sniffer, and add whatever resources they prefer.
We shall implement a basic packet sniffer using the
pcap
library. The anatomy of a basic implementation
with reference to libpcap procedures is given below:
A. Find all devices => find_alldevs() | | \_/ B. Select a device for capturing => pcap_open_live() | | \_/ C. Start capturing packets => pcap_loop() | | \_/ D. Packet analysis => user defined
Examining raw packets is not really very useful, nor very easy. It is through the process of packet analysis that we are able to gather useful information from a raw network capture.
A network packet has two parts: the header and the data. The header part contains the information about the whereabouts of the packet: source, destination, packet type, etc. — and this is what we are going to be most interested in. The data part of the packet contains the actual information being carried by the packet; for example, the data part of an HTTP packet is most likely to contain the HTML source of the Web page being transmitted.
Let's examine the headers of the packets at various layers of an Ethernet-based TCP/IP network. The 4-layer model of the network stack looks like this:
---------------- APPLICATION LAYER (HTTP, SMTP, FTP, POP, TELNET) ---------------- TRANSPORT LAYER (TCP, UDP) ---------------- NETWORK LAYER (IP, ICMP) ---------------- PHYSICAL LAYER (ARP, RARP) ----------------
The protocols mentioned beside each layer are the most important implemented at each layer. Note that ICMP is mentioned as a network-layer protocol. However, it should be kept in mind that an ICMP packet is not directly sent to the physical layer for transmission. It is encapsulated in an IP packet, and only then proceeds on its downward journey through the stack.
As the packet travels down through the stack at the sender's end, header information is added to it at each layer. An HTTP packet (application layer) gets encapsulated in a TCP packet (transport layer), which again gets encapsulated in an IP packet (network layer), which finally gets encapsulated in an Ethernet frame (physical layer). This Ethernet frame then moves onto the communication medium for transmission.
The reverse process happens at the remote host. As the packet travels up through the stack on the receiving end, the appropriate headers are successively stripped off. What the application layer receives at the remote host is what the application generated at the source.
The libpcap library provides the pcap_loop()
function for packet capture. The prototype for pcap_loop() is
shown below:
int pcap_loop(pcap_t *p, int cnt, pcap_handler callback,
u_char *user)
Callback functions are not anything new, and are very common in many APIs. The concept behind a callback function is fairly simple. Suppose my program is waiting for an event of some sort. For this example, let's pretend my program wants a user to press a key. Every time the user does, I want to call a function that will then determine what to do.
[ The above definition of a callback function is somewhat inexact; in event-driven programming, a callback is a handler, a subroutine that is invoked by the run-time system rather than the program itself. For a simple example, see the 'trap' function in 'man bash'. -- Ben ]
Callbacks are used in pcap as well, but instead of being called when a user presses a key, they are called when pcap sniffs a packet. The two functions one can use to define callbacks are pcap_loop() and pcap_dispatch(). Both of them invoke a callback function every time a packet is sniffed that meets our filter requirements (if any filter exists, of course; if not, then any sniffed packet invokes the callback).
The prototype for pcap_loop() is reproduced below:
int pcap_loop(pcap_t *p, int cnt, pcap_handler callback,
u_char *user)
The first argument is our session handle. Following that is an integer telling pcap_loop() how many packets it should sniff before returning (a negative value means it should sniff until an error occurs.) The third argument is the name of the callback function (just its identifier, no parentheses). The last argument is useful in some applications, but many times is simply set to NULL. If we have arguments of our own we wish to send to our callback function, in addition to the arguments pcap_loop() sends, this is where we would do it. Obviously, you must typecast to a u_char pointer to ensure the results make it there correctly; as we will see later, pcap makes use of some very interesting means of passing information in the form of a u_char pointer.
After we examine an example of how pcap does it, it should be obvious how to do it here. If not, consult your local C reference text: an explanation of pointers is beyond the scope of this document.
pcap_dispatch() is almost identical in usage. The only difference between pcap_dispatch() and pcap_loop() is that pcap_dispatch() will only process the first batch of packets it receives from the system, while pcap_loop() will continue processing packets or batches of packets until the count of packets runs out. For a more in-depth discussion of their differences, see the pcap man page.
Before we can provide an example of using pcap_loop(), we must examine the format of our callback function. We cannot arbitrarily define our callback's prototype; otherwise, pcap_loop() would not know how to use the function. So, we use this format as the prototype for our callback function:
void got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet);
Let's examine this in more detail: first, you'll notice that the function has a void return type. This is logical, because pcap_loop() wouldn't know how to handle a return value anyway. The first argument corresponds to the last argument of pcap_loop(). Whatever value is passed as the last argument to pcap_loop() is passed to the first argument of our callback function, every time the function is called. The second argument is the pcap header, which contains information about when the packet was sniffed, how large it is, etc. The pcap_pkthdr structure is defined in pcap.h:
struct pcap_pkthdr { struct timeval ts; /* time stamp */ bpf_u_int32 caplen; /* length of portion present */ bpf_u_int32 len; /* length this packet (off wire) */ };
These values should be fairly self-explanatory. The last argument is the most interesting, and the most confusing to the average novice pcap programmer: it is another pointer to a u_char, and points to the first byte of a chunk of data containing the entire packet, as sniffed by pcap_loop().
However, how do you make use of this variable (named "packet" in our prototype)? A packet contains many attributes, so as you can imagine, it is not really a string, but actually a collection of structures. (For instance, a TCP/IP packet would have an Ethernet header, an IP header, a TCP header, and lastly, the packet's payload (data structure).) This u_char pointer points to the serialized version of these structures. To make any use of it, we must do some interesting typecasting.
First, we need to have the actual structures defined before we can typecast to them. The following are the structure definitions I use to describe a TCP/IP packet over Ethernet. The packet structure can be obtained here (source: http://www.tcpdump.org).
/* Ethernet addresses are 6 bytes */ #define ETHER_ADDR_LEN 6 /* Ethernet header */ struct sniff_ethernet { u_char ether_dhost[ETHER_ADDR_LEN]; /* Destination host address */ u_char ether_shost[ETHER_ADDR_LEN]; /* Source host address */ u_short ether_type; /* IP? ARP? RARP? etc */ }; /* IP header */ struct sniff_ip { u_char ip_vhl; /* version << 4 | header length >> 2 */ u_char ip_tos; /* type of service */ u_short ip_len; /* total length */ u_short ip_id; /* identification */ u_short ip_off; /* fragment offset field */ #define IP_RF 0x8000 /* reserved fragment flag */ #define IP_DF 0x4000 /* don't fragment flag */ #define IP_MF 0x2000 /* more fragments flag */ #define IP_OFFMASK 0x1fff /* mask for fragmenting bits */ u_char ip_ttl; /* time to live */ u_char ip_p; /* protocol */ u_short ip_sum; /* checksum */ struct in_addr ip_src,ip_dst; /* source and dest address */ }; #define IP_HL(ip) (((ip)->ip_vhl) & 0x0f) #define IP_V(ip) (((ip)->ip_vhl) >> 4) /* TCP header */ struct sniff_tcp { u_short th_sport; /* source port */ u_short th_dport; /* destination port */ tcp_seq th_seq; /* sequence number */ tcp_seq th_ack; /* acknowledgement number */ u_char th_offx2; /* data offset, rsvd */ #define TH_OFF(th) (((th)->th_offx2 & 0xf0) >> 4) u_char th_flags; #define TH_FIN 0x01 #define TH_SYN 0x02 #define TH_RST 0x04 #define TH_PUSH 0x08 #define TH_ACK 0x10 #define TH_URG 0x20 #define TH_ECE 0x40 #define TH_CWR 0x80 #define TH_FLAGS (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR) u_short th_win; /* window */ u_short th_sum; /* checksum */ u_short th_urp; /* urgent pointer */ };
Again, we're going to assume we are dealing with a TCP/IP packet over Ethernet. This same technique applies to any packet; the only difference is the structure types that you actually use. So let's begin by defining the variables and compile-time definitions we will need to deconstruct the packet data.
/* ethernet headers are always exactly 14 bytes */ #define SIZE_ETHERNET 14 const struct sniff_ethernet *ethernet; /* The ethernet header */ const struct sniff_ip *ip; /* The IP header */ const struct sniff_tcp *tcp; /* The TCP header */ const char *payload; /* Packet payload */ u_int size_ip; u_int size_tcp;
And now, we do our magical typecasting:
ethernet = (struct sniff_ethernet*)(packet); ip = (struct sniff_ip*)(packet + SIZE_ETHERNET); size_ip = IP_HL(ip)*4; if (size_ip < 20) { printf(" * Invalid IP header length: %u bytes\n", size_ip); return; } tcp = (struct sniff_tcp*)(packet + SIZE_ETHERNET + size_ip); size_tcp = TH_OFF(tcp)*4; if (size_tcp < 20) { printf(" * Invalid TCP header length: %u bytes\n", size_tcp); return; } payload = (u_char *)(packet + SIZE_ETHERNET + size_ip + size_tcp);
How does this work? Consider the layout of the packet data. The u_char pointer is really just a variable containing a memory address. That's what a pointer is in C — it's a variable that points to a location in memory.
For the sake of simplicity, we'll call that address 'X'. Well, if our three structures are just sitting in line, the first of them (sniff_ethernet) located at address X, then we can easily find the address of the structure after it. That address is X plus the length of the Ethernet header, which is 14, or SIZE_ETHERNET.
Similarly, if we have the address of that header, the address of the structure after it is the address of that header plus the length of that header. The IP header, unlike the Ethernet header, does not have a fixed length; its length is given as a count of 4-byte words by the 'header length' field of the IP header; this means that it must be multiplied by 4 to give the size in bytes. The minimum length of that header is 20 bytes.
The TCP header also has a variable length; its length is given as a number of 4-byte words by the "data offset" field of the TCP header, and its minimum length is also 20 bytes.
So let's make a chart:
Variable Location (in bytes) sniff_ethernet X sniff_ip X + SIZE_ETHERNET sniff_tcp X + SIZE_ETHERNET + {IP header length}
The sniff_ethernet structure, being the first in line, is simply at location X. sniff_ip, which follows directly after sniff_ethernet, is at location X, plus however much space the Ethernet header consumes (14 bytes, or SIZE_ETHERNET). sniff_tcp is after both sniff_ip and sniff_ethernet, so it is location at X plus the sizes of the Ethernet and IP headers (14 bytes, and 4 times the IP header length, respectively). Last, the payload (which doesn't have a single structure corresponding to it, as its contents depends on the protocol being used atop TCP) is located after all of them.
So, at this point, we know how to set our callback function, call it, and find out the attributes of the packet that has been sniffed. It's now the time you've been waiting for: writing a useful packet sniffer. Because of source code length, I'm not going to include it in the body of this document. The sniffer output has been verified with output of tcpdump. They seem to match perfectly. Simply download sniff.c, and try it out by compiling it like this:
gcc -o sniff sniff.c -lpcap
This current sniffer is not really feature-rich; it's very limited, as you shall discover when you use it. However, the bare backbone of a sniffer is ready, and you only need to invest some quality time to make it a full-blown sniffer. The sniffer code I've shown here, for example, is going to be used in a project my team members and I are currently working on.
Good luck, and happy sniffing!
"Programming with pcap", by Tim Carstens pcap man page www.tcpdump.org
Talkback: Discuss this article with The Answer Gang
I am a 2nd year student of computer science in India and am a Linux
enthusiast. I would like to make contributions to Linux Gazette in
the areas of network protocols, network security, mobile communications,
etc.
I have been working in the above areas for the past 2 years or so and
have executed projects related to the above categories. For more
details, please visit my homepage.