PYTHON 解码 IP 层

PYTHON 解码 IP 层

      • 引言
      • 1.编写流量嗅探器
      • 1.1 Windows 和 Linux 上的包嗅探
      • 2.解码 IP 层
      • 2.1 struct 库
      • 3.编写 IP 解码器
      • 4.解码 ICMP
      • 5.总结

作者:高玉涵

时间:2023.7.12

环境:Windows 10 专业版 22H2,Python 3.10.4

引言

IP 是 TCP/IP 协议族中最为核心的协议。所有的 TCP、UDP、ICMP 及 IGMP 数据都以 IP 数据报格式传输。图 1-1 所示为典型的 IPv4 头结构。但里面的信息是以二进制形式封装的,许多刚开始接触 TCP/IP 的人(包括我)很难直接读懂。这里我们可以利用 PYTHON 这门简洁且强大的语言来捕获和解码网络数据包的 IP 头部分,结合原理学习网络基础知识,往往能达到事半功倍的效果。

在这里插入图片描述

(图 1-1)

1.编写流量嗅探器

网络上有如 Wireshark 这样现成的嗅控工具,但不管怎样,学会自己编写简单的嗅探器来浏览和解码流量仍然很有好处。你不仅能学到一些新的 PYTHON 技巧,更加深对底层网络实现的理解。这里我将使用原始 socket 来读/写原始 IP 头或 ICMP 头等底层信息。

1.1 Windows 和 Linux 上的包嗅探

在 Windows 和 Linux 上操作原始 socket 的步骤不太相同,但嗅探工具需要具备足够的灵活性以便部署到不同平台。考虑到这一点,我们在创建 socket 对象后会检测系统环境。如果是 Windows 系统,就需要通过 socket 输入/输出控制(IOCTL)机制来设定一些标志,启用网卡的混杂模式。IOCTL 是用户程序和系统内核组件通信的一种方式,更多细节可以参考[百度百科](ioctl_百度百科 (baidu.com))中的解释。

现在我们来写第一个例子——一个简单的原始 socket 嗅探器,它只会读取一个数据包,然后直接退出:

import socket
import os# 监听的主机
HOST = '192.168.1.105'def main():# 创建原始 socket, 绑定公共接口if os.name == 'nt':socket_protocol = socket.IPPROTO_IPelse:socket_protocol = socket.IPPROTO_ICMPsniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW,socket_protocol)sniffer.bind((HOST, 0))# 捕获 IP 头sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)# 开启混杂模式if os.name == 'nt':sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)# 读数据包print(sniffer.recvfrom(65565))# 关闭混杂模式if os.name == 'nt':sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)if __name__=='__main__':main()

先把 HOST 变量设定成本机 IP 地址,然后构建一个 socket 对象,传入嗅探网卡数据所需的参数。这里 Windows 和 Linux 的区别是,前者允许我们嗅探任何协议的所有流入数据,而后者强制我们指定一个协议来嗅探,这里指定的是 ICMP。注意,你需要拥有 Windows 的管理员权限或 Linux 的 root 权限才能启用网卡的混杂模式。启用混杂模式后,就能嗅探到流经网卡的所有数据包,包括那些不归我们接收的数据包。接着,修改 socket 的设置,让它抓包时包含 IP 头。下一步,判断程序是不是运行在 Windows 上,如果是,就额外发送一条 IOCTL 消息启用网卡的混杂模式。现在我们就做好嗅探数据的准备了。在本例中只输出了原始数据包的全部内容,没有实际解码里面的信息,因为目前我们只想确认核心代码都能正常工作。嗅探完一个数据包后,我们会再次检测现在是不是在 Windows 平台,关闭网卡的混杂模式,然后退出。

Windows 命令提示符窗口,运行以下命令:

python sniffer.py
(b'E\x00\x004\x88\x8c@\x00\x80\x06\x00\x00\xc0\xa8\x01i\xc0\xa8\x1f\xa5\xd5\x8d\x1e\x005\t\xed\xb7\x00\x00\x00\x00\x80\x02\xfa\xf0\xa2\x85\x00\x00\x02\x04\x05\xb4\x01\x03\x03\x08\x01\x01\x04\x02', ('192.168.1.105', 0))

你会看到类似于下面内容的乱七八糟的输出。说实话,嗅探一个数据包用处不大,所以我们添加一些新功能,并解码其中的信息。

2.解码 IP 层

如果你分析过网络实际的数据包,应该能明白为什么我们需要对数据解码。图 1-1 所示。我们需要解码整个 IP 头(除了可选参数部分),并提取协议类型、源 IP 地址和目的 IP 地址等信息。这就意味着要直接跟二进制数据打交道,因此我们要找出一套用 PYTHON 分割 IP 头各个数据段的方案。

在 PYTHON 中,要把外来的二进制数据分解成数据结构有不少办法。比如,你可以用 ctypes 库或 struct 库来定义所需的数据结构。 在网上浏览各种开源工具时,你会发现这两种方法各有不少项目在用。本节会使用 struct 库读取 IPv4 头,因为它更专注于操作二进制数据。

2.1 struct 库

struct 库提供了一些格式字符,用来定义二进制数据的结构。在下面的示例中,我们将定义一个 IP 类来存储 IP 头信息。

import ipaddress
import structclass IP:def __init__(self, buff=None):header = struct.unpack('<BBHHHBBH4s4s', buff)self.ver = header[0] >> 4self.ih1 = header[0] & 0xFself.tos = header[1]self.len = header[2]self.id = header[3]self.offset = header[4]self.ttl = header[5]self.protocol_num = header[6]self.sum = header[7]self.src = header[8]self.dst = header[9]# 人类可读 IP 地址self.src_address = ipaddress.ip_address(self.src)self.dst_address = ipaddress.ip_address(self.dst)# 协议常量self.protocol_map = {1: "ICMP", 6: "TCP", 17: "UDP"}

第一个格式字符(示例中的 < )永远都是用来表示数据的字节序的。C 数据类型一般是按照设备中的原生格式和字节序来存储的。在这个例子中,我们使用的是 Windows 系统(x64架构),它使用的是小端序(little-endian)。在小端序设备上,低位字节会被放在较低的内存地址上,高位字节会被放在较高的内存地址上。

接下来的格式字符是用来表示 IP 头的各部分的。struct 库提供了若干格式字符。对于 IP 头来说,我们只需要用到的只有 B(1字节,unsigned char)、H(2字节,unsigned short)和 s(一个字节数组,数组长度需要另外指定,比如 4s 就代表为 4 字节的字节数组)。留意一下我们的格式字符串和图 1-1 中所示的 IP 头结构是如何一一对应的。

在 struct 库里,不存在对应 4 位二进制位组成的数据格式字符,所以我们需要额外做一些操作,把 ver 和 hdrlen 变量从 IP 头的第一个字节里提取出来。

对于 IP 头的第一个字节,我们只想取高位作为 ver 的值。取某字节高位的常规方法是将其向右位移 4 位,相当于在该字节的开头填 4 个 0,把其尾部的 4 位挤出去。这样我们就得到了原字节的高位值。这一行 PYTHON 代码基本上就是做了如下操作:

0 1 0 1 0 1 1 0  >> 4
----------------
0 0 0 0 0 1 0 1

我们想把低位(或者说原字节的最后 4 个二进制位)填进 hdrlen 里,取某个字节低位的常规方法是将其与数字 0xF(00001111)进行按位与运算。它利用了 0 AND 1 = 0 的特性(0 代表假,1 代表真)。想要 AND 表达式为真,表达式两边都必须为真。所以这个操作相当于删除前 4 个二进制位,因为任何数 AND 0 都得 0;它保持了最后 4 个二进制位不变,因为任何数 AND 1 还是原数字。所以,基本上这一行 PYTHON 代码做的就是如下操作:

    0 1 0 1 0 1 1 0
AND 0 0 0 0 1 1 1 1
--------------------0 0 0 0 0 1 1 0

解析 IP 头其实不需要你掌握太多位运算知识,但在这种需要位移操作的情况下,解析二进制数据需要费点心思。其他情况(比如解析 ICMP 消息)大多没这么麻烦:ICMP 消息里的每一个字段位数都是 8 的整数倍,struct 的格式字符位数也都是 8 的整数倍。在图 2-1 所示的 Echo Reply ICMP 消息结构中,你可以看到 ICMP 头的每个字段都可以用一个格式字符组合来表示(BBHHH)。
在这里插入图片描述

图 2-1

因此,解析 ICMP 头结构的办法非常简单,只要为前两个成员变量分配 1 字节,为后三个成员变量分配 2 字节就可以了。

class ICMP:def __init__(self, buff):header = struct.unpack('<BBHHH', buff)self.type = header[0]self.code = header[1]self.sum = header[2]self.id = header[3]self.seq = header[4]

3.编写 IP 解码器

现在,把刚才设计的 IP 头解码代码写下来,文件名就叫 sniffer_ip_header_decode.py,文件内容如下所示:

import ipaddress
import os
import socket
import struct
import sysclass IP:def __init__(self, buff=None):header = struct.unpack('<BBHHHBBH4s4s', buff)self.ver = header[0] >> 4self.ih1 = header[0] & 0xFself.tos = header[1]self.len = header[2]self.id = header[3]self.offset = header[4]self.ttl = header[5]self.protocol_num = header[6]self.sum = header[7]self.src = header[8]self.dst = header[9]# 人类可读 IP 地址self.src_address = ipaddress.ip_address(self.src)self.dst_address = ipaddress.ip_address(self.dst)# 常用服务映射到名字self.protocol_map = {1: "ICMP", 6: "TCP", 17: "UDP"}try:self.protocol = self.protocol_map[self.protocol_num]except Exception as e:print('%s No protocol for %s' % (e, self.protocol_num))self.protocol = str(self.protocol_num)def sniff(host):if os.name == 'nt':socket_protocol = socket.IPPROTO_IPelse:socket_protocol = socket.IPPROTO_ICMPsniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol)sniffer.bind((host, 0))sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)if os.name == 'nt':sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)try:while True:raw_buffer = sniffer.recvfrom(65565)[0]# 取前 20 个字节创建 IP 头ip_header = IP(raw_buffer[0:20])# 打印检测的 IP 和协议print('Protocol: %s %s -> %s' % (ip_header.protocol,ip_header.src_address,ip_header.dst_address))except KeyboardInterrupt:if os.name == 'nt':sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)sys.exit()if __name__ == '__main__':if len(sys.argv) == 2:host = sys.argv[1]else:host = '192.168.1.105'sniff(host)

首先,我们写下刚才的 IP 类,它定义了一个 PYTHON 结构,可以把数据包的前 20 字节映射到一个便于读/写的 IP 头对象里。如你所见,我们辨识出的所有字段都和标准的 IP 头结构完美契合。然后整理数据,将其输出为人类可读的形式,展示目前的通信协议和通信双方的 IP 地址。用上了新打造的 IP 头结构后,我们把抓包的逻辑改成持续抓包和解析。每读入一个包,就将它的前 20 字节转换成 IP 头对象。接着,只需要把抓取的信息打印到屏幕上就可以了。

我们来测试刚才写的代码,看看能从原始数据包中提取出什么样的信息。建议在 Windows 设备上测试这些代码,因为这样就能同时看到 TCP、UDP 和 ICMP 等协议的数据,易于进行一些简便的测试(比如直接打开浏览器浏览网页)。如果你不得不使用 Linux 系统,那就做一次 ping 测试吧。

python sniffer_ip_header_decode.py
Protocol: UDP 192.168.1.104 -> 239.255.255.250
Protocol: UDP 192.168.1.1 -> 239.255.255.250
Protocol: UDP 192.168.1.1 -> 239.255.255.250
Protocol: UDP 192.168.1.1 -> 239.255.255.250
Protocol: UDP 192.168.1.1 -> 239.255.255.250
Protocol: UDP 192.168.1.1 -> 239.255.255.250
Protocol: UDP 192.168.1.1 -> 239.255.255.250
Protocol: UDP 192.168.1.1 -> 239.255.255.250
Protocol: UDP 192.168.1.1 -> 239.255.255.250
Protocol: UDP 192.168.1.1 -> 239.255.255.250
Protocol: UDP 192.168.1.1 -> 239.255.255.250
Protocol: UDP 192.168.1.104 -> 239.255.255.250
Protocol: TCP 192.168.1.105 -> 20.198.162.78

因为 Windows 是个挺“健谈”的系统,所以你很可能立即就看到了测试结果。接下来,我们要运用解码 IP 头的技术来解码 ICMP 消息。

4.解码 ICMP

现在我们已经可以完整解码数据包层的 IP 层,接下来还需要解码网络数据包触发的 ICMP 响应。不同的 ICMP 消息之间千差万别,但有三个字段是一定存在的:类型(type)、代码(code)和校验和(checksum)。类型和代码两个字段告诉接收者,接下来要接收的 ICMP 信息是什么类型的,也就指明了如何正确地解码里面的数据。

这里我们需要检查类型为3、代码为 3 的 ICMP 消息。类型为 3 表示目标不可达(Destination Unreachable),而代码为 3 表示导致目标不可达的具体原因是端口不可达(Port Unreachable)。图 4-1 展示的就是 Destination Unreachable 类型的 ICMP 消息结构。
在这里插入图片描述

图 4-1

可以看到,数据包开头的 8 个二进制位代表类型,其后的 8 个二进制位代表 ICMP 代码。这里注意一个有意思的细节:当一台主机发送出 ICMP 消息的时候,会把触发 ICMP 消息的原始数据包的 IP 头附在消息未尾。另外,为了确认这个 ICMP 消息是被我们触发的,还可以自定义 8 字节的特征数据放在 UDP 数据包的开头,然后与接收到的 ICMP 消息的最后 8 字节 进行对比。关于这个技巧常用于黑客扫描工具,本文我们只研究解码数据包,另,找机会再讨论。

文件名 sniffer_with_icmp.py,文件内容如下所示:

import ipaddress
import os
import socket
import struct
import sysclass IP:def __init__(self, buff=None):header = struct.unpack('<BBHHHBBH4s4s', buff)self.ver = header[0] >> 4self.ih1 = header[0] & 0xFself.tos = header[1]self.len = header[2]self.id = header[3]self.offset = header[4]self.ttl = header[5]self.protocol_num = header[6]self.sum = header[7]self.src = header[8]self.dst = header[9]# 人类可读 IP 地址self.src_address = ipaddress.ip_address(self.src)self.dst_address = ipaddress.ip_address(self.dst)# 常用服务映射到名字self.protocol_map = {1: "ICMP", 6: "TCP", 17: "UDP"}try:self.protocol = self.protocol_map[self.protocol_num]except Exception as e:print('%s No protocol for %s' % (e, self.protocol_num))self.protocol = str(self.protocol_num)class ICMP:def __init__(self, buff):header = struct.unpack('<BBHHH', buff)self.type = header[0]self.code = header[1]self.sum = header[2]self.id = header[3]self.seq = header[4]def sniff(host):if os.name == 'nt':socket_protocol = socket.IPPROTO_IPelse:socket_protocol = socket.IPPROTO_ICMPsniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol)sniffer.bind((host, 0))sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)if os.name == 'nt':sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)try:while True:raw_buffer = sniffer.recvfrom(65565)[0]# 取前 20 个字节创建 IP 头ip_header = IP(raw_buffer[0:20])# ICMPif ip_header.protocol == "ICMP":print('Protocol: %s %s -> %s' % (ip_header.protocol,ip_header.src_address,ip_header.dst_address))print(f'Version:{ip_header.ver}')print(f'Header Lenght:{ip_header.ih1} TTL:{ip_header.ttl}')# 计算 ICMP 包开始位置offset = ip_header.ih1 * 4buf = raw_buffer[offset:offset + 8]# 创建 ICMP 对象icmp_header = ICMP(buf)print('ICMP -> Type: %s Code: %s\n' %(icmp_header.type, icmp_header.code))except KeyboardInterrupt:if os.name == 'nt':sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)sys.exit()if __name__ == '__main__':if len(sys.argv) == 2:host = sys.argv[1]else:host = '192.168.1.105'sniff(host)

这段简单的代码在之前的 IP 结构下方又创建了一个 ICMP 结构。在负责接收数据包的主循环中,我们会判断接收到的数据包是否为 ICMP 数据包,然后计算出 ICMP 数据在原始数据包中的偏移,最后将数据按照 ICMP 结构进行解析,输出其中的类型(type)和代码(code)字段。IP 头的长度是基于 IP 头中的 ih1字段计算的,该字段记录了 IP 头中有多少个 32 位(4字节)长的数据块。所以只需要将这个字段乘 4,就能计算出 IP 头的大小,以及数据包中下一个网络层(这里指 ICMP)开始的位置。

Windows 命令提示符窗口,运行以下命令:

python sniffer_with_icmp.py

另,打开一个命令行窗口执行 ping 命令:

ping www.baidu.com

输出结果:

Protocol: ICMP 192.168.1.105 -> 47.254.33.193
Version:4
Header Lenght:5 TTL:128
ICMP -> Type: 8 Code: 0Protocol: ICMP 47.254.33.193 -> 192.168.1.105
Version:4
Header Lenght:5 TTL:50
ICMP -> Type: 0 Code: 0Protocol: ICMP 192.168.1.105 -> 47.254.33.193
Version:4
Header Lenght:5 TTL:128
ICMP -> Type: 8 Code: 0

这表明 ping (ICMP Echo)响应数据被正确地接收并解码了。

5.总结

通过上述举例。我的初衷只是为了让原本乏味枯燥的,网络相关理论知识变的有趣,为我们接下来深入学习起到“抛转引玉”的作用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/19142.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

十四、flex弹性容器属性样式2

目录&#xff1a; 1.准备工作 2.属性解析&#xff1a; align-items 3.属性解析&#xff1a; align-content 4.弹性元素的属性 一、准备工作 我们在前面的基础上&#xff0c;修改代码&#xff0c;把ul的高度定下来&#xff0c;设置800px, li的高度不定。 然后&#xff0c;body里…

LVS + keepalived

一、keepalived概述1.1 keepalived 服务重要功能1.1.1 管理LVS负载均衡器软件1.1.2 支持故障自动切换&#xff08;failover&#xff09;1.1.3 实现LVS集中节点的健康检查&#xff08;health checking&#xff09;1.1.4 实现 LVS 负载调度器、节点服务器的高可用性&#xff08;H…

OpenAI GPT-4 Code Interpreter测试

OpenAI GPT-4 Beta版本Code Interpreter功能分析 OpenAI最近在GPT-4中推出了Code Interpreter功能的Beta版本&#xff0c;它是ChatGPT的一个版本&#xff0c;可以编写和执行Python代码&#xff0c;并处理文件上传。以下是对其表现的基本分析。 主要功能 文件信息获取&#xf…

Blender基础入门(0):下载和资源

文章目录 我个人的Blender专栏前言相关资料Blender和C4D如何选择视频资源BlenderBlender官网下载基础设置常用快捷键介绍空格键&#xff1a;跳出选择框ShiftA&#xff1a;跳出添加框选中物体按F9:显示物体属性 Blender能做到什么总结 我个人的Blender专栏 Blender简单教学 前…

冯诺依曼结构和操作系统的理解

在正式讲解进程之前&#xff0c;需要先铺垫一些基本知识. 目录 冯诺依曼结构 操作系统 冯诺依曼结构 这个名词相信大家非常熟悉&#xff0c;我们常见的计算机&#xff0c;如笔记本。我们不常见的计算机&#xff0c;如服务器&#xff0c;大部分都遵守冯诺依曼体系。 这张图…

从零开始学习自动驾驶决策规划

从零开始学习自动驾驶决策规划 从入门到掌握的一系列讲解&#xff0c;其中涵盖的内容如下&#xff1a; 前言课 第一节-ros工程的创建 第一节-运行环境和工程目录简介第二节-工程运行和小车模型搭建简介 第二节-车辆里程计第三节-整体架构思路 第三节-地图路线构建方法 第三节…

Spring Cache (基础知识+瑞吉外卖项目)

Spring Cache 基本介绍 Spring Cache是一个框架&#xff0c;实现了基于注解的缓存功能&#xff0c;只需要简单地加一个注解&#xff0c;就能实现缓存功能。 Spring Cache提供了一层抽象&#xff0c;底层可以切换不同的cache实现。具体就是通过CacheManager接口来统一不同的缓…

基于github制作个人学术网站(主页)

模板 首先找到一个学术模板&#xff0c;fork到远程仓库。academicpages&#xff0c;如果不是很清楚具体的步骤&#xff0c;可以参考保姆级教程。在github上对该网站代码修改不是很方便&#xff0c;肯定是在本地进行更新后push到远程仓库。 本地Git 学会下载和安装就行&#…

基于高斯混合模型聚类的风电场短期功率预测方法(Pythonmatlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

正则表达式测试(二)

一、中括号的语法 匹配所有的字符&#xff0c;返回一个数组,包含匹配的所有字符内容&#xff0c;按顺序展开&#xff1b; 注意&#xff1a;空格也会被匹配到 匹配所有符合的字符&#xff0c;返回一个数组。 匹配空白字符 匹配非空白字符 匹配 空白字符 非空白字符 如上所示&am…

生成式AI:大语言模型ChatGPT交互的机制

推荐&#xff1a;将NSDT场景编辑器加入你的3D工具链 3D工具集&#xff1a;NSDT简石数字孪生 与 ChatGPT 有效交互的快速工程 随着生成式人工智能的普及&#xff0c;特别是 ChatGPT&#xff0c;提示已成为人工智能世界中越来越重要的技能。制作提示&#xff0c;与大型语言模型&…

SIP业务之BLF

BLF&#xff08;Busy Lamp Field&#xff09;是SIP应用中的一项重要业务&#xff0c;用来监视目标号码的状态&#xff0c;常用于调度、坐席监控等场景。 一、 BLF原理 BLF功能需要IP终端或话机与SIP服务器协同实现的&#xff0c;主要流程如下&#xff1a; IP话机向SIP服务器发…