嗅探器的主要功能就是解析报文,要解析那么就要了解报文的结构,以及数据如何封装

数据封装

当我们应用程序用TCP传输数据的时候,数据被送入协议栈中,然后逐个通过每一层,知道最后到物理层数据转换成比特流,送入网络。而再这个过程中,每一层都会对要发送的数据加一些首部信息。整个过程如下图。

数据封装

以太网帧格式

以太网常用帧格式有两种,一种是Ethernet II,另一种是IEEE 802.3 格式。这两种格式区别是:Ethernet II中包含一个Type字段。而IEEE 802.3格式中,此位置是长度字段。其中Type字段描述了,以太网首部后面所跟数据包的类型,例如Type为0x8000时为IP协议包,Type为8060时,后面为ARP协议包。以太网中多数数据帧使用的Ethernet II帧格式。

Ethernet II 帧格式

IEEE 802.3 帧格式

前导码:Ethernet II是由8个8'b10101010构成,IEEE802.3由7个8'b10101010+1个字节SFD

目的地址:目的设备的MAC物理地址

源地址:发送设备的MAC物理地址

类型(Ethernet II):以太网首部后面所跟数据包的类型,例如Type为0x8000时为IP协议包,Type为8060时,后面为ARP协议包

长度(IEEE802.3):当长度小于1500时,说明该帧为IEEE802.3帧格式,大于1500时,说明该帧为Ethernet II帧格式

数据:数据长度最小为46字节,不足46字节时,填充至46字节。因为最小帧长度是64字节,所以,46+6+6+2+4=64。(不算前导码)

FCS:就是CRC校验值

所以以太网帧的首部就是:

以太网帧格式首部

IP数据包格式

IP数据包格式

  1. 版本号 4位:表示IP协议的版本号,如目前广泛使用的是IPv4,还有其他版本的IP协议,如IPv6。通信两端协议必须一致。一般的值为0100(IPv4),0110(IPv6)。

  2. 首部长度:占 4 位,可表示的最大十进制数值是 15。请注意,这个字段所表示数的单位是32位字 ( 1 个32位字长是4 字节),因此,当 IP 的首部长度为 1111 时 (即十进制的 15),首部长度就达到 60字节。当 IP 分组的首部长度不是4字节的整数倍时,必须利用最后的填充字段加以填充。因此数据部分永远在 4字节的整数倍开始,这样在实现 IP协议时较为方便。首部长度限制为 60字节的缺点是有时可能不够用。这样做的目的是希望用户尽量减少开销。最常用的首部长度就是 20 字节 (即首部长度为 0101),这时不使用任何选项。

  3. 服务: 占 8 位,用来获得更好的服务。这个字段在旧标准中叫做服务类型,但实际上一直没有被使用过。1998年IETF把这个字段改名为区分服务 DS (DifferentiatedServices)。只有在使用区分服务时,这个字段才起作用。

  4. 总长度:总长度指首都及数据之和的长度,单位为字节。因为总长度字段为 16位,所以数据报的最大长度为 216-1=65 535字节。 在IP层下面的每一种数据链路层都有自己的帧格式,其中包括帧格式中的数据字段的最大长度,即最大传送单元 MTU (Maximum Transfer Unit)。当一个数据报封装成链路层的帧时,此数据报的总长度 (即首部加上数据部分)一定不能超过下面的数据链路层的MTU值。

  5. 标识符: 占 16位。IP软件在存储器中维持一个计数器,每产生一个数据报,计数器就加 1,并将此值赋给标识字段。但这个“标识”并不是序号,因为 IP是无连接的服务,数据报不存在按序接收的问题。当数据报由于长度超过网络的 MTU 而必须分片时,这个标识字段的值就被复制到所有的数据报的标识字段中。相同的标识字段的值使分片后的各数据报片最后能正确地重装成为原来的数据报。

  6. 标志:占 3 位,但目前只有2位有意义。标志字段中的最低位记为 MF (More Fragment)。MF=1即表示后面“还有分片”的数据报。MF=0表示这已是若干数据报片中的最后一个。标志字段中间的一位记为 DF(Don't Fragment),意思是“不能分片”。只有当 DF=0时才允许分片。

  7. 片偏移:占 13 位。较长的分组在分片后,某片在原分组中的相对位置。也就是说,相对用户数据字段的起点,该片从何处开始。片偏移以 8个字节为偏移单位。这就是说,每个分片的长度一定是 8字节 (64位)的整数倍。

  8. 生存时间:占 8 位,生存时间字段常用的英文缩写是TTL (Time To Live),其表明数据报在网络中的寿命。由发出数据报的源点设置这个字段。其目的是防止无法交付的数据报无限制地在因特网中兜围子,因而白白消耗网络资源。最初的设计是以秒作为 TTL的单位。每经过一个路由器时,就把TTL减去数据报在路由器消耗掉的一段时间。若数据报在路由器消耗的时间小于 1 秒,就把TTL值减 1。当 TTL值为 0时,就丢弃这个数据报。

  9. 协议:占 8 位,协议字段指出此数据报携带的数据是使用何种协议,以便使目的主机的IP层知道应将数据部分上交给哪个处理过程。

    比较常用的协议号:

    1 2 6 17 88 89
    ICMP IGMP TCP UDP IGRP OSPF
  10. 首部检验和:占16位。这个字段只检验数据报的首部,但不包括数据部分。这是因为数据报每经过一个路由器,都要重新计算一下首都检验和 (一些字段,如生存时间、标志、片偏移等都可能发生变化)。不检验数据部分可减少计算的工作量。

  11. IP源地址: 32位IP地址

  12. IP目的地址: 32位IP地址

TCP报文格式

  1. 源端口(Source port)和目的端口(Destination port)

    各16 bits。IP地址标识互联网中的不同终端,端口号标识终端中的不同应用进程,具有本地意义

  2. 序号(Sequence Number)和确认序号(Acknowledgment Number)

    各32 bits。TCP连接传输的字节流中的每一个字节都有序号。SN指示本报文段所发送的数据第一个字节的序号。AN指示期望收到对方的下一个报文的第一个字节的序号,所有小于AN的报文都被正确接收。

  3. 首部长度(Data offset)

    4 bits,以32-bit字为单位。TCP首部长短,也是TCP报文数据部分的偏移量。范围5~15,即20 bytes ~ 60 bytes。options部分最多允许40 bytes。

  4. 保留(Resevered)

    3 bits,将来使用,目前应设为0。

  5. 标志位(Flags)

    URG = 1,指示报文中有紧急数据,应尽快传送(相当于高优先级的数据)。

    PSH = 1,接到后尽快交付给接收的应用进程。

    RST = 1,TCP连接中出现严重差错(如主机崩溃),必须释放连接,在重新建立连接。

    FIN = 1,发送端已完成数据传输,请求释放连接。

    SYN = 1,处于TCP连接建立过程。

    ACK = 1,确认序号(AN)有效。

  6. 窗口(Window size)

    16 bits,接收窗口的大小。接收端希望接收的字节数。

  7. 校验和(Checksum)

    16 bits,校验报文首部、数据。

  8. 紧急指针(Urgent pointer)

    16 bits,如果URG = 1,该字段指示紧急数据的大小(相对于SN的偏移),紧急数据在数据部分的最前面。

  9. 可选项(Options)

    TCP报文的字段实现了TCP的功能,标识进程、对字节流拆分组装、差错控制、流量控制、建立和释放连接等。

UDP报文格式

  1. 源端口(Source port)和目的端口(Destination port)

  2. 报文长度(Length)

    16 bits,指示UDP报文(首部和数据)的总长度。最小8 bytes,只有首部,没有数据。最大值为65535 bytes。实际上,由于IPv4分组的最大数据长度为(65535 - 20 = 65515) bytes,UDP的报文长度不超过65515 bytes。IPv6允许UDP的长度超过65535,此时length字段设为0。

  3. 校验和(Checksum)

libpcap库的使用

  1. 获取网络接口

    首先我们需要获取监听的网络接口,我们可以手动指定或让libpcap自动选择,先介绍如何让libpcap自动选择:

    char * pcap_lookupdev(char * errbuf)

    上面这个函数返回第一个合适的网络接口的字符串指针,如果出错,则errbuf存放出错信息字符串,errbuf至少应该是PCAP_ERRBUF_SIZE个字节长度的。注意,很多libpcap函数都有这个参数。

    pcap_lookupdev()一般可以在跨平台的,且各个平台上的网络接口名称都不相同的情况下使用。如果我们手动指定要监听的网络接口,则这一步跳过,我们在第二步中将要监听的网络接口字符串硬编码在pcap_open_live里。

  2. 释放网络接口

    在操作为网络接口后,我们应该要释放它:

    void pcap_close(pcap_t * p)

    该函数用于关闭pcap_open_live()获取的pcap_t的网络接口对象并释放相关资源。

  3. 打开网络接口

    获取网络接口后,我们需要打开它:

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

    上面这个函数会返回指定接口的pcap_t类型指针,后面的所有操作都要使用这个指针。

    第一个参数是第一步获取的网络接口字符串,可以直接使用硬编码。

    第二个参数是对于每个数据包,从开头要抓多少个字节,我们可以设置这个值来只抓每个数据包的头部,而不关心具体的内容。典型的以太网帧长度是1518字节,但其他的某些协议的数据包会更长一点,但任何一个协议的一个数据包长度都必然小于65535个字节。

    第三个参数指定是否打开混杂模式(Promiscuous Mode),0表示非混杂模式,任何其他值表示混合模式。如果要打开混杂模式,那么网卡必须也要打开混杂模式

    第四个参数指定需要等待的毫秒数,超过这个数值后,第3步获取数据包的这几个函数就会立即返回。0表示一直等待直到有数据包到来。

    第五个参数是存放出错信息的数组。

  4. 获取数据包

    打开网络接口后就已经开始监听了,那如何知道收到了数据包呢?有下面3种方法:

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

      如果返回值为NULL,表示没有抓到包,注意这个函数只要收到一个数据包后就会立即返回

      第一个参数是第2步返回的pcap_t类型的指针

      第二个参数是保存收到的第一个数据包的pcap_pkthdr类型的指针

      pcap_pkthdr类型的定义如下:

         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) */
         };
    • int pcap_loop(pcap_t p, int cnt, pcap_handler callback, u_char user)

      第一个参数是第2步返回的pcap_t类型的指针

      第二个参数是需要抓的数据包的个数,一旦抓到了cnt个数据包,pcap_loop立即返回。负数的cnt表示pcap_loop永远循环抓包,直到出现错误。

      第三个参数是一个回调函数指针,它必须是如下的形式:

      void callback(u_char * userarg, const struct pcap_pkthdr * pkthdr, const u_char * packet)

      第一个参数是pcap_loop的最后一个参数,当收到足够数量的包后pcap_loop会调用callback回调函数,同时将pcap_loop()的user参数传递给它

      第二个参数是收到的数据包的pcap_pkthdr类型的指针

      第三个参数是收到的数据包数据

    • int pcap_dispatch(pcap_t p, int cnt, pcap_handler callback, u_char user)

      这个函数和pcap_loop()非常类似,只是在超过to_ms毫秒后就会返回(to_ms是pcap_open_live()的第4个参数)

  5. 分析数据包

  6. 过滤数据包

    • BPF使用一种类似于汇编语言的语法书写过滤表达式,不过libpcap和tcpdump都把它封装成更高级且更容易的语法了,具体可以man tcpdump

    • 构造完过滤表达式后,我们需要编译它:

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

      fp:这是一个传出参数,存放编译后的bpf

      str:过滤表达式

      optimize:是否需要优化过滤表达式

      metmask:简单设置为0即可

    • 最后我们需要应用这个过滤表达式:
      int pcap_setfilter(pcap_t p, struct bpf_program fp)
      第二个参数fp就是前一步pcap_compile()的第二个参数

实现思路

整体流程

根据上面PCAP库的使用,我们实现一个嗅探器,肯定要有下面几个过程:

  1. 获取网络设备

      char errBuf[PCAP_ERRBUF_SIZE], * devStr;
      devStr = pcap_lookupdev(errBuf);

    自动获取最佳的网卡

  2. 打开网络设备

      pcap_t * device = pcap_open_live(devStr, 65535, 1, 0, errBuf);
      if(!device)
      {
           printf("error: pcap_open_live(): %s\n", errBuf);
           exit(1);
      }

    第一个参数是第一步获取的网络接口字符串devStr

    第二个参数是对于每个数据包,从开头要抓多少个字节,这里我设置的是最大,即65535

    第三个参数指定确定打开混杂模式

    第四个参数指定需要等待的毫秒数,超过这个数值后,第3步获取数据包的这几个函数就会立即返回。0表示一直等待直到有数据包到来

    第五个参数是存放出错信息的数组

  3. 过滤数据包

      struct bpf_program filter;
      pcap_compile(device, &filter, "tcp || udp", 1, 0);
      pcap_setfilter(device, &filter);

    因为我的嗅探器只检测TCP和UDP的包,那么就设置规则为tcp || udp

  4. 接受数据包

      int id = 0;
      pcap_loop(device, -1, getPacket, (u_char*)&id);

    一直不停的进行数据包的接受,选择pcap_loop函数,并将第二个参数设置为-1

    int型数据id的作用是打印结果时的报文计数,没检测一个报文就加1

    接受的数据包的后续处理在回调函数getPacket中完成,后续我将主要将这个怎么实现

  5. 关闭设备

      pcap_close(device);

报文解析的实现

  1. 报头结构的定义和初始化

    根据前面部分描述的各种报文的结构,构造下列报头结构

    以太网帧报头如下:

      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报头如下:

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

    UDP报头如下:

      struct sniff_udp {
      uint16_t sport;       /* source port */
      uint16_t dport;       /* destination port */
      uint16_t udp_length;
      uint16_t udp_sum;     /* checksum 检验和 */
      };

    TCP报头如下:

      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 */
      u_char  th_flags;
      u_short th_win;                 /* window */
      u_short th_sum;                 /* checksum */
      u_short th_urp;                 /* urgent pointer */
      };

    初始化:

      struct sniff_ethernet *ethernet;        /* 以太网头部 */
      struct sniff_ip *ip;                    /* IP 头部    */
      struct sniff_tcp *tcp;                  /* TCP 头部   */
      struct sniff_udp *udp;                  /* UDP 头部   */
      unsigned char *payload;                 /* Packet payload */
  2. 报头定位

    定义以太网头部:

      ethernet = (struct sniff_ethernet*)(packet);

    定义IP层头部:

      ip = (struct sniff_ip*)(packet + SIZE_ETHERNET);

    确定协议(TCP还是UDP):

      switch(ip->ip_p) {
           case IPPROTO_TCP:       //useful
                printf("Protocol: TCP\n");
                proto_flag=0;
                break;
           case IPPROTO_UDP:       //useful
                printf("Protocol: UDP\n");
                proto_flag=1;
                break;
           case IPPROTO_IP:        //useful
                printf("Protocol: IP\n");
                return;
           default:
                printf("Protocol: unknown\n");
                return;
      }
  3. 输出解析信息

    参考Wireshark网络嗅探工具确认需求:首先要对报文进行计数和报时,然后要打印出源和目的MAC地址、IP地址、端口号,最后就是打印出报文总长度和应用层的内容

    • 报文计数和同步时间

         printf("Packet number: %d\n", ++(*id));
         printf("Recieved time: %s\n", ctime((const time_t *)&pkthdr->ts.tv_sec));
    • 以太网首部

      在这个部分,能实现报文长度的打印、MAC地址的打印

      报文长度:

         printf("Packet length: %d\n", pkthdr->len);
         printf("Number of bytes: %d\n", pkthdr->caplen);

      MAC地址:

         u_char *ptr;
         ptr = ethernet->ether_dhost;
         printf("Dst MAC addr: ");
         j = ETHER_ADDR_LEN;
         do{
              printf("%s%x",(j == ETHER_ADDR_LEN) ? " " : ":",*ptr++);
         }while(--j>0);
         printf("\n");
      
         ptr = ethernet->ether_shost;
         printf("Src MAC addr: ");
         j = ETHER_ADDR_LEN;
         do{
              printf("%s%x",(j == ETHER_ADDR_LEN) ? " " : ":",*ptr++);
         }while(--j>0);
      
         printf("\n\nIP:\n");

      使用一个指针*ptr和通过for循环来逐字打印,ptr初始指向ethernet->ether_dhost和ethernet->ether_shost

    • IP首部

      在这个地方可以过滤本机线程之间通过网络通信的数据包、IP地址、端口号、应用层数据

      过滤本机IP:

         #define MYIP "127.0.0.1"
      
         if(strcmp(inet_ntoa(ip->ip_src), MYIP) == 0)
              return;

      这里要注意这个函数inet_ntoa,将网络字节整数型IP地址转化字符串型,相反的其实还有函数inet_addr,将字符串信息转换为网络字节序的整数型

      还有一组函数也很重要:

         unsigned short htons(unsigned short);   //把short型数据从主机字节序转化为网络字节序
         unsigned short ntohs(unsigned short);
         unsigned short htonl(unsigned long);
         unsigned short ntohl(unsigned long);

      因为网络通信是跨平台的,但是有的操作系统是大端序排列,有的操作系统是小端序排列,为了防止出现不同平台通信错误,发送时地址统一转换成网络字节序排列,接收解析时从网络字节序转换为本地字节序

      如果是TCP:

         if (proto_flag == 0) {
         /* 定义/计算 TCP 头部偏移 */
         tcp = (struct sniff_tcp *) (packet + SIZE_ETHERNET + size_ip);
         printf("Src ip: %s\n", inet_ntoa(ip->ip_src));
         printf("Dst ip: %s\n", inet_ntoa(ip->ip_dst));
         printf ("Src port  : %d\n", ntohs (tcp->th_sport));
         printf ("Dst port  : %d\n", ntohs (tcp->th_dport));
      
         payload = (unsigned char *) (packet + SIZE_ETHERNET + size_ip + size_tcp);
         size_payload = ntohs (ip->ip_len) - (size_ip + size_tcp);
         }

      如果是UDP:

         else if (proto_flag == 1) {
         /* define/compute udp header offset */
         udp = (struct sniff_udp *) (packet + SIZE_ETHERNET + size_ip);
         printf("Src ip: %s\n", inet_ntoa(ip->ip_src));
         printf("Dst ip: %s\n", inet_ntoa(ip->ip_dst));
         printf ("Src port: %d\n", ntohs (udp->sport));
         printf ("Dst port: %d\n", ntohs (udp->dport));
      
         payload = (unsigned char *) (packet + SIZE_ETHERNET + size_ip + 8);
         size_payload = ntohs (ip->ip_len) - (size_ip + 8);
         }

      应用层数据只能打印出16进制或者将能转换为符号的字符转换为字符,这个函数很长,因为需要每16字节换行,同时输出两种,这里就不完全贴函数代码了,只简单描述一下思路,具体代码见后续完整代码

      首先是输出功能子函数print_hex_ascii_line,将当前行输出的16进制字符以及字符格式输出(匹配ASCII码),没有匹配的ASCII码则输出.来占位

      将上述print_hex_ascii_line子函数通过print_payload函数来调度输出,print_payload函数通过判断数据长度来确定要输出多少行

其他事项

  1. 在main函数获取网络设备后通过getchar()阻塞,先展示当前网卡的名称,按任一键后才开始展示嗅探的报文,提高交互性

      printf("Press any key to continue.\n");
      getchar();
  2. 程序编译

    因为用了libpcap库,所以编译时要加上-lpcap

      bear@bear-PC:~/Desktop/网路安全实验/sniffer$ gcc -o sniffer sniffer.c -lpcap
  3. 运行注意事项

    因为要调用网卡的混杂模式,所以,要提前将网卡设置为混杂模式

    同时,运行要root权限才行

    如果不注意上面的事项,之前设置的error会告诉你哪里错了,例如:

      bear@bear-PC:~/Desktop/网路安全实验/sniffer$ ./sniffer 
      success: device: wlp2s0
      Press any key to continue.
    
      error: pcap_open_live(): wlp2s0: You don't have permission to capture on that device (socket: Operation not permitted)
    

效果

开始运行时:

TCP报文:

TCP嗅探

UDP报文:

UDP嗅探

程序会一直运行实时更新数据

完整源码

#include <pcap.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

/* 以太网头部14个字节 */
#define SIZE_ETHERNET 14

/* 以太网地址6个字节 */
#define ETHER_ADDR_LEN 6

#define TCP_FLAG   0
#define UDP_FLAG   1
#define MYIP "192.168.253.134"

#define IP_HL(ip)               (((ip)->ip_vhl) & 0x0f)


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

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

struct sniff_udp {
    uint16_t sport;       /* source port */
    uint16_t dport;       /* destination port */
    uint16_t udp_length;
    uint16_t udp_sum;     /* checksum 检验和 */
};

typedef unsigned long 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 */
    u_char  th_flags;
    u_short th_win;                 /* window */
    u_short th_sum;                 /* checksum */
    u_short th_urp;                 /* urgent pointer */
};


void getPacket(u_char * arg, const struct pcap_pkthdr * pkthdr, const u_char * packet);
void print_payload(const u_char *payload, int len);
void print_hex_ascii_line(const u_char *payload, int len, int offset);


int main()
{
    char errBuf[PCAP_ERRBUF_SIZE], * devStr;

    //获取网络设备
    devStr = pcap_lookupdev(errBuf);
    if(devStr)
    {
        printf("success: device: %s\n", devStr);
    }
    else
    {
        printf("error: %s\n", errBuf);
        exit(1);
    }

    printf("Press any key to continue.\n");
    getchar();

    //打开网络设备
    pcap_t * device = pcap_open_live(devStr, 65535, 1, 0, errBuf);
    if(!device)
    {
        printf("error: pcap_open_live(): %s\n", errBuf);
        exit(1);
    }

    //过滤数据包(tcp和udp)
    struct bpf_program filter;
    pcap_compile(device, &filter, "tcp || udp", 1, 0);
    pcap_setfilter(device, &filter);

    //等待接收数据包
    int id = 0;
    pcap_loop(device, -1, getPacket, (u_char*)&id);

    //关闭设备
    pcap_close(device);

    return 0;
}

void getPacket(u_char * arg, const struct pcap_pkthdr * pkthdr, const u_char * packet)
{
    int * id = (int *)arg;
    int j;

    struct sniff_ethernet *ethernet;        /* 以太网头部 */
    struct sniff_ip *ip;                    /* IP 头部    */
    struct sniff_tcp *tcp;                  /* TCP 头部   */
    struct sniff_udp *udp;                  /* UDP 头部   */
    unsigned char *payload;                 /* Packet payload */
    int size_ip;
    int size_tcp;
    int size_udp;
    int size_payload;

    int proto_flag = 2;                     // 0=TCP_FLAG; 1=UDP_FLAG

    printf("\n==================================================================\n\n");
    printf("Packet number: %d\n", ++(*id));
    printf("Recieved time: %s\n", ctime((const time_t *)&pkthdr->ts.tv_sec));
    printf("MAC:\n");
    printf("Packet length: %d\n", pkthdr->len);
    printf("Number of bytes: %d\n", pkthdr->caplen);


    /* 定义以太网头部 */
    ethernet = (struct sniff_ethernet*)(packet);
    /* 定义/计算 IP 头部偏移 */
    ip = (struct sniff_ip*)(packet + SIZE_ETHERNET);
    size_ip = IP_HL(ip)*4;              // ip头部长度
    if (size_ip < 20) {
        printf("   * Invalid IP header length: %u bytes\n", size_ip);
        return;
    }

    //打印MAC地址
    u_char *ptr;
    ptr = ethernet->ether_dhost;
    printf("Dst MAC addr: ");
    j = ETHER_ADDR_LEN;
    do{
        printf("%s%x",(j == ETHER_ADDR_LEN) ? " " : ":",*ptr++);
    }while(--j>0);
    printf("\n");

    ptr = ethernet->ether_shost;
    printf("Src MAC addr: ");
    j = ETHER_ADDR_LEN;
    do{
        printf("%s%x",(j == ETHER_ADDR_LEN) ? " " : ":",*ptr++);
    }while(--j>0);

    printf("\n\nIP:\n");


    /* 显示源IP和目的IP     print source and destination IP addresses */
    // only print internet->me information
    if(strcmp(inet_ntoa(ip->ip_src), MYIP) == 0)
        return;

    /* 确定协议 determine protocol */
    switch(ip->ip_p) {
        case IPPROTO_TCP:       //useful
            printf("Protocol: TCP\n");
            proto_flag=0;
            break;
        case IPPROTO_UDP:       //useful
            printf("Protocol: UDP\n");
            proto_flag=1;
            break;
        case IPPROTO_IP:        //useful
            printf("Protocol: IP\n");
            return;
        default:
            printf("Protocol: unknown\n");
            return;
    }

//This packet is TCP.
    if (proto_flag == 0) {
        /* 定义/计算 TCP 头部偏移 */
        tcp = (struct sniff_tcp *) (packet + SIZE_ETHERNET + size_ip);
        printf("Src ip: %s\n", inet_ntoa(ip->ip_src));
        printf("Dst ip: %s\n", inet_ntoa(ip->ip_dst));
        printf ("Src port  : %d\n", ntohs (tcp->th_sport));
        printf ("Dst port  : %d\n", ntohs (tcp->th_dport));

        payload = (unsigned char *) (packet + SIZE_ETHERNET + size_ip + size_tcp);
        size_payload = ntohs (ip->ip_len) - (size_ip + size_tcp);
    }

//This packet is UDP.
    else if (proto_flag == 1) {
      /* define/compute udp header offset */
        udp = (struct sniff_udp *) (packet + SIZE_ETHERNET + size_ip);
        printf("Src ip: %s\n", inet_ntoa(ip->ip_src));
        printf("Dst ip: %s\n", inet_ntoa(ip->ip_dst));
        printf ("Src port: %d\n", ntohs (udp->sport));
        printf ("Dst port: %d\n", ntohs (udp->dport));

        payload = (unsigned char *) (packet + SIZE_ETHERNET + size_ip + 8);
        size_payload = ntohs (ip->ip_len) - (size_ip + 8);
    }

    printf("\n");
    print_payload(payload,size_payload);

    //printf("\n\n");
}


void print_payload(const u_char *payload, int len)
{
    int len_rem = len;
    int line_width = 16;   /* number of bytes per line */
    int line_len;
    int offset = 0;     /* zero-based offset counter */
    const u_char *ch = payload;

    if (len <= 0)
        return;

    /* data fits on one line */
    if (len <= line_width) {
        print_hex_ascii_line(ch, len, offset);
        return;
    }

    /* data spans multiple lines */
    for ( ;; ) {
        /* compute current line length */
        line_len = line_width % len_rem;
        /* print line */
        print_hex_ascii_line(ch, line_len, offset);
        /* compute total remaining */
        len_rem = len_rem - line_len;
        /* shift pointer to remaining bytes to print */
        ch = ch + line_len;
        /* add offset */
        offset = offset + line_width;
        /* check if we have line width chars or less */
        if (len_rem <= line_width) {
            /* print last line and get out */
            print_hex_ascii_line(ch, len_rem, offset);
            break;
        }  
    }
    return;
}


/*
 * print data in rows of 16 bytes: offset   hex   ascii
 *
 * 00000   47 45 54 20 2f 20 48 54  54 50 2f 31 2e 31 0d 0a   GET / HTTP/1.1..
 */
void print_hex_ascii_line(const u_char *payload, int len, int offset)
{
    int i;
    int gap;
    const u_char *ch;

    /* offset */
    printf("%05d   ", offset);

    /* hex */
    ch = payload;
    for(i = 0; i < len; i++) {
        printf("%02x ", *ch);
        ch++;
        /* print extra space after 8th byte for visual aid */
        if (i == 7)
        printf(" ");
    }
    /* print space to handle line less than 8 bytes */
    if (len < 8)
        printf(" ");

    /* fill hex gap with spaces if not full line */
    if (len < 16) {
        gap = 16 - len;
        for (i = 0; i < gap; i++) {
            printf("   ");
        }
    }
    printf("   ");

    /* ascii (if printable) */
    ch = payload;
    for(i = 0; i < len; i++) {
        if (isprint(*ch))
            printf("%c", *ch);
        else
            printf(".");
        ch++;
    }
    printf("\n");
    return;
}
Last modification:November 17th, 2020 at 02:53 pm