Перехват сетевого трафика при помощи библиотеки libpcap

28 сентября 2016

При отладке программ бывает удобно посмотреть, что они передают по сети. Для решения этой задачи существует множество программ-снифферов. Проблема в том, что они бывают не слишком удобны. Так, tcpdump всегда выводит IP- и TCP-заголовки пакетов, а мне они не всегда интересны. Этого недостатка лишен tcpflow, однако он не работает с UDP и имеет больно уж широкий вывод в консоль. Столкнувшись с этим в очередной раз, я решил написать маленький сниффер, который был бы удобен лично мне. Оказалось, что пишутся снифферы очень просто.

Примечание: Кое-кто из вас может помнить, что однажды я уже писал свой сниффер. Существенный недостаток этого сниффера заключается в том, что он имеет графический интерфейс и написан на Perl. Я же в последнее время предпочитаю консольные утилиты, а из языков — либо Python, либо чистый C.

Итак, код сниффера получилcя следующим:

/*
 * eaxsniff - libpcap usage example
 * (c) Aleksander Alekseev 2016 | http://eax.me/
 */


#include <pcap/pcap.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "include/net_headers.h"

#include <string.h>
#include <stdio.h>
#include <stdlib.h>

#define UNUSED(x) ((void)(x))

#define PRINT_BYTES_PER_LINE 16

static void
print_data_hex(const uint8_t* data, int size)
{
    int offset = 0;
    int nlines = size / PRINT_BYTES_PER_LINE;
    if(nlines * PRINT_BYTES_PER_LINE < size)
        nlines++;

    printf("        ");

    for(int i = 0; i < PRINT_BYTES_PER_LINE; i++)
        printf("%02X ", i);

    printf("\n\n");

    for(int line = 0; line < nlines; line++)
    {
        printf("%04X    ", offset);
        for(int j = 0; j < PRINT_BYTES_PER_LINE; j++)
        {
            if(offset + j >= size)
                printf("   ");
            else
                printf("%02X ", data[offset + j]);
        }

        printf("   ");

        for(int j = 0; j < PRINT_BYTES_PER_LINE; j++)
        {
            if(offset + j >= size)
                printf(" ");
            else if(data[offset + j] > 31 && data[offset + j] < 127)
                printf("%c", data[offset + j]);
            else
                printf(".");
        }

        offset += PRINT_BYTES_PER_LINE;
        printf("\n");
    }
}

static void
handle_packet(uint8_t* user, const struct pcap_pkthdr *hdr,
                const uint8_t* bytes)
{
    UNUSED(user);

    struct iphdr* ip_header = (struct iphdr*)(bytes +
                                              sizeof(struct ethhdr));
    struct sockaddr_in  source, dest;

    memset(&source, 0, sizeof(source));
    memset(&dest, 0, sizeof(dest));
    source.sin_addr.s_addr = ip_header->saddr;
    dest.sin_addr.s_addr = ip_header->daddr;

    char source_ip[128];
    char dest_ip[128];
    strncpy(source_ip, inet_ntoa(source.sin_addr), sizeof(source_ip));
    strncpy(dest_ip, inet_ntoa(dest.sin_addr), sizeof(dest_ip));

    int source_port = 0;
    int dest_port = 0;
    int data_size = 0;
    int ip_header_size = ip_header->ihl * 4;
    char* next_header = (char*)ip_header + ip_header_size;

    if(ip_header->protocol == IP_HEADER_PROTOCOL_TCP)
    {
        struct tcphdr* tcp_header = (struct tcphdr*)next_header;
        source_port = ntohs(tcp_header->source);
        dest_port = ntohs(tcp_header->dest);
        int tcp_header_size = tcp_header->doff * 4;
        data_size = hdr->len - sizeof(struct ethhdr) -
                        ip_header_size - tcp_header_size;
    }
    else if(ip_header->protocol == IP_HEADER_PROTOCOL_UDP)
    {
        struct udphdr* udp_header = (struct udphdr*)next_header;
        source_port = ntohs(udp_header->source);
        dest_port = ntohs(udp_header->dest);
        data_size = hdr->len - sizeof(struct ethhdr) -
                        ip_header_size - sizeof(struct udphdr);
    }

    printf("\n%s:%d -> %s:%d, %d (0x%x) bytes\n\n",
        source_ip, source_port, dest_ip, dest_port,
        data_size, data_size);

    if(data_size > 0)
    {
        int headers_size = hdr->len - data_size;
        print_data_hex(bytes + headers_size, data_size);
    }
}

void
list_devs()
{
    int errcode;
    pcap_if_t *alldevs, *currdev;
    char errbuff[PCAP_ERRBUF_SIZE];

    errcode = pcap_findalldevs(&alldevs, errbuff);
    if(errcode != 0)
    {
        fprintf(stderr, "pcap_findalldevs failed: %s\n", errbuff);
        return;
    }

    currdev = alldevs;

    while(currdev)
    {
        printf("%s\t%s\n", currdev->name,
            currdev->description ? currdev->description :
                                   "(no description)"
        );
        currdev = currdev->next;
    }

    if(alldevs)
        pcap_freealldevs(alldevs);
}

int
main(int argc, char* argv[])
{
    int res;

    if((argc < 3) && !((argc == 2) &&
                       (strcmp(argv[1], "--list-devs") == 0)))
    {
        printf("Usage: %s device filter\n"
               "       %s --list-devs\n",
               argv[0], argv[0]);
        printf("Example: %s eth0 'udp src or dst port 53'\n", argv[0]);
        printf("%s\n", pcap_lib_version());
        return 1;
    }

    if(argc == 2)
    {
        list_devs();
        return 0;
    }

    const char* device = argv[1];
    const char* filter = argv[2];
    char errbuf[PCAP_ERRBUF_SIZE];

    pcap_t* pcap = pcap_open_live(device, 65535, 1, 100, errbuf);
    if(pcap == NULL)
    {
        fprintf(stderr, "pcap_open_live failed: %s\n", errbuf);
        return 1;
    }

    struct bpf_program filterprog;
    res = pcap_compile(pcap, &filterprog, filter, 0,
                       PCAP_NETMASK_UNKNOWN);
    if(res != 0)
    {
        fprintf(stderr, "pcap_compile failed: %s\n",
                pcap_geterr(pcap));
        pcap_close(pcap);
        return 1;
    }

    res = pcap_setfilter(pcap, &filterprog);
    if(res != 0)
    {
        fprintf(stderr, "pcap_setfilter failed: %s\n",
                pcap_geterr(pcap));
        pcap_close(pcap);
        return 1;
    }

    printf("Listening %s, filter: %s...\n", device, filter);

    res = pcap_loop(pcap, -1, handle_packet, NULL);
    printf("pcap_loop returned %d\n", res);

    pcap_close(pcap);
    return 0;
}

На мой взгляд, особого rocket science здесь не происходит, поэтому нудно расписывать, какая строчка что делает, я не стану. Вы можете найти все интересующие вас подробности в man 3 pcap. Интересно, что определение заголовков Ethernet-, IP-, TCP- и UDP-пакетов в Linux и FreeBSD сильно различается. Соответствующие им структуры и поля этих структур имеют разные названия, не говоря уже о том, что определение структур находится в разных файлах. Чтобы код был переносимым, мне пришлось самостоятельно описать эти структуры в заголовочном файле net_headers.h.

Пример использования сниффера:

$ sudo ./eaxsniff eth0 'udp src or dst port 53'
Listening eth0, filter: udp src or dst port 53...

В соседнем терминале:

$ dig @8.8.8.8 eax.me

; <<>> DiG 9.10.3-P4-Ubuntu <<>> @8.8.8.8 eax.me
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51361
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;eax.me.                IN  A

;; ANSWER SECTION:
eax.me.         1026    IN  A   212.193.240.242

;; Query time: 24 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Fri Jul 29 11:44:01 MSK 2016
;; MSG SIZE  rcvd: 51

При этом в первом терминале увидим что-то вроде:

192.168.27.178:41787 -> 8.8.8.8:53, 35 (0x23) bytes

      00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

0000  C8 A1 01 20 00 01 00 00 00 00 00 01 03 65 61 78  ... .........eax
0010  02 6D 65 00 00 01 00 01 00 00 29 10 00 00 00 00  .me.......).....
0020  00 00 00                                         ...            

8.8.8.8:53 -> 192.168.27.178:41787, 51 (0x33) bytes

      00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

0000  C8 A1 81 80 00 01 00 01 00 00 00 01 03 65 61 78  .............eax
0010  02 6D 65 00 00 01 00 01 C0 0C 00 01 00 01 00 00  .me.............
0020  04 02 00 04 D4 C1 F0 F2 00 00 29 02 00 00 00 00  ..........).....
0030  00 00 00                                         ...

Похоже, работает! Полную версию исходников, а также инструкцию по их сборке, вы найдете в этом репозитории на GitHub. Понятно, что, помимо озвученной отладки приложений, libpcap открывает для нас безграничные возможности по изучению сетевых протоколов, а также всевозможному перехвату веб-форм, паролей и вот этого всего. Кроме того, вы можете подменять пакеты. Детали можно найти в man 3 pcap_inject.

Дополнение: Вас также могут заинтересовать статьи Изучаем Ethernet-фреймы с помощью осциллографа и Снифинг Ethernet-трафика с платой Throwing Star.

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.