查看原文
其他

一文教你如何用 C 代码解析一段网络数据包?

CPP开发者 2022-07-01

The following article is from 一口Linux Author 土豆居士

本文的目的是通过随机截取的一段网络数据包,然后根据协议类型来解析出这段内存。

学习本文需要掌握的基础知识:

  1. 网络协议
  2. C语言
  3. Linux操作
  4. 抓包工具的使用

其中抓包工具的安装和使用见下文:

一文包你学会网络数据抓包

一、截取一个网络数据包

通过抓包工具,随机抓取一个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协议后面的额协议类型,常见的值如下:

数值描述
0保留字段,用于IPv6(跳跃点到跳跃点选项)
1Internet控制消息 (ICMP)
2Internet组管理 (IGMP)
3网关到网关 (GGP)
41P中的IP(封装)
6传输控制 (TCP)
7CBT
8外部网关协议 (EGP)
9任何私有内部网关(Cisco在它的IGRP实现中使用) (IGP)
10BBNRCC监视
11网络语音协议
12PUP
13ARGUS
14EMCON
15网络诊断工具
16混乱(Chaos)
17用户数据报文 (UDP)
411Pv6
581Pv6的ICMP
591Pv6的无下一个报头
60IPv6的信宿选项
89OSPF IGP
92多播传输协议
94IP内部的IP封装协议
95可移动网络互连控制协议
96旗语通讯安全协议
97IP中的以太封装
98封装报头
100GMTP
101Ipsilon流量管理协议
133~254未分配
255保留

五、解析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");
  }
 }
 ………………
}

结果如下:

好了,这个例子掌握了,那么网络就算入门了,快操练起来吧!

完整代码请公众号后台回复:数据包解析


- EOF -

推荐阅读  点击标题可跳转

1、编写可移植 C/C++ 程序的一些要点

2、C++ 内存管理(建议收藏)

3、Modern C++ 有哪些能真正提升开发效率的语法糖?


关注『CPP开发者』

看精选C++技术文章 . 加C++开发者专属圈子

点赞和在看就是最大的支持❤️

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存