Packet sniffer and parser in C
This post will cover a concise implementation of how to open live pcap sessions on any network device and reading the incoming packets on that interface. In the end, the post will display how to parse the packets appropriately to get the required information.
We use libpcap in the implementation to listen to the packets on the network device. The same can also be used to directly read from a pcap file instead of live sessions. In the implementation, we have turned the promiscuous mode to true so that we are able to listen to packets also not destined for the machine on which the session is being run on.
#include <stdio.h>
#include <pcap.h>
int main() {
char *dev = argv[1], errbuf[PCAP_ERRBUF_SIZE];
int BUFSIZE = 1024;
pcap_t *handle;
struct bpf_program fp;
char filter_exp[] = "port 22";
bpf_u_int32 mask;
bpf_u_int32 net;
struct pcap_pkthdr header;
const u_char *packet;
dev = pcap_lookupdev(errbuf);
if (dev == NULL) {
fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
return(2);
}
printf("\nDevice: %s\n", dev);
handle = pcap_open_live(dev, BUFSIZE, 1, 1000, errbuf);
if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
if (pcap_setfilter(handle, &fp) == -1) {
fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
packet = pcap_next(handle, &header);
printf("Jacked a packet with length of [%d]\n", header.len);
return (0);
}
In the above gist, we use bpf (Berkeley Packet Filters) along with libpcap to compile the required filters. BPF or eBPF can be used to run secure sandboxed code directly in the kernel. We can use the bpf filters to listen for the kind of traffic that we are interested in. In the above gist, we use this line to listen on port 22 which is mainly used for SSH.
char filter_exp[] = “port 22”;
Once the filters are compiled, we set the filter on the libpcap session to filter the packets appropriately.
We are using pcap_next here to get the next packet. Ideally, we should use pcap_loop so that can read the packets from the session in a loop.
To compile and run the code, try the following
gcc -o /tmp/pcapper packet_capture.c -lpcap
Now that we have received the packet, we move on to the techniques used to parse the packet to retrieve the information required.
#include <netinet/in.h>
#include <pcap.h>
/* Ethernet addresses are 6 bytes */
#define ETHER_ADDR_LEN 6
#define SIZE_ETHERNET 14
/* 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 /* dont 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 */
typedef u_int tcp_seq;
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 */
};
int main(int argc, char *argv[]) {
printf("Launching Packet Capture");
char *dev = argv[1], errbuf[PCAP_ERRBUF_SIZE];
int BUFSIZE = 1024;
pcap_t *handle;
struct bpf_program fp;
char filter_exp[] = "port 80";
bpf_u_int32 mask;
bpf_u_int32 net;
struct pcap_pkthdr header;
const u_char *packet;
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;
dev = pcap_lookupdev(errbuf);
if (dev == NULL) {
fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
return(2);
}
printf("\nDevice: %s\n", dev);
handle = pcap_open_live(dev, BUFSIZE, 1, 1000, errbuf);
if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
if (pcap_setfilter(handle, &fp) == -1) {
fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
packet = pcap_next(handle, &header);
printf("Jacked a packet with length of [%d]\n", header.len);
printf("Parsing Ethernet header\n");
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 (0);
}
printf("Parsing TCP header\n");
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 (0);
}
printf("Ether Type %d\n", ethernet->ether_type);
printf("Src Host %d\n", ethernet->ether_shost[ETHER_ADDR_LEN]);
printf("Dst Host %d\n", ethernet->ether_dhost[ETHER_ADDR_LEN]);
printf("Src Port %d\n", tcp->th_sport);
printf("Dst Port %d\n", tcp->th_dport);
printf("Protocol %d\n", ip->ip_p);
payload = (u_char *)(packet + SIZE_ETHERNET + size_ip + size_tcp);
pcap_close(handle);
return(0);
}
In order to understand the above gist, it is important to understand pointer arithmetics and how information is laid across the memory.
We can see that the packet variable is a pointer of u_char which points to the starting byte of the data pointed by packet.
const u_char *packet;
In order to read the ethernet header, we need to typecast the packet to the ethernet variable of type sniff_ethernet
ethernet = (struct sniff_ethernet*)(packet);
The ethernet headers are of fixed length always. To parse the next layer, we do the following.
ip = (struct sniff_ip*)(packet + SIZE_ETHERNET);
The IP header starts from (packet + SIZE_ETHERNET). The TCP headers can be derived from the same format and so on.
You can also implement custom tunneling protocols via this format and parse the packet accordingly.
The entire post has been heavily derived from https://www.tcpdump.org/pcap.html.
Please let me know if you have any queries regarding the article. Happy reading!!
References