Yo. Dans ce post je vais expliquer comment je me suis battu pour faire un
truc assez simple avec la libpcap sous linux.
Au début était un sniffer, je voulais écouter sur une interface à grands
coups de SOCK_RAW + recvfrom(). À l'ancienne, quoi. L'idée était de ne pas
utiliser de handler pcap (pcap_t *). Au bout d'un moment je me dis "tiens, ça
serait sympa d'ajouter les filtres BPF comme dans tcpdump". Brillante idée,
non ? Non, ok. Bref, le code de départ est le suivant :
// -*- c-basic-offset: 4; indent-tabs-mode: nil -*-
// vim:sw=4 ts=4 sts=4 expandtab
#include <limits.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <netinet/in.h>
#include <linux/if_ether.h>
#include <arpa/inet.h>
#include <netpacket/packet.h>
#include <poll.h>
#include <linux/types.h>
#include <errno.h>
#include <pcap.h>
#include <linux/filter.h>
#define FRAME_SIZE 65536
/* iface_setprom() sets "device" in promiscuous mode */
static int
iface_setprom(int sock_fd, const char *device)
{
struct ifreq ifr;
memset(&ifr, 0, sizeof ifr);
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", device);
if (ioctl(sock_fd, SIOCGIFFLAGS, &ifr) == -1) {
printf("[%s] ioctl: %s.\n", device, strerror(errno));
return -1;
}
ifr.ifr_flags |= (IFF_PROMISC | IFF_UP);
if (ioctl(sock_fd, SIOCSIFFLAGS, &ifr) == -1) {
printf("[%s] ioctl: %s.\n", device, strerror(errno));
return -1;
}
printf("[%s] enter in promiscuous mode\n", device);
return 0;
}
À partir d'une chaîne de caractère entrée, par exemple "tcp and port 22", on
veut obtenir une structure comprise par pcap_compile*(). Comme dit
précédemment, je ne veux pas de pcap_t* à manipuler, je vais donc utiliser
pcap_compile_nopcap() (non mentionné dans le man de pcap_compile sous linux,
évidemment).
/* iface_setfilter() applies a LSF filter to the file descriptor */
static int
iface_setfilter(int sock_fd, const char * const device, const char * const filter)
{
if (!filter || !*filter) {
printf("[%s] no filter to install\n", device);
return 0;
}
struct bpf_program bpf;
memset(&bpf, 0, sizeof bpf);
char errbuf[PCAP_ERRBUF_SIZE = "";
if (-1 == pcap_compile_nopcap(FRAME_SIZE, DLT_RAW, &bpf, filter, 1, 0)) {
printf("[%s] pcap_compile_nopcap failed for '%s'\n", device, filter);
return -1;
}
if (-1 == setsockopt(sock_fd, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof bpf)) {
printf("[%s] setsockopt: %s (filter '%s')\n", device, strerror(errno), filter);
return -1;
}
printf("[%s] filter '%s' successfully installed\n", device, filter);
return 0;
}
/* iface_init() is the main device initialization function */
static int
iface_init(struct pollfd *fds, const char * const device, const char * const filter)
{
printf("Initializing interface %s (filter '%s')\n", device, filter);
fds->fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (fds->fd == -1) {
printf("[%s] socket: %s. Exiting...\n", device, strerror(errno));
exit(EXIT_FAILURE);
}
fds->events = POLLIN | POLLPRI;
if (0 != iface_setprom(fds->fd, device)) return -1;
if (0 != iface_bind(fds->fd, device)) return -1;
if (0 != iface_setfilter(fds->fd, device, filter)) return -1;
return 0;
}
static void
frame_manager(const char * const device, struct pollfd *fd, struct timeval const *now)
{
uint32_t i;
if ((fd->revents & (POLLIN | POLLPRI)) == 0)
return;
unsigned char buf[1500] = "";
ssize_t n = recvfrom(fd->fd, buf, sizeof buf, MSG_DONTWAIT, NULL, NULL);
if (n < 0) {
printf("[%s] recvfrom: %s (i=%d, fd=%d)\n", device, strerror(errno), i, fd->fd);
return;
}
printf("[%d,%d] packet received\n", (int)now->tv_sec, (int)now->tv_usec);
}
int main(int ac, char **av)
{
char *filter = "";
char *device = "dummy0";
if (ac > 1) {
filter = av[1];
}
struct pollfd fd;
(void)iface_init(&fd, device, filter);
while (/*CONSTCOND*/ 1) {
if (poll(&fd, 1, -1) > 0) {
struct timeval now;
gettimeofday(&now, NULL);
frame_manager(device, &fd, &now);
}
}
close(fd.fd);
return EXIT_SUCCESS;
}
Tout d'abord, je monte une interface virtuelle pour éviter le bruit généré
par les paquets circulants sur mes vraies interfaces :
$ sudo modprobe dummy
$ sudo ifconfig dummy0 mtu 1600 up
Maintenant, si je compile et j'exécute le code, et que je balance du ssh sur
dummy0, j'ai ça :
$ cc -o cap cap.c -lpcap
$ sudo ./cap "tcp and port 22"
[...]
Et dans un autre terminal :
$ sudo tcpreplay -i dummy0 ssh.pcap
Mais aucun "packet received" ne s'affiche. Damned ! En inspectant un
peu le code, on voit ça :
if (-1 == setsockopt(sock_fd, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof bpf)) {
Ça a l'air bien comme ça, hein. Malheureusement, le 'struct bpf_program'
n'est pas compris sous linux, qui, ne voulant pas faire comme ses potes *BSD
attend un 'struct sock_fprog *' en avant dernier argument. Bon, ici je me dis
"il doit bien y avoir une fonction qui fait la conversion, cherchons dans le
source de libpcap". En jetant un coup d'oeil rapide, on tombe sur
ça :
libpcap-0.9.8/pcap-linux.c:215 static int fix_program(pcap_t *handle, struct sock_fprog *fcode);
Je suis content, mais visiblement je dois passer par un pcap_t. Du coup, je
reprends le code en l'adaptant pour taper directement dans la structure interne
du handler pcap, et j'écris la fonction suivante :
#define SLL_HDR_LEN 16
static int lcc = 0; // linux cooked capture
static int
fix_offset(struct bpf_insn *p)
{
if (p->k >= SLL_HDR_LEN) {
p->k -= SLL_HDR_LEN;
} else if (p->k == 14) {
p->k = SKF_AD_OFF + SKF_AD_PROTOCOL;
} else {
return -1;
}
return 0;
}
static int
fix_code(struct bpf_program *bpf, struct sock_fprog *fcode)
{
size_t prog_size;
register int i;
register struct bpf_insn *p;
struct bpf_insn *f;
int len;
len = bpf->bf_len;
prog_size = sizeof(bpf->bf_insns) * len;
f = malloc(prog_size);
if (f == NULL) {
printf("malloc: %s\n", strerror(errno));
return -1;
}
memcpy(f, bpf->bf_insns, prog_size);
fcode->len = len;
fcode->filter = (struct sock_filter *)f;
for (i = 0; i < len; ++i) {
p = &f[i];
switch (BPF_CLASS(p->code)) {
case BPF_RET:
if (BPF_MODE(p->code) == BPF_K) {
if (p->k != 0)
p->k = 65535;
}
break;
case BPF_LD:
case BPF_LDX:
switch (BPF_MODE(p->code)) {
case BPF_ABS:
case BPF_IND:
case BPF_MSH:
if (lcc) {
if (fix_offset(p) < 0) {
return 0;
}
}
break;
}
break;
}
}
return 1; /* success! */
}
Maintenant je réécris ma fonction iface_setfilter() pour convertir le code
BPF :
static int
iface_setfilter(int sock_fd, const char * const device, const char * const filter)
{
if (! filter || ! *filter) {
printf("[%s] no filter to install\n", device);
return 0;
}
struct bpf_program bpf;
memset(&bpf, 0, sizeof bpf);
if (-1 == pcap_compile_nopcap(FRAME_SIZE, DLT_RAW, &bpf, filter, 1, 0)) {
printf("[%s] pcap_compile_nopcap failed for '%s'\n", device, filter);
return -1;
}
struct sock_fprog fcode;
memset(&fcode, 0, sizeof fcode);
int ret = fix_code(&bpf, &fcode);
if (1 != ret) {
printf("[%s] fix_code failed (rcode = %d) for filter '%s'\n", device, ret, filter);
return -1;
} else {
printf("[%s] fix_code worked (rcode = %d) for filter '%s'\n", device, ret, filter);
}
if (-1 == setsockopt(sock_fd, SOL_SOCKET, SO_ATTACH_FILTER, &fcode, sizeof fcode)) {
printf("[%s] setsockopt: %s (filter '%s')\n", device, strerror(errno), filter);
return -1;
}
printf("[%s] filter '%s' successfully installed\n", device, filter);
return 0;
}
Là, logiquement je me dis "Youpi les knakis¸ ça va marcher !". Mais si je
reteste, toujours rien. Rien ne s'affiche. La blague est même que si
j'invoque :
$ sudo ./cap "not port 80"
... et que je joue du traffic http (sur le port 80 bien sûr), mon outil
logue qu'il a vu un paquet ! Par contre avec "port 80" il n'affiche rien.
Bon je me suis pas mal gratté la tête ici, j'avoue. Du coup j'appelle mon
copain strace à la rescousse, et je compare le raw des filtres passés à
setsockopt() entre mon code et l'appel tcpdump :
$ sudo strace -s 256 -f -v ./cap "port 80" 2>&1 | grep ATTACH_FILTER
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, "\33\0\0\0\0\0\0\0\20\240\363\0\0\0\0\0", 16) = 0
^C
$ sudo strace -s 256 -f -v tcpdump -ni dummy0 port 80 2>&1 | grep ATTACH_FILTER
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, "\1\0\0\0\0\0\0\0h\303\315E(\177\0\0", 16) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, "\30\0\0\0\0\0\0\0\360V1G(\177\0\0", 16) = 0
^C
Les outputs ne sont pas les mêmes. La génération du filtre est donc
foireuse, je vérifie maintenant comme suit :
$ sudo tcpdump -nd port 80
(000) ldh [12]
(001) jeq #0x86dd jt 2 jf 10
(002) ldb [20]
(003) jeq #0x84 jt 6 jf 4
(004) jeq #0x6 jt 6 jf 5
(005) jeq #0x11 jt 6 jf 23
(006) ldh [54]
(007) jeq #0x50 jt 22 jf 8
(008) ldh [56]
(009) jeq #0x50 jt 22 jf 23
(010) jeq #0x800 jt 11 jf 23
(011) ldb [23]
(012) jeq #0x84 jt 15 jf 13
(013) jeq #0x6 jt 15 jf 14
(014) jeq #0x11 jt 15 jf 23
(015) ldh [20]
(016) jset #0x1fff jt 23 jf 17
(017) ldxb 4*([14]&0xf)
(018) ldh [x + 14]
(019) jeq #0x50 jt 22 jf 20
(020) ldh [x + 16]
(021) jeq #0x50 jt 22 jf 23
(022) ret #96
(023) ret #0
Et j'invoque
if (-1 == pcap_compile_nopcap(FRAME_SIZE, DLT_RAW, &bpf, filter, 1, 0)) {
printf("[%s] pcap_compile_nopcap failed for '%s'\n", device, filter);
return -1;
}
bpf_dump(&bpf, 0);
J'ai en sortie :
Initializing interface dummy0 (filter 'port 80')
[dummy0] enter in promiscuous mode
[dummy0] bind succeed: index 1634 <--> dummy0
(000) ldb [0]
(001) and #0xf0
(002) jeq #0x60 jt 3 jf 11
(003) ldb [6]
(004) jeq #0x84 jt 7 jf 5
(005) jeq #0x6 jt 7 jf 6
(006) jeq #0x11 jt 7 jf 26
(007) ldh [40]
(008) jeq #0x50 jt 25 jf 9
(009) ldh [42]
(010) jeq #0x50 jt 25 jf 26
(011) ldb [0]
(012) and #0xf0
(013) jeq #0x40 jt 14 jf 26
(014) ldb [9]
(015) jeq #0x84 jt 18 jf 16
(016) jeq #0x6 jt 18 jf 17
(017) jeq #0x11 jt 18 jf 26
(018) ldh [6]
(019) jset #0x1fff jt 26 jf 20
(020) ldxb 4*([0]&0xf)
(021) ldh [x + 0]
(022) jeq #0x50 jt 25 jf 23
(023) ldh [x + 2]
(024) jeq #0x50 jt 25 jf 26
(025) ret #65536
(026) ret #0
[dummy0] fix_code worked (rcode = 1) for filter 'port 80'
[dummy0] filter 'port 80' successfully installed
^C
On voit bien que c'est différent au début. Le reste est plutôt
cohérent
(016) jeq #0x6 jt 18 jf 17 # 0x06 c'est l'IPPROTO_TCP (6)
(017) jeq #0x11 jt 18 jf 26 # 0x11 = 17 = IPPROTO_UDP
Si on n'est dans aucun des deux cas, on saute en 26 (jf 26), soit la fin.
Sinon, on regarde si le port vaut 80 (0x50) :
(022) jeq #0x50 jt 25 jf 23
(023) ldh [x + 2]
(024) jeq #0x50 jt 25 jf 26
Le problème est vraiment le prologue. Et là je on me souffle à l'oreille que
c'est peut-être le datalink type. Réaction: "bon dieu de !#@@!#@!# mais c'est
bien sûr, ça doit être mon datalink (DLT) qui est moisi !" Si
j'écris :
if (-1 == pcap_compile_nopcap(FRAME_SIZE, DLT_IEEE802, &bpf, filter, 1, 0)) {
J'obtiens en sortie :
$ cc -o cap cap.c -lpcap && sudo ./cap "port 80"
Initializing interface dummy0 (filter 'port 80')
[dummy0] enter in promiscuous mode
[dummy0] bind succeed: index 1634 <--> dummy0
(000) ldh [20]
(001) jeq #0x86dd jt 2 jf 10 # 0x86dd est l'ethertype IPv6
(002) ldb [28]
(003) jeq #0x84 jt 6 jf 4
(004) jeq #0x6 jt 6 jf 5
(005) jeq #0x11 jt 6 jf 23
(006) ldh [62]
(007) jeq #0x50 jt 22 jf 8
(008) ldh [64]
(009) jeq #0x50 jt 22 jf 23
(010) jeq #0x800 jt 11 jf 23 # 0x800 est l'ethertype IPv4
[...]
Mais toujours rien n'est logué si je joue du http ! En remplaçant
DLT_IEEE802 par DLT_EN10MB en revanche...
[...]
[dummy0] enter in promiscuous mode
[dummy0] bind succeed: index 1634 <--> dummy0
[dummy0] fix_code worked (rcode = 1) for filter 'port 80'
[dummy0] filter 'port 80' successfully installed
[1286457559,615193] packet received
Hallelujah ! Hosanna hosanna !
Enfin bon, c'est quand même moche d'écrire le DLT en dur dans le code, donc
je l'ai récupéré via la libpcap (mais on est obligé de passer par un handler
pcap_t *, ce que je ne voulais pas trop au début...)
char errbuf[256] = "";
pcap_t *p = pcap_open_live(device, FRAME_SIZE, 1, 0, errbuf);
if (! p) {
printf("[%s] pcap_open_live failed: %s\n", device, errbuf);
return -1;
}
int dlt = pcap_datalink(p);
pcap_close(p);
if (-1 == pcap_compile_nopcap(FRAME_SIZE, dlt, &bpf, filter, 1, 0)) {
printf("[%s] pcap_compile_nopcap failed for '%s'\n", device, filter);
return -1;
}
En fait, comme on dispose du handler pcap_t, on peut même dégager
pcap_compile_nopcap() et utiliser pcap_compile(), qui lui n'a même pas besoin
d'un DLT en paramètre... Tout ça parce que j'ai voulu me passer de ce foutu
handler, en somme. Bref le code est disponible ici