Building a packet sniffer Part 2 : Accessing layers headers

Talal
8 min readAug 2, 2023

--

In the previous part we made a basic network sniffer that can only alert us about packets arriving. Today we want to improve our program so that it will show more information about those captured packets.

We know that pcap_loop() passes packet data to the callback function, and in our code we defined packetd_ptr as the pointer that will point to the beginning of this data.

Displaying headers information

Network (IP) layer header

packetd_ptr will be pointing to the datalink header, we need to skip it to get to the IP layer header. This can be done by adding the datalink header length to the pointer, and to do that we need to know the link layer header type.

We can get the header type by calling pcap_datalink() which takes the device (capdev) as an argument and returns an integer. There are many link types defined in libpcap but we are going to work with two types DLT_NULL and DLT_EN10MB. Any type other than those two will have a header length of 0.

int link_hdr_length = 0;

void call_me(u_char *user, const struct pcap_pkthdr *pkthdr,
const u_char *packetd_ptr) {
.
.
.
}

int main(int argc, char const *argv[]) {
.
.
int link_hdr_type = pcap_datalink(capdev);

switch(link_hdr_type) {
// Loopback
case DLT_NULL:
link_hdr_length = 4;
break;
// Ethernet
case DLT_EN10MB:
link_hdr_length = 14;
break;
default:
link_hdr_length = 0;
}
.
.
}

Take a look at the available link types in https://www.tcpdump.org/linktypes.html .

Now we can skip this header and then type cast the pointer to ip struct (struct ip). From this we can extract IP header information such as source/destination ip addresses, type of service, time to live, protocol, length, …etc.

void call_me(u_char *user, const struct pcap_pkthdr *pkthdr,
const u_char *packetd_ptr) {
packetd_ptr += link_hdr_length;
struct ip *ip_hdr = (struct ip*) packetd_ptr;

// inet_ntoa() writes it's result to an address and returns this address,
// but subsequent calls to inet_ntoa() will also write to the same address,
// so we need to copy the result to a buffer.
char packet_srcip[INET_ADDRSTRLEN]; // source ip address
char packet_dstip[INET_ADDRSTRLEN]; // destination ip address
strcpy(packet_srcip, inet_ntoa(ip_hdr->ip_src));
strcpy(packet_dstip, inet_ntoa(ip_hdr->ip_dst));
int packet_id = ntohs(ip_hdr->ip_id), // identification
packet_ttl = ip_hdr->ip_ttl, // Time To Live
packet_tos = ip_hdr->ip_tos, // Type Of Service
packet_len = ntohs(ip_hdr->ip_len), // header length + data length
packet_hlen = ip_hdr->ip_hl; // header length

// Print it
printf("************************************"
"**************************************\n");
printf("ID: %d | SRC: %s | DST: %s | TOS: 0x%x | TTL: %d\n",
packet_id, packet_srcip, packet_dstip, packet_tos, packet_ttl);
}

Here is the full code until now :

/*
* nsniff.c
*/

#include <arpa/inet.h>
#include <netinet/ip.h>
#include <pcap/pcap.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int link_hdr_length = 0;

void call_me(u_char *user, const struct pcap_pkthdr *pkthdr,
const u_char *packetd_ptr) {
packetd_ptr += link_hdr_length;
struct ip *ip_hdr = (struct ip *)packetd_ptr;

char packet_srcip[INET_ADDRSTRLEN]; // source ip address
char packet_dstip[INET_ADDRSTRLEN]; // destination ip address
strcpy(packet_srcip, inet_ntoa(ip_hdr->ip_src));
strcpy(packet_dstip, inet_ntoa(ip_hdr->ip_dst));
int packet_id = ntohs(ip_hdr->ip_id), // identification
packet_ttl = ip_hdr->ip_ttl, // Time To Live
packet_tos = ip_hdr->ip_tos, // Type Of Service
packet_len = ntohs(ip_hdr->ip_len), // header length + data length
packet_hlen = ip_hdr->ip_hl; // header length

printf("************************************"
"**************************************\n");
printf("ID: %d | SRC: %s | DST: %s | TOS: 0x%x | TTL: %d\n", packet_id,
packet_srcip, packet_dstip, packet_tos, packet_ttl);
}

int main(int argc, char const *argv[]) {
char *device = "enp0s3";
char error_buffer[PCAP_ERRBUF_SIZE];
int packets_count = 5;

pcap_t *capdev = pcap_open_live(device, BUFSIZ, 0, -1, error_buffer);

if (capdev == NULL) {
printf("ERR: pcap_open_live() %s\n", error_buffer);
exit(1);
}

int link_hdr_type = pcap_datalink(capdev);

switch (link_hdr_type) {
case DLT_NULL:
link_hdr_length = 4;
break;
case DLT_EN10MB:
link_hdr_length = 14;
break;
default:
link_hdr_length = 0;
}

if (pcap_loop(capdev, packets_count, call_me, (u_char *)NULL)) {
printf("ERR: pcap_loop() failed!\n");
exit(1);
}

return 0;
}

Now compile and run. To test this program you can run `ping` on another terminal tab and set the ttl to 16 or any number you want :

$ ping 192.168.8.1 -4 -t 16
$ cc -o nsniff nsniff.c -lpcap
$ sudo ./nsniff
**************************************************************************
ID: 55041 | SRC: 192.168.8.1 | DST: 192.168.8.1 | TOS: 0x0 | TTL: 16
**************************************************************************
ID: 55041 | SRC: 192.168.8.4 | DST: 192.168.8.4 | TOS: 0x0 | TTL: 255
**************************************************************************
ID: 55088 | SRC: 192.168.8.1 | DST: 192.168.8.1 | TOS: 0x0 | TTL: 16
**************************************************************************
ID: 55088 | SRC: 192.168.8.4 | DST: 192.168.8.4 | TOS: 0x0 | TTL: 255
**************************************************************************
ID: 55314 | SRC: 192.168.8.1 | DST: 192.168.8.1 | TOS: 0x0 | TTL: 16

Transport layer header

Time to move on to the next header, the transport layer header. Nothing new here we just add the IP header length multiplied by 4 to the packet pointer.

Q: Why do we multiply by 4 ?

The header length (IHL) field of the IP header is a 4-bit field that gives us the header length in 32-bit increments.

The minimum length of an IP header is 20 bytes that means the minimum value that will be returned by ip_hdr->ip_hl is 5. The maximum length of an IP header is 60 bytes, because 4-bits can hold a value up to 15.

For example :

Transport layer header fields depend on the used protocol so we need to type cast our pointer to a specific protocol header. We can get the protocol type from ip_hdr->ip_p.

We are going to need more header files, so add the following to the top of the file :

#include <netinet/in.h>
#include <netinet/ip_icmp.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>

These header files define tcphdr, udphdr and icmp structures in addition to protocols (IPPROTO_) and flags (TH_) definitions. Other protocols exist but we are going to work with tcp, udp and icmp.

We are going to extract a number of fields from the segment depending on the used protocol.

packetd_ptr += (4 * packet_hlen);
int protocol_type = ip_hdr->ip_p;

struct tcphdr* tcp_header;
struct udphdr* udp_header;
struct icmp* icmp_header;
int src_port, dst_port;

switch (protocol_type) {
case IPPROTO_TCP:
tcp_header = (struct tcphdr*)packetd_ptr;
src_port = tcp_header->th_sport;
dst_port = tcp_header->th_dport;
// Extracting SYN, ACK and URG flags
printf("PROTO: TCP | FLAGS: %c/%c/%c | SPORT: %d | DPORT: %d |\n",
(tcp_header->th_flags & TH_SYN ? 'S' : '-'),
(tcp_header->th_flags & TH_ACK ? 'A': '-'),
(tcp_header->th_flags & TH_URG ? 'U': '-'),
src_port, dst_port);
break;
case IPPROTO_UDP:
udp_header = (struct udphdr*)packetd_ptr;
src_port = udp_header->uh_sport;
dst_port = udp_header->uh_dport;
printf("PROTO: UDP | SPORT: %d | DPORT: %d |\n", src_port, dst_port);
break;
case IPPROTO_ICMP:
icmp_header = (struct icmp*) packetd_ptr;
// Get ICMP type and code
int icmp_type = icmp_header->icmp_type;
int icmp_type_code = icmp_header->icmp_code;
printf("PROTO: ICMP | TYPE: %d | CODE: %d |\n", icmp_type, icmp_type_code);
break;
}

Note: If you want to see more protocols refer to the following man page https://www.man7.org/linux/man-pages/man0/netinet_in.h.0p.html.

Below is the full code :

/*
* nsniff.c
*/

#include <netinet/in.h>
#include <netinet/ip_icmp.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <pcap/pcap.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int link_hdr_length = 0;

void call_me(u_char *user, const struct pcap_pkthdr *pkthdr,
const u_char *packetd_ptr) {
packetd_ptr += link_hdr_length;
struct ip *ip_hdr = (struct ip *)packetd_ptr;

char packet_srcip[INET_ADDRSTRLEN];
char packet_dstip[INET_ADDRSTRLEN];
strcpy(packet_srcip, inet_ntoa(ip_hdr->ip_src));
strcpy(packet_dstip, inet_ntoa(ip_hdr->ip_dst));
int packet_id = ntohs(ip_hdr->ip_id),
packet_ttl = ip_hdr->ip_ttl,
packet_tos = ip_hdr->ip_tos,
packet_len = ntohs(ip_hdr->ip_len),
packet_hlen = ip_hdr->ip_hl;

printf("************************************"
"**************************************\n");
printf("ID: %d | SRC: %s | DST: %s | TOS: 0x%x | TTL: %d\n", packet_id,
packet_srcip, packet_dstip, packet_tos, packet_ttl);

packetd_ptr += (4 * packet_hlen);
int protocol_type = ip_hdr->ip_p;

struct tcphdr *tcp_header;
struct udphdr *udp_header;
struct icmp *icmp_header;
int src_port, dst_port;

switch (protocol_type) {
case IPPROTO_TCP:
tcp_header = (struct tcphdr *)packetd_ptr;
src_port = tcp_header->th_sport;
dst_port = tcp_header->th_dport;
printf("PROTO: TCP | FLAGS: %c/%c/%c | SPORT: %d | DPORT: %d |\n",
(tcp_header->th_flags & TH_SYN ? 'S' : '-'),
(tcp_header->th_flags & TH_ACK ? 'A' : '-'),
(tcp_header->th_flags & TH_URG ? 'U' : '-'), src_port, dst_port);
break;
case IPPROTO_UDP:
udp_header = (struct udphdr *)packetd_ptr;
src_port = udp_header->uh_sport;
dst_port = udp_header->uh_dport;
printf("PROTO: UDP | SPORT: %d | DPORT: %d |\n", src_port, dst_port);
break;
case IPPROTO_ICMP:
icmp_header = (struct icmp *)packetd_ptr;
int icmp_type = icmp_header->icmp_type;
int icmp_type_code = icmp_header->icmp_code;
printf("PROTO: ICMP | TYPE: %d | CODE: %d |\n", icmp_type, icmp_type_code);
break;
}
}

int main(int argc, char const *argv[]) {
char *device = "enp0s3";
char error_buffer[PCAP_ERRBUF_SIZE];
int packets_count = 10;

pcap_t *capdev = pcap_open_live(device, BUFSIZ, 0, -1, error_buffer);

if (capdev == NULL) {
printf("ERR: pcap_open_live() %s\n", error_buffer);
exit(1);
}

int link_hdr_type = pcap_datalink(capdev);

switch (link_hdr_type) {
case DLT_NULL:
link_hdr_length = 4;
break;
case DLT_EN10MB:
link_hdr_length = 14;
break;
default:
link_hdr_length = 0;
}

if (pcap_loop(capdev, packets_count, call_me, (u_char *)NULL)) {
printf("ERR: pcap_loop() failed!\n");
exit(1);
}

return 0;
}

Now let compile and run :

$ cc -o nsniff nsniff.c -lpcap
$ sudo ./nsniff
**************************************************************************
ID: 520 | SRC: 192.168.8.4 | DST: 192.168.8.4 | TOS: 0x0 | TTL: 255
PROTO: TCP | FLAGS: -/A/- | SPORT: 20480 | DPORT: 52940 |
**************************************************************************
ID: 29533 | SRC: 216.58.204.131 | DST: 216.58.204.131 | TOS: 0x0 | TTL: 64
PROTO: TCP | FLAGS: -/A/- | SPORT: 52940 | DPORT: 20480 |
**************************************************************************
ID: 37721 | SRC: 198.252.206.25 | DST: 198.252.206.25 | TOS: 0x0 | TTL: 64
PROTO: TCP | FLAGS: -/A/- | SPORT: 42729 | DPORT: 47873 |
**************************************************************************
ID: 521 | SRC: 192.168.8.4 | DST: 192.168.8.4 | TOS: 0x0 | TTL: 255
PROTO: TCP | FLAGS: -/A/- | SPORT: 47873 | DPORT: 42729 |
**************************************************************************
ID: 522 | SRC: 192.168.8.4 | DST: 192.168.8.4 | TOS: 0x0 | TTL: 255
PROTO: UDP | SPORT: 47873 | DPORT: 43953 |
**************************************************************************
ID: 523 | SRC: 192.168.8.4 | DST: 192.168.8.4 | TOS: 0x0 | TTL: 255
PROTO: TCP | FLAGS: -/A/- | SPORT: 47873 | DPORT: 42729 |
**************************************************************************
ID: 37722 | SRC: 198.252.206.25 | DST: 198.252.206.25 | TOS: 0x0 | TTL: 64
PROTO: TCP | FLAGS: -/A/- | SPORT: 42729 | DPORT: 47873 |
**************************************************************************
ID: 0 | SRC: 142.250.180.168 | DST: 142.250.180.168 | TOS: 0x0 | TTL: 64
PROTO: UDP | SPORT: 43953 | DPORT: 47873 |
**************************************************************************
ID: 37723 | SRC: 198.252.206.25 | DST: 198.252.206.25 | TOS: 0x0 | TTL: 64
PROTO: TCP | FLAGS: -/A/- | SPORT: 42729 | DPORT: 47873 |
**************************************************************************
ID: 524 | SRC: 192.168.8.4 | DST: 192.168.8.4 | TOS: 0x0 | TTL: 255
PROTO: UDP | SPORT: 47873 | DPORT: 57738 |

Now we have a packet sniffer that shows us useful information, but what if we want to capture certain packets ? like TCP packets or what if we want packets originating from a certain ip address ? this can be done via filters.

In the next part we will implement filtering so we can get the packets we want, and we are also going to take a look at libpcap statistics.

--

--