本文的目的是通过随机截取的一段网络数据包,然后根据协议类型来解析出这段内存。
学习本文需要掌握的基础知识:
- 网络协议
- C语言
- Linux操作
- 抓包工具的使用
其中抓包工具的安装和使用见下文:
视频教学链接如下:
一、截取一个网络数据包
通过抓包工具,随机抓取一个tcp数据包
科莱抓包工具解析出的数据包信息如下:
数据包的内存信息:
数据信息可以直接拷贝出来:
二、用到的结构体
下面,一口君就手把手教大家如何解析出这些数据包的信息。
我们可以从Linux内核中找到协议头的定义
以太头:
- drivers\staging\rtl8188eu\include\if_ether.h
- struct ethhdr {
- unsigned char h_dest[ETH_ALEN]; /* destination eth addr */
- unsigned char h_source[ETH_ALEN]; /* source ether addr */
- unsigned short h_proto; /* packet type ID field */
- };
IP头
- include\uapi\linux\ip.h
- struct iphdr {
- #if defined(__LITTLE_ENDIAN_BITFIELD) //小端模式
- __u8 ihl:4,
- version:4;
- #elif defined(__BIG_ENDIAN_BITFIELD) //大端模式
- __u8 version:4,
- ihl:4;
- #endif
- __u8 tos;
- __u16 tot_len;
- __u16 id;
- __u16 frag_off;
- __u8 ttl;
- __u8 protocol;
- __u16 check;
- __u32 saddr;
- __u32 daddr;
- /*The options start here. */
- };
tcp头
- include\uapi\linux\tcp.h
- struct tcphdr {
- __be16 source;
- __be16 dest;
- __be32 seq;
- __be32 ack_seq;
- #if defined(__LITTLE_ENDIAN_BITFIELD)
- __u16 res1:4,
- doff:4,
- fin:1,
- syn:1,
- rst:1,
- psh:1,
- ack:1,
- urg:1,
- ece:1,
- cwr:1;
- #elif defined(__BIG_ENDIAN_BITFIELD)
- __u16 doff:4,
- res1:4,
- cwr:1,
- ece:1,
- urg:1,
- ack:1,
- psh:1,
- rst:1,
- syn:1,
- fin:1;
- #else
- #error "Adjust your <asm/byteorder.h> defines"
- #endif
- __be16 window;
- __sum16 check;
- __be16 urg_ptr;
- };
因为协议头长度都是按照标准协议来定义的,
所以以太长度是14, IP头长度是20, tcp头长度是20,
各个协议头对应的内存空间如下:
三、解析以太头
- #define MAC_ARG(p) p[0],p[1],p[2],p[3],p[4],p[5]
- struct ethhdr *ethh;
- unsigned char *p = pkt;
- ethh = (struct ethhdr *)p;
- printf("h_dest:%02x:%02x:%02x:%02x:%02x:%02x \n", MAC_ARG(ethh->h_dest));
- printf("h_source:%02x:%02x:%02x:%02x:%02x:%02x \n", MAC_ARG(ethh->h_source));
- printf("h_proto:%04x\n",ntohs(ethh->h_proto));
- 注意,数据包中的数据是网络字节序,如果要提取数据一定要注意字节序问题 ethh->h_proto 是short类型,占2个字节,所以存储到本地需要使用函数ntohs 其中:
- n:network 网络字节序
- h:host 主机字节序
- s:short 2个字节 l:
- long 4个字节
- ntohl() :4字节网络字节序数据转换成主机字节序
- htons() :2字节主机字节序数据转换成网络字节序
- ntohs() :2字节网络字节序数据转换成主机字节序
- htonl() :4字节主机字节序数据转换成网络字节序
当执行下面这条语句时,
- ethh = (struct ethhdr *)p;
结构体指针变量eth的成员对应关系如下:
最终打印结果如下:
四、解析ip头
解析ip头思路很简单,
就是从pkt头开始偏移过以太头长度(14字节)就可以找到IP头,
解析代码如下:
- #define IP_ARG(p) p[0],p[1],p[2],p[3]
- /*
- 解析IP头
- */
- if(ntohs(ethh->h_proto) == 0x0800)
- {
- iph = (struct iphdr *)(p + sizeof(struct ethhdr));
- q = (unsigned char *)&(iph->saddr);
- printf("src ip:%d.%d.%d.%d\n",IP_ARG(q));
- q = (unsigned char *)&(iph->daddr);
- printf("dest ip:%d.%d.%d.%d\n",IP_ARG(q));
- }
Iiph
最终解析结果如下:
可以看到我们正确解析出了IP地址, 结果与抓包工具分析出的数据保持了一致。
其中protocol字段表示了ip协议后面的额协议类型,常见的值如下:
五、解析tcp头
查找tcp头思路很,
就是从pkt头开始偏移过以太头长度(14字节)、和IP头长度(20字节)就可以找到tcp头,
- switch(iph->protocol)
- {
- case 0x1:
- //icmp
- break;
- case 0x6:
- //tcp
- tcph = (struct tcphdr *)(p + sizeof(struct ethhdr) + sizeof(struct iphdr));
- printf("source:%d dest:%d \n",ntohs(tcph->source),ntohs(tcph->dest);
- break;
- case 0x11:
- //udp
- break;
- }
结构体与内存对应关系
打印结果如下:
六、学会用不同格式打印这块内存
在实际项目中,可能我们解析的并不是标准的TCP/IP协议数据包,
可能是我们自己的定义的协议数据包,
只要掌握了上述方法,
所有的协议分析都能够手到擒来!
有时候我们还需要打印对方发送过来的数据帧内容,
往往我们会以16进制形式将所有数据打印出来,
这样是最有利于我们分析数据内容的。
1. 按字节打印
代码如下:
- for(i=0;i<400;i++)
- {
- printf("%02x ",pkt[i]);
- if(i%20 == 19)
- {
- printf("\n");
- }
- }
2. 按short类型分析一段内存
我们接收数据时,虽然使用一个unsigned char型数组,
但是有时候对方发送过来的数据可能是2个字节的数组,
那我们只需要用short类型的指针,指向内存的头,
然后就可以通过该指针访问到对方发送的数据,
这个时候一定要注意字节序问题,
不同场景可能不一样,所以一定要具体问题具体分析,
本例因为是网络字节序数据转换成主机字节序,
所以需要转换字节序。
- //转变short型字节序
- void indian_reverse(unsigned short arr[],int num)
- {
- int i;
- unsigned short temp;
- for(i=0;i<num;i++)
- {
- temp = 0;
- temp = (arr[i]&0xff00)>>8;
- temp |= (arr[i]&0xff)<<8;
- arr[i] = temp;
- }
- }
- main()
- {
- unsigned short spkt[200];
- ………………
- memcpy(spkt,pkt,sizeof(pkt));
- indian_reverse(spkt,ARRAY_SIZE(spkt));
- for(i=0;i<200;i++)
- {
- printf("%04x ",spkt[i]);
- if(i%10 == 9)
- {
- printf("\n");
- }
- }
- ………………
- }
结果如下:
转载本文请联系一口Linux公众号。