使用Libpcap捕获局域网中的数据包

2023-11-23 11:17:07 浏览数 (2)

头文件与报头结构

包含相关的头文件和以太网帧头部、IP头、TCP头、UDP头部结构。

代码语言:javascript复制
#include <stdio.h>
#include <stdlib.h>
#include <pcap.h>
#include <arpa/inet.h>
#include <rpc/types.h>
#include <netinet/ip.h>
#include <netinet/ether.h>
#include <ctype.h>

struct ethheader
{
    u_char ether_dhost[6];
    // 目的mac地址
    u_char ether_shost[6];
    // 源mac地址
    u_short ether_type;
};

/* IP Header */
struct ipheader
{
    unsigned char iph_ihl : 4,       // IP header length
        iph_ver : 4;                 // IP version
    unsigned char iph_tos;           // Type of service
    unsigned short int iph_len;      // IP Packet length (data   header)
    unsigned short int iph_ident;    // Identification
    unsigned short int iph_flag : 3, // Fragmentation flags
        iph_offset : 13;             // Flags offset
    unsigned char iph_ttl;           // Time to Live
    unsigned char iph_protocol;      // Protocol type
    unsigned short int iph_chksum;   // IP datagram checksum
    struct in_addr iph_sourceip;     // Source IP address
    struct in_addr iph_destip;       // Destination IP address
};

struct tcphdr
{
    unsigned short sport;    // 源端口
    unsigned short dport;    // 目标端口
    unsigned int seq;        // 序列号
    unsigned int ack_seq;    // 确认号
    unsigned char len;       // 首部长度
    unsigned char flag;      // 标志位
    unsigned short win;      // 窗口大小
    unsigned short checksum; // 校验和
    unsigned short urg;      // 紧急指针
};

/* UDP Header */
struct udphdr
{
    u_int16_t sport; /* source port */
    u_int16_t dport; /* destination port */
    u_int16_t ulen;  /* udp length */
    u_int16_t sum;   /* udp checksum */
};

数据包处理函数声明

void handler(u_char *, const struct pcap_pkthdr *, const u_char *);是一个回调函数,用于处理数据包。它接受三个参数:

  • u_char *: 这是一个无类型指针,可以用于传递一些附加的数据给回调函数。通常情况下,它会被用来传递一些上下文信息或者回调函数所需的其他数据。
  • const struct pcap_pkthdr *: 这是一个指向 pcap_pkthdr 结构体的指针,其中包含了数据包的元数据信息,比如时间戳、数据包长度等。
  • const u_char *: 这是指向数据包的原始字节流的指针,可以通过这个指针来访问数据包的内容。

void print_data(unsigned char *, int);用于输出数据包的内容。第一个参数为指向数据payload的指针,第二个参数为数据包的字节数。

代码语言:javascript复制
// 处理数据包的回调函数
void handler(u_char *, const struct pcap_pkthdr *, const u_char *);
// 输出数据包payload
void print_data(unsigned char *, int);

BPF捕获数据包

下面的代码都在主函数中

变量释义:

  • handle 是一个指向 pcap_t 结构体的指针,用于表示一个网络数据包捕获的会话。
  • errbuf 是一个字符数组,用于存储错误信息的缓冲区。在发生错误时,会将错误信息存储在这个缓冲区中,以便进行错误处理和调试。
  • fp 是一个用于存储编译后的过滤程序的结构体。用于编译和设置数据包过滤规则。
  • filter_exp 是一个字符数组,用于指定数据包过滤表达式。在这个例子中,过滤表达式是 "ip",表示只捕获 IP 数据包。可以自己根据BPF语法设定规则。
  • net 是一个无符号 32 位整数,用于存储网络的 IP 地址。在后续的代码中,它将被用于编译过滤表达式时传递给 pcap_compile 函数。
  • devs 是一个指向网络设备列表的指针。
  • dev 是一个指向网络设备的指针。
代码语言:javascript复制
    pcap_t *handle;
    char errbuf[PCAP_ERRBUF_SIZE];
    struct bpf_program fp;
    char filter_exp[] = "ip";
    bpf_u_int32 net;
    // 统计数据包个数
    int packet_count = 0;
    pcap_if_t *devs;
    pcap_if_t *dev;

net 变量在某些情况下可以不设置。具体是否需要设置 net 取决于过滤表达式中是否涉及网络地址相关的条件。如果过滤表达式中不包含网络地址相关的条件,例如只捕获所有数据包或仅捕获特定端口的数据包,那么可以不设置 net 变量。在这种情况下,pcap_compile 函数不会使用 net 变量,而是仅根据过滤表达式编译过滤程序。 然而,如果过滤表达式中涉及网络地址相关的条件,例如指定源地址或目的地址,那么 net 变量就需要设置为相应的网络地址。这是因为编译过滤表达式时,pcap_compile 函数需要确定网络地址的位掩码,以便进行地址匹配。在这种情况下,可以使用其他方法获取网络地址,或者通过调用 pcap_lookupnet 函数来获取网络地址并将其存储在 net 变量中。

下面这段代码使用pcap_findalldevs(&devs, errbuf)寻找所有可用的网络接口,并将它们的信息存储在 pcap_if_t 类型的链表中,通过 devs 指针参数返回。然后遍历设备链表,输出网络接口的信息。

代码语言:javascript复制
    // 可用的的设备
    int ret = pcap_findalldevs(&devs, errbuf);
    if (ret == -1)
    {
        printf("no dev up err %sn", errbuf);
        return 0;
    }
    // 输出设备信息
    for (dev = devs; dev != NULL; dev = dev->next)
    {
        printf("Device: %sn", dev->name);
        printf("Description: %sn", dev->description);
        printf("--------------------------------------n");
    }

pcap_if_tpcap_if的别名,pcap_if_t的结构如下,next指向下一个网络接口的地址,name为接口的名称,descriptionaddresses指向网络接口的地址结构。

代码语言:javascript复制
/*
 * Item in a list of interfaces.
 */
struct pcap_if {
    struct pcap_if *next;
    char *name;     /* name to hand to "pcap_open_live()" */
    char *description;  /* textual description of interface, or NULL */
    struct pcap_addr *addresses;
    bpf_u_int32 flags;  /* PCAP_IF_ interface flags */
};

选择默认设备,也就是链表的第一个网络接口进行数据包捕获。 dev->name 表示要打开的网络设备的名称。BUFSIZ 表示数据包捕获时使用的缓冲区大小。参数1 表示启用混杂模式,0 表示禁用混杂模式。1000 表示超时时间,以毫秒为单位,在此时间内等待数据包到达。errbuf 用于存储错误信息。

函数将返回一个指向 pcap_t 结构体的指针,表示打开的网络设备会话。

代码语言:javascript复制
    // 选择默认设备
    dev = devs;
    // 打开设备
    handle = pcap_open_live(dev->name, BUFSIZ, 1, 1000, errbuf);
    printf("listening on network card, ret: %p...n", handle);

然后编译过滤规则filter_exp,将编译后过滤程序的结构体存储在fp中。再用pcap_setfilter 函数用于将编译后的过滤程序应用到捕获会话上。

代码语言:javascript复制
    // 编译规则
    printf("try to compile filter...n");
    pcap_compile(handle, &fp, filter_exp, 0, net);
    printf("try to set filter...n");
    pcap_setfilter(handle, &fp);

pcap_loop 函数用于循环捕获数据包并将其传递给指定的处理函数进行处理。选择回调函数为handler-1:表示捕获的数据包数量,设置为 -1 表示无限循环捕获,直到遇到错误或显式停止。然后将用于统计数据包的packet_count变量的地址经过强制类型转化为unsigned char *作为参数传递给回调函数。

当捕获过程完成后,需要使用 pcap_close 函数关闭数据包捕获会话, pcap_freealldevs 函数释放设备列表资源。

代码语言:javascript复制
    // 开始捕获
    printf("start to sniff...n");
    // 传递packet_count统计包个数
    pcap_loop(handle, -1, handler, (unsigned char *)&packet_count);
    // 关闭设备
    pcap_close(handle);
    pcap_freealldevs(devs);

回调函数的实现

回调函数接受的参数如下,第一个参数为主函数传递的(unsigned char *)&packet_count,用于统计数据包。第二个参数为一个指向 pcap_pkthdr 结构体的指针,其中包含了数据包的元数据信息,比如时间戳、数据包长度等。第三个参数为指向数据包的原始字节流的指针,可以通过这个指针来访问数据包的内容。

代码语言:javascript复制
void handler(u_char *args, const struct pcap_pkthdr *hdr, const u_char *packet);

/*
 * Generic per-packet information, as supplied by libpcap.
 *
 * The time stamp can and should be a "struct timeval", regardless of
 * whether your system supports 32-bit tv_sec in "struct timeval",
 * 64-bit tv_sec in "struct timeval", or both if it supports both 32-bit
 * and 64-bit applications.  The on-disk format of savefiles uses 32-bit
 * tv_sec (and tv_usec); this structure is irrelevant to that.  32-bit
 * and 64-bit versions of libpcap, even if they're on the same platform,
 * should supply the appropriate version of "struct timeval", even if
 * that's not what the underlying packet capture mechanism supplies.
 */
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) */
};

获取packet_count的地址并转化为int类型的指针,然后对指针进行解引用并加1表示又收到了一个数据包。

代码语言:javascript复制
    // 统计数据包
    int *packet_count = (int *)args;
    (*packet_count)  ;

获取数据包原始字节流中的以太网帧头部。然后输出源和目的MAC地址以及以太网类型(IPV4、IPV6、ARP等)。

代码语言:javascript复制
    printf("***********************receive a packet***********************n");
    printf("receive a packet, packet count: %dn", *packet_count);
    // 获取以太网帧
    struct ethheader *eth = (struct ethheader *)packet;
    // 输出源和目的mac地址
    printf("Source MAC: ");
    for (int i = 0; i < 6; i  )
    {
        printf("x ", eth->ether_shost[i]);
    }
    printf("t");
    printf("Destination MAC: ");
    for (int i = 0; i < 6; i  )
    {
        printf("x ", eth->ether_dhost[i]);
    }
    printf("n");
    // 输出以太网类型
    printf("Ethernet Type: xn", ntohs(eth->ether_type));

如果是IPV4,则获取数据包原始字节流地址偏移一个以太网帧头部长度地址的内容并转化为IP头部类型。然后解析IP头部,输出源和目的IP地址。

最后对IPV4上层协议进行处理,输出TCP和UDP的源和目的端口号以及承载的数据内容。payload_length=hdr->lenpayload_length即数据包载荷的字节数,存储在pcap_pkthdr 结构中。

代码语言:javascript复制
    // 判断以太网类型
    if (ntohs(eth->ether_type) == 0x0800)
    {
        // 如果是ipv4
        printf("Protocol: IPV4n");
        // 获取ip报头
        struct ipheader *ip = (struct ipheader *)(packet   sizeof(struct ethheader));
        printf("From: %st", inet_ntoa(ip->iph_sourceip));
        printf("To: %sn", inet_ntoa(ip->iph_destip));
        int payload_length;
        // 对上层协议处理
        switch (ip->iph_protocol)
        {
        case IPPROTO_TCP:
            printf("Protocol: TCPn");
            // 获取tcp报头
            struct tcphdr *tcp = (struct tcphdr *)(packet   sizeof(struct ethheader)   sizeof(struct iphdr));
            // 输出源和目的端口号
            printf("From: %dt", ntohs(tcp->sport));
            printf("To: %dn", ntohs(tcp->dport));
            // 输出协议的payload
            // 获取 TCP 数据的指针
            unsigned char *tcp_data = (unsigned char *)(tcp)   (tcp->len * 4);
            payload_length = hdr->len;
            // 输出 TCP 协议数据的十六进制和 ASCII 形式
            print_data(tcp_data, payload_length);
            return;
        case IPPROTO_UDP:
            printf("Protocol: UDPn");
            // 获取udp报头
            struct udphdr *udp = (struct udphdr *)(packet   sizeof(struct ethheader)   sizeof(struct iphdr));
            // 输出源和目的端口号
            printf("From: %dt", ntohs(udp->sport));
            printf("To: %dn", ntohs(udp->dport));
            // 输出协议的payload
            // 获取 UDP 数据的指针
            unsigned char *udp_data = (unsigned char *)(udp)   sizeof(struct udphdr);
            // 计算 UDP payload 的长度
            payload_length = hdr->len;
            // 输出 UDP 协议数据的十六进制和 ASCII 形式
            print_data(udp_data, payload_length);
            return;
        case IPPROTO_ICMP:
            printf("Protocol: ICMPn");
            return;
        default:
            printf("Protocol: othersn");
            return;
        }
    }
    else if (ntohs(eth->ether_type) == 0x0806)
    {
        printf("Protocol: ARPn");
    }
    else if (ntohs(eth->ether_type) == 0x86DD)
    {
        printf("Protocol: IPV6n");
    }

print_data(unsigned char *data, unsigned int len)函数如下,用于输出HEX和ASCII格式的数据载荷。

代码语言:javascript复制
void print_data(unsigned char *data, unsigned int len)
{
    int i;
    printf("receve %d bytesn***************************payload****************************n", len);
    for (i = 0; i < len; i  )
    {
        printf("X", data[i]); // 输出十六进制值,并在值之间添加空格
        if ((i   1) % 16 == 0)
        {
            printf("t");
            for (int j = i - 15; j <= i; j  )
            {
                if (data[j] >= 32 && data[j] <= 126)
                {
                    printf("%c", data[j]); // 输出 ASCII 字符(如果可打印)
                }
                else
                {
                    printf(".");
                }
                if ((j   1) % 16 == 0)
                {
                    printf("n"); // 在每行的末尾打印换行符
                }
            }
        }
    }
    printf("n");
}

嗅探效果展示

完整代码如下

代码语言:javascript复制
/**
 * @file sniffer.c
 * @author cSuk1 (652240843@qq.com)
 * @brief 嗅探程序示例
 * @version 0.1
 * @date 2023-11-22
 *
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <pcap.h>
#include <arpa/inet.h>
#include <rpc/types.h>
#include <netinet/ip.h>
#include <netinet/ether.h>
#include <ctype.h>

struct ethheader
{
    u_char ether_dhost[6];
    // 目的mac地址
    u_char ether_shost[6];
    // 源mac地址
    u_short ether_type;
};

/* IP Header */
struct ipheader
{
    unsigned char iph_ihl : 4,       // IP header length
        iph_ver : 4;                 // IP version
    unsigned char iph_tos;           // Type of service
    unsigned short int iph_len;      // IP Packet length (data   header)
    unsigned short int iph_ident;    // Identification
    unsigned short int iph_flag : 3, // Fragmentation flags
        iph_offset : 13;             // Flags offset
    unsigned char iph_ttl;           // Time to Live
    unsigned char iph_protocol;      // Protocol type
    unsigned short int iph_chksum;   // IP datagram checksum
    struct in_addr iph_sourceip;     // Source IP address
    struct in_addr iph_destip;       // Destination IP address
};

struct tcphdr
{
    unsigned short sport;    // 源端口
    unsigned short dport;    // 目标端口
    unsigned int seq;        // 序列号
    unsigned int ack_seq;    // 确认号
    unsigned char len;       // 首部长度
    unsigned char flag;      // 标志位
    unsigned short win;      // 窗口大小
    unsigned short checksum; // 校验和
    unsigned short urg;      // 紧急指针
};

/* UDP Header */
struct udphdr
{
    u_int16_t sport; /* source port */
    u_int16_t dport; /* destination port */
    u_int16_t ulen;  /* udp length */
    u_int16_t sum;   /* udp checksum */
};

// 处理数据包的回调函数
void handler(u_char *, const struct pcap_pkthdr *, const u_char *);
// 输出数据包payload
void print_data(unsigned char *, unsigned int);

int main(int argc, char *argv[])
{
    pcap_t *handle;
    char errbuf[PCAP_ERRBUF_SIZE];
    struct bpf_program fp;
    char filter_exp[] = "ip";
    bpf_u_int32 net;
    // 统计数据包个数
    int packet_count = 0;
    pcap_if_t *devs;
    pcap_if_t *dev;
    // 可用的的设备
    int ret = pcap_findalldevs(&devs, errbuf);
    if (ret == -1)
    {
        printf("no dev up err %sn", errbuf);
        return 0;
    }
    // 输出设备信息
    for (dev = devs; dev != NULL; dev = dev->next)
    {
        printf("Device: %sn", dev->name);
        printf("Description: %sn", dev->description);
        printf("--------------------------------------n");
    }
    // 选择默认设备
    dev = devs;
    // 打开设备
    handle = pcap_open_live(dev->name, BUFSIZ, 1, 1000, errbuf);
    printf("listening on network card, ret: %p...n", handle);
    // 编译规则
    printf("try to compile filter...n");
    pcap_compile(handle, &fp, filter_exp, 0, net);
    printf("try to set filter...n");
    pcap_setfilter(handle, &fp);

    // 开始捕获
    printf("start to sniff...n");
    // 传递packet_count统计包个数
    pcap_loop(handle, -1, handler, (unsigned char *)&packet_count);
    // 关闭设备
    pcap_close(handle);
    pcap_freealldevs(devs);
    return 0;
}

/// @brief
/// @param args
/// @param hdr
/// @param packet
void handler(u_char *args, const struct pcap_pkthdr *hdr, const u_char *packet)
{
    // 统计数据包
    int *packet_count = (int *)args;
    (*packet_count)  ;
    printf("***********************receive a packet***********************n");
    printf("receive a packet, packet count: %dn", *packet_count);
    // 获取以太网帧
    struct ethheader *eth = (struct ethheader *)packet;
    // 输出源和目的mac地址
    printf("Source MAC: ");
    for (int i = 0; i < 6; i  )
    {
        printf("x ", eth->ether_shost[i]);
    }
    printf("t");
    printf("Destination MAC: ");
    for (int i = 0; i < 6; i  )
    {
        printf("x ", eth->ether_dhost[i]);
    }
    printf("n");
    // 输出以太网类型
    printf("Ethernet Type: xn", ntohs(eth->ether_type));
    // 判断以太网类型
    if (ntohs(eth->ether_type) == 0x0800)
    {
        // 如果是ipv4
        printf("Protocol: IPV4n");
        // 获取ip报头
        struct ipheader *ip = (struct ipheader *)(packet   sizeof(struct ethheader));
        printf("From: %st", inet_ntoa(ip->iph_sourceip));
        printf("To: %sn", inet_ntoa(ip->iph_destip));
        int payload_length;
        // 对上层协议处理
        switch (ip->iph_protocol)
        {
        case IPPROTO_TCP:
            printf("Protocol: TCPn");
            // 获取tcp报头
            struct tcphdr *tcp = (struct tcphdr *)(packet   sizeof(struct ethheader)   sizeof(struct iphdr));
            // 输出源和目的端口号
            printf("From: %dt", ntohs(tcp->sport));
            printf("To: %dn", ntohs(tcp->dport));
            // 输出协议的payload
            // 获取 TCP 数据的指针
            unsigned char *tcp_data = (unsigned char *)(tcp)   (tcp->len * 4);
            payload_length = hdr->len;
            // 输出 TCP 协议数据的十六进制和 ASCII 形式
            print_data(tcp_data, payload_length);
            return;
        case IPPROTO_UDP:
            printf("Protocol: UDPn");
            // 获取udp报头
            struct udphdr *udp = (struct udphdr *)(packet   sizeof(struct ethheader)   sizeof(struct iphdr));
            // 输出源和目的端口号
            printf("From: %dt", ntohs(udp->sport));
            printf("To: %dn", ntohs(udp->dport));
            // 输出协议的payload
            // 获取 UDP 数据的指针
            unsigned char *udp_data = (unsigned char *)(udp)   sizeof(struct udphdr);
            // 计算 UDP payload 的长度
            payload_length = hdr->len;
            // 输出 UDP 协议数据的十六进制和 ASCII 形式
            print_data(udp_data, payload_length);
            return;
        case IPPROTO_ICMP:
            printf("Protocol: ICMPn");
            return;
        default:
            printf("Protocol: othersn");
            return;
        }
    }
    else if (ntohs(eth->ether_type) == 0x0806)
    {
        printf("Protocol: ARPn");
    }
    else if (ntohs(eth->ether_type) == 0x86DD)
    {
        printf("Protocol: IPV6n");
    }
}

void print_data(unsigned char *data, unsigned int len)
{
    int i;
    printf("receve %d bytesn***************************payload****************************n", len);
    for (i = 0; i < len; i  )
    {
        printf("X", data[i]); // 输出十六进制值,并在值之间添加空格
        if ((i   1) % 16 == 0)
        {
            printf("t");
            for (int j = i - 15; j <= i; j  )
            {
                if (data[j] >= 32 && data[j] <= 126)
                {
                    printf("%c", data[j]); // 输出 ASCII 字符(如果可打印)
                }
                else
                {
                    printf(".");
                }
                if ((j   1) % 16 == 0)
                {
                    printf("n"); // 在每行的末尾打印换行符
                }
            }
        }
    }
    printf("n");
}

编译运行

代码语言:javascript复制
gcc -o sniffer sniffer.c -lpcap
sudo ./sniffer

输出网络接口结果如下

代码语言:javascript复制
Device: ens33
Description: (null)
--------------------------------------
Device: lo
Description: (null)
--------------------------------------
Device: any
Description: Pseudo-device that captures on all interfaces
--------------------------------------
Device: bluetooth-monitor
Description: Bluetooth Linux Monitor
--------------------------------------
Device: nflog
Description: Linux netfilter log (NFLOG) interface
--------------------------------------
Device: nfqueue
Description: Linux netfilter queue (NFQUEUE) interface
--------------------------------------
Device: bluetooth0
Description: Bluetooth adapter number 0
--------------------------------------

数据包捕获部分输出结果如下

代码语言:javascript复制
***********************receive a packet***********************
receive a packet, packet count: 20
Source MAC: 00 0c 29 b4 92 c5   Destination MAC: 00 50 56 c0 00 08 
Ethernet Type: 0800
Protocol: IPV4
From: 192.168.219.128   To: 192.168.219.1
Protocol: TCP
From: 22        To: 13133
receve 138 bytes
***************************payload****************************
8CDC5D652D5C501A3600000036000000        ..]e-P.6...6...
090000005A0068000000000000000000        ....Z.h.........
00000000000000000000000011000800        ................
0200000001000406000C29B492C50000        ..........).....
00000000000000000000000000000000        ................
000000000000005056C00008000C29B4        .......PV.....).
92C5080045080028313E40004006D1B6        ....E..(1>@.@...
C0A8DB80C0A8DB010016334DD0F2D46E        ..........3M...n
D353326250101D2D37EE
***********************receive a packet***********************
receive a packet, packet count: 21
Source MAC: 00 50 56 c0 00 08   Destination MAC: 00 0c 29 b4 92 c5 
Ethernet Type: 0800
Protocol: IPV4
From: 192.168.219.1     To: 192.168.219.128
Protocol: TCP
From: 13133     To: 22
receve 114 bytes
***************************payload****************************
56C00008000C29B492C5080045080054        V.....).....E..T
313F40004006D189C0A8DB80C0A8DB01        1?@.@...........
0016334DD0F2D46ED353326250181D2D        ..3M...n.S2bP..-
381A0000AFCBB72069F810AFC4F06C08        8...... i.....l.
57F7DCDA1F917FC550511553028D7D18        W.......PQ.S..}.
B52529DE4CD1DFC9F0C4B1B3A8887100        .%).L.........q.
00000000980000008DDC5D659C76600E        ..........]e.v`.
3C00
***********************receive a packet***********************
receive a packet, packet count: 22
Source MAC: 00 0c 29 b4 92 c5   Destination MAC: 00 50 56 c0 00 08 
Ethernet Type: 0800
Protocol: IPV4
From: 192.168.219.128   To: 192.168.219.1
Protocol: TCP
From: 22        To: 13133
receve 54 bytes
***************************payload****************************
4FE140008006731BC0A8DB01C0A8DB80        O.@...s.........
334D0016D3533262D0F2D49A50101003        3M...S2b....P...
895700000000000000000000D0000000        .W..............
8DDC5D654D41

-----本页内容已结束,喜欢请分享------


0 人点赞