Setting the Device

Có thể nhận vào tên network interface thông qua CLI argument như sau:

#include <pcap.h>
#include <stdio.h>
 
int main(int argc, char *argv[])
{
	char *dev = argv[1];
 
	printf("Device: %s\n", dev);
	return(0);
}

Biên dịch và chạy:

root@docker-desktop:/volumes# gcc -o sniffer sniffer.c -lpcap
root@docker-desktop:/volumes# ./sniffer eth0
Device: eth0

Cũng có thể sử dụng hàm pcap_lookupdev() để tìm network interface mặc định như sau:

#include <pcap.h>
#include <stdio.h>
#include <stdlib.h>
 
int main(int argc, char *argv[])
{
	char *dev, errbuf[PCAP_ERRBUF_SIZE];
 
	dev = pcap_lookupdev(errbuf);
	if (dev == NULL) {
		fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
		return(2);
	}
	printf("Device: %s\n", dev);
	return(0);
}

Với errbuf là buffer lưu thông điệp lỗi của hàm pcap_lookupdev().

Biên dịch và chạy:

root@docker-desktop:/volumes# gcc -o sniffer sniffer.c -lpcap
root@docker-desktop:/volumes# ./sniffer
Device: br-88ab64b587ba

Opening the Device for Sniffing

Chúng ta sẽ sử dụng hàm pcap_open_live() để mở một session để lắng nghe. Prototype của hàm này có dạng như sau:

pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf)

Với:

  • char *device: tên của network interface mà ta cần lắng nghe.
  • int snaplen: số lượng byte tối đa có thể capture bởi pcap.
  • int promisc: nếu được set thành 1 thì sẽ lắng nghe ở chế độ promiscuous.
  • int to_ms: read time out có đơn vị là mili giây.
  • char *ebuf: chuỗi lưu thông điệp lỗi (tương tự như hàm pcap_lookupdev()).

Giá trị trả về là một session handle có kiểu là pcap_t *.

Note

Nếu không lắng nghe ở chế độ promiscuous, chỉ những gói tin được gửi đi, gửi đến hoặc định tuyến thông qua host mới được capture. Ngược lại, khi lắng nghe ở chế độ promiscuous, tất cả traffic đều sẽ được capture.

Info

Để sử dụng chế độ promiscuous thì ta cần quyền root.

Có thể kiểm tra xem một interface nào đó có đang ở trong chế độ promiscuos hay không bằng lệnh sau:

$ ip -d link show dev br-88ab64b587ba
5: br-88ab64b587ba: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:4b:60:b2:90 brd ff:ff:ff:ff:ff:ff promiscuity 1 minmtu 68 maxmtu 65535

Nếu giá trị promiscuity là 1 thì có nghĩa là interface đang ở trong chế độ promiscuos.

Ví dụ sử dụng hàm pcap_open_live():

#include <pcap.h>
...
pcap_t *handle;
 
handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL) {
	fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
	return(2);
}

Trong ví dụ trên, ta sẽ lắng nghe network interface được lưu trong chuỗi dev, capture BUFSIZ byte, chạy ở chế độ promiscuous với read time out là 1 giây.

Không phải thiết bị nào cũng dùng một loại link-layer header1 giống nhau. Do đó, ta cần xác định loại link-layer header bằng hàm pcap_datalink() mà thiết bị cung cấp để có thể xử lý các gói tin.

Filtering Traffic

Filter ở trong pcap có cú pháp pcap-filter và được lưu trong một chuỗi thông thường (mảng char). Trước khi áp dụng filter, ta cần biên dịch nó bằng hàm pcap_compile(), có prototype như sau:

int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)

Với:

  • pcap_t *p là session handle được tạo ra thông qua hàm pcap_open_live().
  • struct bpf_program *fp: con trỏ trỏ đến struct lưu filter sau khi được biên dịch.
  • char *str: filter.
  • int optimize: giá trị 1 cho biết filter sẽ được tối ưu.
  • bpf_u_int32 netmask: subnet mask của network mà ta áp dụng filter.

Hàm pcap_compile() sẽ trả về -1 nếu xảy ra lỗi.

Sau khi biên dịch filter, ta sẽ dùng hàm pcap_setfilter() để áp dụng. Hàm này có prototype như sau:

int pcap_setfilter(pcap_t *p, struct bpf_program *fp)

Dễ thấy, tham số đầu tiên là session handle còn tham số thứ hai là con trỏ trỏ đến struct lưu filter đã được biên dịch.

Ví dụ:

#include <pcap.h>
...
pcap_t *handle;                /* Session handle */
char dev[] = "rl0";            /* Device to sniff on */
char errbuf[PCAP_ERRBUF_SIZE]; /* Error string */
struct bpf_program fp;         /* The compiled filter expression */
char filter_exp[] = "port 23"; /* The filter expression */
bpf_u_int32 mask;              /* The netmask of our sniffing device */
bpf_u_int32 net;               /* The IP of our sniffing device */
 
if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1)
{
    fprintf(stderr, "Can't get netmask for device %s\n", dev);
    net = 0;
    mask = 0;
}
handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL)
{
    fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
    return (2);
}
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);
}

Ví dụ trên lắng nghe các traffic đi đến hoặc đi ra port 23 trên interface rl0 ở chế độ promiscuous. Hàm pcap_lookupnet() trong ví dụ trên sẽ giúp trả về một trong số những địa chỉ IP và subnet mask tương ứng của interface.

The Actual Sniffing

Có hai cách để capture các gói tin: capture từng gói hoặc dùng vòng lặp để chờ n gói rồi mới capture. Chúng ta sẽ sử dụng hàm pcap_next() để làm điều này. Prototype của hàm có dạng như sau:

u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)

Tham số đầu tiên là session handle còn tham số thứ hai là con trỏ trỏ đến cấu trúc có chứa các thông tin chung của gói tin.

Ví dụ:

#include <pcap.h>
#include <stdlib.h>
#include <stdio.h>
 
int main(int argc, char *argv[])
{
	pcap_t *handle;			/* Session handle */
	char *dev;			/* The device to sniff on */
	char errbuf[PCAP_ERRBUF_SIZE];	/* Error string */
	struct bpf_program fp;		/* The compiled filter */
	char filter_exp[] = "port 23";	/* The filter expression */
	bpf_u_int32 mask;		/* Our netmask */
	bpf_u_int32 net;		/* Our IP */
	struct pcap_pkthdr header;	/* The header that pcap gives us */
	const u_char *packet;		/* The actual packet */
 
	/* Define the device */
	dev = pcap_lookupdev(errbuf);
	if (dev == NULL) {
		fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
		return(2);
	}
	/* Find the properties for the device */
	if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
		fprintf(stderr, "Couldn't get netmask for device %s: %s\n", dev, errbuf);
		net = 0;
		mask = 0;
	}
	/* Open the session in promiscuous mode */
	handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
	if (handle == NULL) {
		fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
		return(2);
	}
	/* Compile and apply the filter */
	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);
	}
	/* Grab a packet */
	packet = pcap_next(handle, &header);
	/* Print its length */
	printf("Jacked a packet with length of [%d]\n", header.len);
	/* And close the session */
	pcap_close(handle);
	return(0);
}

Tuy nhiên, ta thường sử dụng hàm pcap_loop() và hàm pcap_dispatch() để bắt gói tin hơn là hàm pcap_next(). Hai hàm này sẽ dùng một callback để xử lý gói tin bắt được. Prototype của hàm pcap_loop():

int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)

Với:

  • Tham số đầu tiên là session handle.
  • int cnt cho biết ta sẽ lắng nghe bao nhiêu gói tin. Nếu dùng giá trị -1 thì nó sẽ lắng nghe đến khi nào xảy ra lỗi.
  • pcap_handler callback là callback để xử lý gói tin.
  • u_char *user được dùng để chứa các đối số cần truyền vào callback function.

Hàm pcap_dispatch() cũng tương tự hàm pcap_loop() nhưng nó chỉ xử lý một số gói tin ban đầu chứ không lặp như pcap_loop().

Prototype của callback truyền vào pcap_loop()pcap_dispatch():

void got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet);

Với:

  • Tham số đầu tiên sẽ nhận giá trị truyền vào tham số cuối cùng của pcap_loop().

  • const struct pcap_pkthdr *header là header của pcap. Cấu trúc pcap_pkthdr được định nghĩa trong pcap.h như sau:

    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) */
    };
  • const u_char *packet là con trỏ trỏ đến dạng serialized của các cấu trúc đại diện cho các layer trong gói tin (Ethernet, IP, TCP, …).

Để phân tách gói tin, trước tiên, ta cần định nghĩa cấu trúc của các gói tin. Ví dụ sau định nghĩa cấu trúc của gói tin TCP/IP:

#include <netinet/in.h>
 
/* 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 */
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 */
};

Sau khi có các cấu trúc thì ta khai báo các con trỏ:

/* 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;

Và thực hiện phân tách gói tin:

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);

Có thể thấy, các thành phần trong gói tin được truy xuất thông qua con trỏ packet cùng với một offset cụ thể nào đó.

Giả sử thành phần Ethernet nằm ở vị trí X trong bộ nhớ, vị trí của thành phần khác có dạng như sau:

VariableLocation (in bytes)
sniff_ethernetX
sniff_ipX + SIZE_ETHERNET
sniff_tcpX + SIZE_ETHERNET + {IP header length}
payloadX + SIZE_ETHERNET + {IP header length} + {TCP header length}
list
from outgoing([[Programming with pcap]])
sort file.ctime asc

Resources

Footnotes

  1. danh sách các loại link-layer header: Link-layer header types | TCPDUMP & LIBPCAP