• 自己动手写网络抓包工具


    看了太多的“自己动手”,这次咱也“自己动手”一下,写个简单的网络抓包工具吧。要写出像tcpdump和wireshark(ethereal)这样的大牛程序来,咱也没那能耐,呵呵。所以这个工具只能抓取本地IP数据报,同时它还使用了BPF,目的是了解如何进行简单有效的网络抓包。

    当打开一个标准SOCKET套接口时,我们比较熟悉的协议往往是用AF_INET来建立基于TCP(SOCK_STREAM)或UDP(SOCK_DGRAM)的链接。但是这些只用于IP层以上,要想从更底层抓包,我们需要使用AF_PACKET来建立套接字,它支持SOCK_RAW和SOCK_DGRAM,它们都能从底层抓包,不同的是后者得到的数据不包括以太网帧头(最开始的14个字节)。好了,现在我们就知道该怎样建立SOCKET套接口了:
    1. sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));
    最后一个参数 ETH_P_IP 指出,我们只对IP包感兴趣,而不是ARP,RARP等。之后就可以用recvfrom从套接口读取数据了。

    现在我们可以抓到发往本地的所有IP数据报了,那么有没有办法抓到那些“流经”本地的数据呢?呵呵,当然可以了,这种技术叫网络嗅探(sniff),它很能威胁网络安全,也非常有用,尤其是当你对网内其他用户的隐私感兴趣时:(  由于以太网数据包是对整个网段广播的,所以网内所有用户都能收到其他用户发出的数据,只是默认的,网卡只接收目的地址是自己或广播地址的数据,而把不是发往自己的数据包丢弃。但是多数网卡驱动会提供一种混杂模式(promiscous mode),工作在这种模式下的网卡会接收网络内的所有数据,不管它是发给谁的。下面的方法可以把网卡设成混杂模式:
    1.       // set NIC to promiscous mode, so we can recieve all packets of the network
    2.       strncpy(ethreq.ifr_name, "eth0", IFNAMSIZ);
    3.       ioctl(sock, SIOCGIFFLAGS, &ethreq);
    4.       ethreq.ifr_flags |= IFF_PROMISC;
    5.       ioctl(sock, SIOCSIFFLAGS, &ethreq);
    通过ifconfig可以很容易的查看当前网卡是否工作在混杂模式(PROMISC)。但是请注意,程序退出后,网卡的工作模式不会改变,所以别忘了关闭网卡的混杂模式:
    1.       // turn off promiscous mode
    2.       ethreq.ifr_flags &= ~IFF_PROMISC;
    3.       ioctl(sock, SIOCSIFFLAGS, &ethreq);
    现在我们可以抓到本网段的所有IP数据包了,但是问题也来了:那么多的数据,怎么处理?CPU可能会被严重占用,而且绝大多数的数据我们可能根本就不敢兴趣!那怎么办呢?用if语句?可能要n多个,而且丝毫不会降低内核的繁忙程度。最好的办法就是告诉内核,把不感兴趣的数据过滤掉,不要往应用层送。BPF就为此而生。

    BPF(Berkeley Packet Filter)是一种类是汇编的伪代码语言,它也有命令代码和操作数。例如,如果我们只对用户192.168.1.4的数据感兴趣,可以用tcpdump的-d选项生成BPF代码如下:
    1.       $tcpdump -d host 192.168.1.4
    2.       (000) ldh      [12]
    3.       (001) jeq      #0x800           jt 2 jf 6
    4.       (002) ld       [26]
    5.       (003) jeq      #0xc0a80104      jt 12 jf 4
    6.       (004) ld       [30]
    7.       (005) jeq      #0xc0a80104      jt 12 jf 13
    8.       (006) jeq      #0x806           jt 8 jf 7
    9.       (007) jeq      #0x8035          jt 8 jf 13
    10.       (008) ld       [28]
    11.       (009) jeq      #0xc0a80104      jt 12 jf 10
    12.       (010) ld       [38]
    13.       (011) jeq      #0xc0a80104      jt 12 jf 13
    14.       (012) ret      #96
    15.       (013) ret      #0
    其中第一列代表行号,第二列是命令代码,后面是操作数。下面我们采用汇编注释的方式简单的解释一下:

          (000) ldh      [12] ;load h?? (2 bytes) from ABS offset 12 (the TYPE of ethernet header)
          (001) jeq      #0x800           jt 2 jf 6 ;compare and jump, jump to line 2 if true; else jump to line 6
          (002) ld       [26] ;load word (4 bytes) from ABS offset 26 (src IP address of IP header)
          (003) jeq      #0xc0a80104      jt 12 jf 4 ;compare and jump, jump to line 12 if true, else jump to line 4
          (004) ld       [30] ; load word (4 bytes) from ABS offset 30 (dst IP address of IP header)
          (005) jeq      #0xc0a80104      jt 12 jf 13 ;see line 3
          (006) jeq      #0x806           jt 8 jf 7 ;compare with ARP, see line 1
          (007) jeq      #0x8035          jt 8 jf 13 ;compare with RARP, see line 1
          (008) ld       [28] ;src IP address for other protocols
          (009) jeq      #0xc0a80104      jt 12 jf 10 
          (010) ld       [38] ;dst IP address for other protocols
          (011) jeq      #0xc0a80104      jt 12 jf 13 
          (012) ret      #96 ;return 96 bytes to user application
          (013) ret      #0 ;drop the packet

    但是这样的伪代码我们是无法在应用程序里使用的,所以tcpdum提供了一个-dd选项来输出一段等效的C代码:
    1.       $tcpdump -dd host 192.168.1.4
    2.       { 0x28, 0, 0, 0x0000000c },
    3.       { 0x15, 0, 4, 0x00000800 },
    4.       { 0x20, 0, 0, 0x0000001a },
    5.       { 0x15, 8, 0, 0xc0a80104 },
    6.       { 0x20, 0, 0, 0x0000001e },
    7.       { 0x15, 6, 7, 0xc0a80104 },
    8.       { 0x15, 1, 0, 0x00000806 },
    9.       { 0x15, 0, 5, 0x00008035 },
    10.       { 0x20, 0, 0, 0x0000001c },
    11.       { 0x15, 2, 0, 0xc0a80104 },
    12.       { 0x20, 0, 0, 0x00000026 },
    13.       { 0x15, 0, 1, 0xc0a80104 },
    14.       { 0x6, 0, 0, 0x00000060 },
    15.       { 0x6, 0, 0, 0x00000000 },
    该代码对应的数据结构是struct sock_filter,该结构在linux/filter.h中定义如下:
    1.       struct sock_filter  // Filter block
    2.       {
    3.              __u16 code; // Actual filter code
    4.              __u8 jt;    // Jump true
    5.              __u8 jf;    // Jump false
    6.              __u32 k;   // Generic multiuse field
    7.       };
    code对应命令代码;jt是jump if true后面的操作数,注意这里用的是相对行偏移,如2就表示向前跳转2行,而不像伪代码中使用绝对行号;jf为jump if false后面的操作数;k对应伪代码中第3列的操作数。

    了解了BPF伪代码和结构,我们就可以自己定制更加简单有效的BPF filter了,如上例中的6-11行不是针对IP协议的,而我们的套接字已经指定只读取IP数据了,所以就可以把他们删除,不过要注意,行偏移也要做相应的修改。

    另外,tcpdump默认只返回96字节的数据,但对大部分应用来说,96字节是远远不够的,所以tcpdump提供了-s选项用于指定返回的数据长度。

    OK,下面我们就来看看怎样把过滤器安装到套接口上吧:
    1.       $tcpdump ip -d -s 2048 host 192.168.1.2
    2.       (000) ldh      [12] 
    3.       (001) jeq      #0x800           jt 2 jf 7 
    4.       (002) ld       [26] 
    5.       (003) jeq      #0xc0a80102      jt 6 jf 4 
    6.       (004) ld       [30] 
    7.       (005) jeq      #0xc0a80102      jt 6 jf 7 
    8.       (006) ret      #2048
    9.       (007) ret      #0 

    10.       struct sock_filter bpf_code[] = {
    11.             { 0x28, 0, 0, 0x0000000c }, 
    12.             { 0x15, 0, 5, 0x00000800 }, 
    13.             { 0x20, 0, 0, 0x0000001a }, 
    14.             { 0x15, 2, 0, 0xc0a80102 }, 
    15.             { 0x20, 0, 0, 0x0000001e }, 
    16.             { 0x15, 0, 1, 0xc0a80102 }, 
    17.             { 0x6, 0, 0, 0x00000800 }, 
    18.             { 0x6, 0, 0, 0x00000000 }
    19.       };

    20.       struct sock_fprog filter;
    21.       filter.len = sizeof(bpf_code)/sizeof(bpf_code[0]);
    22.       filter.filter = bpf_code;
    23.       setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter));
    最后加上信号处理器,以便能在程序退出前恢复网卡的工作模式。到现在我们已经可以看到一个小聚规模抓包小工具了,呵呵,麻雀虽小,但也五脏俱全啊!下面给出完整的代码。
    1.       #include <sys/types.h>
    2.       #include <sys/time.h>
    3.       #include <sys/ioctl.h>
    4.       #include <sys/socket.h>
    5.       #include <linux/types.h>
    6.       #include <netinet/in.h>
    7.       #include <netinet/udp.h>
    8.       #include <netinet/ip.h>
    9.       #include <netpacket/packet.h>
    10.       #include <net/ethernet.h>
    11.       #include <arpa/inet.h>
    12.       #include <string.h>
    13.       #include <signal.h>
    14.       #include <net/if.h>
    15.       #include <stdio.h>
    16.       #include <sys/uio.h>
    17.       #include <fcntl.h>
    18.       #include <unistd.h>
    19.       #include <linux/filter.h>
    20.       #include <stdlib.h>

    21.       #define ETH_HDR_LEN 14
    22.       #define IP_HDR_LEN 20
    23.       #define UDP_HDR_LEN 8
    24.       #define TCP_HDR_LEN 20

    25.       static int sock;

    26.       void sig_handler(int sig) 
    27.       {
    28.             struct ifreq ethreq;
    29.             if(sig == SIGTERM)
    30.                   printf("SIGTERM recieved, exiting.../n");
    31.             else if(sig == SIGINT)
    32.                   printf("SIGINT recieved, exiting.../n");
    33.             else if(sig == SIGQUIT)
    34.                   printf("SIGQUIT recieved, exiting.../n");
    35.             // turn off the PROMISCOUS mode 
    36.             strncpy(ethreq.ifr_name, "eth0", IFNAMSIZ);
    37.             if(ioctl(sock, SIOCGIFFLAGS, &ethreq) != -1) {
    38.                   ethreq.ifr_flags &= ~IFF_PROMISC;
    39.                   ioctl(sock, SIOCSIFFLAGS, &ethreq);
    40.             }
    41.             close(sock);
    42.             exit(0);
    43.       }

    44.       int main(int argc, char ** argv) {
    45.             int n;
    46.             char buf[2048];
    47.             unsigned char *ethhead;
    48.             unsigned char *iphead;
    49.             struct ifreq ethreq;
    50.             struct sigaction sighandle;

    51.       #if 0
    52.             $tcpdump ip -s 2048 -d host 192.168.1.2
    53.             (000) ldh      [12]
    54.             (001) jeq      #0x800           jt 2 jf 7
    55.             (002) ld       [26]
    56.             (003) jeq      #0xc0a80102      jt 6 jf 4
    57.             (004) ld       [30]
    58.             (005) jeq      #0xc0a80102      jt 6 jf 7
    59.             (006) ret      #2048
    60.             (007) ret      #0
    61.       #endif

    62.             struct sock_filter bpf_code[] = {
    63.                   { 0x28, 0, 0, 0x0000000c },
    64.                   { 0x15, 0, 5, 0x00000800 },
    65.                   { 0x20, 0, 0, 0x0000001a },
    66.                   { 0x15, 2, 0, 0xc0a80102 },
    67.                   { 0x20, 0, 0, 0x0000001e },
    68.                   { 0x15, 0, 1, 0xc0a80102 },
    69.                   { 0x6, 0, 0, 0x00000800 },
    70.                   { 0x6, 0, 0, 0x00000000 }
    71.             };

    72.             struct sock_fprog filter;
    73.             filter.len = sizeof(bpf_code)/sizeof(bpf_code[0]);
    74.             filter.filter = bpf_code;

    75.             sighandle.sa_flags = 0;
    76.             sighandle.sa_handler = sig_handler;
    77.             sigemptyset(&sighandle.sa_mask);
    78.             //sigaddset(&sighandle.sa_mask, SIGTERM);
    79.             //sigaddset(&sighandle.sa_mask, SIGINT);
    80.             //sigaddset(&sighandle.sa_mask, SIGQUIT);
    81.             sigaction(SIGTERM, &sighandle, NULL);
    82.             sigaction(SIGINT, &sighandle, NULL);
    83.             sigaction(SIGQUIT, &sighandle, NULL);

    84.             // AF_PACKET allows application to read pecket from and write packet to network device
    85.             // SOCK_DGRAM the packet exclude ethernet header
    86.             // SOCK_RAW raw data from the device including ethernet header
    87.             // ETH_P_IP all IP packets 
    88.             if((sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP))) == -1) {
    89.                   perror("socket");
    90.                   exit(1);
    91.             }

    92.             // set NIC to promiscous mode, so we can recieve all packets of the network
    93.             strncpy(ethreq.ifr_name, "eth0", IFNAMSIZ);
    94.             if(ioctl(sock, SIOCGIFFLAGS, &ethreq) == -1) {
    95.                   perror("ioctl");
    96.                   close(sock);
    97.                   exit(1);
    98.             }

    99.             ethreq.ifr_flags |= IFF_PROMISC;
    100.             if(ioctl(sock, SIOCSIFFLAGS, &ethreq) == -1) {
    101.                   perror("ioctl");
    102.                   close(sock);
    103.                   exit(1);
    104.             }

    105.             // attach the bpf filter
    106.             if(setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) == -1) {
    107.                   perror("setsockopt");
    108.                   close(sock);
    109.                   exit(1);
    110.             }

    111.             while(1) {
    112.                   n = recvfrom(sock, buf, sizeof(buf), 0, NULL, NULL);
    113.                   if(n < (ETH_HDR_LEN+IP_HDR_LEN+UDP_HDR_LEN)) {
    114.                         printf("invalid packet/n");
    115.                         continue;
    116.                   }

    117.                   printf("%d bytes recieved/n", n);

    118.                   ethhead = buf;
    119.                   printf("Ethernet: MAC[%02X:%02X:%02X:%02X:%02X:%02X]", ethhead[0], ethhead[1], ethhead[2],
    120.                         ethhead[3], ethhead[4], ethhead[5]);
    121.                   printf("->[%02X:%02X:%02X:%02X:%02X:%02X]", ethhead[6], ethhead[7], ethhead[8],
    122.                         ethhead[9], ethhead[10], ethhead[11]);
    123.                   printf(" type[%04x]/n", (ntohs(ethhead[12]|ethhead[13]<<8)));

    124.                   iphead = ethhead + ETH_HDR_LEN;
    125.                   // header length as 32-bit
    126.                   printf("IP: Version: %d HeaderLen: %d[%d]", (*iphead>>4), (*iphead & 0x0f), (*iphead & 0x0f)*4);
    127.                   printf(" TotalLen %d", (iphead[2]<<8|iphead[3]));
    128.                   printf(" IP [%d.%d.%d.%d]", iphead[12], iphead[13], iphead[14], iphead[15]);
    129.                   printf("->[%d.%d.%d.%d]", iphead[16], iphead[17], iphead[18], iphead[19]);
    130.                   printf(" %d", iphead[9]);

    131.                   if(iphead[9] == IPPROTO_TCP)
    132.                         printf("[TCP]");
    133.                   else if(iphead[9] == IPPROTO_UDP)
    134.                         printf("[UDP]");
    135.                   else if(iphead[9] == IPPROTO_ICMP)
    136.                         printf("[ICMP]");
    137.                   else if(iphead[9] == IPPROTO_IGMP)
    138.                         printf("[IGMP]");
    139.                   else if(iphead[9] == IPPROTO_IGMP)
    140.                         printf("[IGMP]");
    141.                   else
    142.                         printf("[OTHERS]");

    143.                   printf(" PORT [%d]->[%d]/n", (iphead[20]<<8|iphead[21]), (iphead[22]<<8|iphead[23]));
    144.             }
    145.             close(sock);
    146.             exit(0);
    147.       }
    参考资料:
    [1] Linux下Sniffer程序的实现

    [2] 使用socket BPF

    转自:http://blog.csdn.net/wangxg_7520/article/details/2795229

  • 相关阅读:
    nohub
    swoole聊天室
    swoole httpserver学习
    swore tcp服务学习
    ps查看命令
    nginx和php-fpm调用方式
    Unix/Linux环境C编程入门教程(1) Solaris 11 64bit环境搭建
    Unix/Linux环境C编程入门教程(1) Solaris 11 64bit环境搭建
    C语言入门(4)——常量、变量与赋值
    C语言入门(3)——对Hello World程序的解释
  • 原文地址:https://www.cnblogs.com/alan666/p/8311902.html
Copyright © 2020-2023  润新知