黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第三章 网络工程-原始套接字与嗅探(2)解码IP包
写在前面
在当前的模式下,我们的嗅探器接收所有IP头,以及任何更高的协议,如TCP、UDP或ICMP。信息以二进制形式打包,如前所示,很难理解。接下来,我们将对数据包的IP部分进行解码,以便从中提取有用的信息,例如协议类型(TCP、UDP或ICMP)以及源/目标IP地址。这将为以后进一步的协议解析奠定基础。 如果要检查网络上实际的数据包的样子,我们应当了解我们需要如何解码传入的数据包。IP头的组成如下图所示。  我们将会解压整个IP头部(不包括可选字段区域),并提取协议类型、源/目的IP地址。这意味着我们将直接跟二进制打交道,并且我们将通过一些措施用python来区分IP头的各个部分。 在Python中,与很多方式获取外部二进制数据到数据结构中。你可以使用ctypes模块或者struct模块来定义数据结构。对于Python来说,ctypes是一个外部函数库,提供了跟基于C的语言的交互,这使我们能够在共享库中使用与C兼容的数据类型和调用函数。另外,struct模块在Python值和C结构之间的转换。总之,ctypes模块处理二进制数据类型,并且提供一些其它的功能;struct模块主要处理二进制数据。本节中我们将会展示如何用他们从网络读取一个IPv4头部。
ctypes模块
下面的代码段定义了一个新的类IP,能够读取一个数据包,并将头部解析成单独分开的域。
from ctypes import *
import socket
import struct
class IP(Structure):
_fields_ = [
("ihl", c_ubyte, 4),
("version", c_ubyte, 4),
("tos", c_ubyte, 8),
("len", c_ushort, 16),
("id", c_ushort, 16),
("offset", c_ushort, 16),
("ttl", c_ubyte, 8),
("protocol_num", c_ubyte, 8),
("sum", c_ushort, 16),
("src", c_uint, 32),
("dst", c_uint, 32),
]
def __new__(cls, socket_buffer=None):
return cls.from_buffer_copy(socket_buffer)
def __init__(self, socket_buffer=None):
self.src_address = socket.inet_ntoa(struct.pack("<L", self.src))
self.dst_address = socket.inet_ntoa(struct.pack("<L", self.dst))
上述IP类中创建了一个名为_fields_的结构,用来描述IP头中的各个部分。结构中使用了在ctypes模块中定义的C类型。例如,c_ubyte类型是一个无符号字符,c_ushort类型是一个无符号短整形。你会发现定义的结构跟上图中描述的IP头的各个部分是对应的。结构中的每一个域包含三个属性:域的名称、取值的类型、以及二进制位的宽度。能够指定位的宽度还是很方便的,因为它提供了指定所需长度的自由,而不仅仅是在字节级别。 IP类继承自ctypes模块中的Structure类,Structure指定在创建任何对象之前必须具有已定义的_fields_结构。为了填充_fields_结构,structure类使用__new__方法,它将类引用为第一个参数,创建并返回类的对象,该对象传递给__init__方法。当我们创建IP对象时,Python调用__new__,它在创建对象之前立即填充_fields_数据结构(当调用__init__方法时)。只要您事先定义了结构,就可以将__new__方法传递给外部网络数据包的数据,这些字段应该神奇地显示为对象的属性。 现在您已经了解了如何将C数据类型映射到IP头值。在转换为Python对象时,使用C代码作为参考可能很有用,因为转换为纯Pythons是无缝的。有关使用此模块的完整详细信息,请参阅类型文档。
Struct模块
Struct模块提供格式字符,可用于指定二进制数据的结构。在下面的示例中,我们将再次定义一个IP类来保存IP头信息。不过,这次我们将使用格式字符来表示IP头的各个部分。
from email import header
import ipaddress
import struct
class IP:
def __init__(self, buff=None):
header = struct.unpack('<BBHHHBBH4s4s', buff)
self.ver = header[0] >> 4
self.ihl = header[0] & 0xF
self.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]
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类型以机器的原始格式和字节顺序表示。在本示例中,我们在Kali(x64)上展示,kali是小字节序(低字节序)。在小字节序机器中,最低有效字节存储在较低地址,最高有效字节存储于最高地址。 接下来的格式字符表示IP头的各个部分。struct模块提供了几个格式字符。对于IP头,我们只需要格式字符B(1字节无符号字符)、H(2字节无符号短整形)和s(需要字节宽度规范的字节数组;4s表示4字节字符串)。注意我们的格式字符串与IP头示意图的结构相匹配。 需要注意的是,用ctypes时,我们需要指定IP头的各个部分的位宽;使用struct时,没有nybble格式字符(一个4bit单元,也被称为nibble,半字节),因此我们必须进行一些操作才能从IP头的第一部分获取ver和hdrlen变量。 在我们接收到的IP头数据的第一个字节中,我们想为ver变量只分配字节高位的nybble(字节中的第一个半字节)。获取字节高位的典型方法是将字节右移四位,这相当于在字节前面加上四个零,导致最后四位脱落。这只剩下原始字节的第一个半字节。Python代码基本上执行以下操作:  我们想给hdrlen变量赋值低位的nybble(半字节),或叫字节的最后四位。获取字节第二个nybble的典型方法是跟0xF(00001111)进行逻辑AND运算。Python代码对字节的处理操作如下所示。  实际上,解析IP头的时候,读者不需要对二进制操作了解太多,但是这里将会看到诸如在探索其他黑客代码时反复使用的shifts和and的方式,这些技术值得了解。 在这种需要移位的情况下,解码二进制数据需要一些努力。但对于许多情况(如读取ICMP消息),设置非常简单:ICMP消息的每个部分都是1字节的整数倍,struct模块提供的格式字符也是1字节的整数倍,因此无需将字节拆分为半字节。在下图所示的ICMP协议的Echo Reply消息中,可以看到ICMP头的每个参数都可以用一个现有格式字母(BBHHH)在结构中定义。  解析此消息的一种快速方法是简单地为前两个属性分配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]
如需获取使用该模块的详细信息,可以阅读struct的文档(https://docs.python.org/3/library/struct.html)。 读者可以使用ctypes模块或struct模块来读取和解析二进制数据。无论采用哪种方法,都将进行如下所示的实例化类:
mypacket = IP(buff)
print(f'{mypacket.src_address} -> {mypacket.dst_address}')
在本示例中,我们使用变量buff中的包数据实例化IP类。
编写IP解码器
接下来我们将IP解码逻辑创建到名为sniffer_IP_header_decode.py的文件中。
import ipaddress
import os
import socket
import struct
import sys
class IP:
def __init__(self, buff=None):
header = struct.unpack('<BBHHHBBH4s4s', buff)
self.ver = header[0] >> 4
self.ihl = header[0] & 0xF
self.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]
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_IP
else:
socket_protocol = socket.IPPROTO_ICMP
sniffer = 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(65535)[0]
ip_header = IP(raw_buffer[0:20])
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.65.141'
sniff(host)
上述代码中,我们首先完成了IP类的定义,这是一个python结构,将接收到的buffer的前20个字节友好的映射到IP头。我们识别的所有字段都与IP头的结构很好地匹配。接下来,我们做了一些内部处理,以生成可读的输出,展示正在使用的协议和连接中涉及的IP地址。我们使用新创建的IP结构,编写逻辑来持续读取数据包并解析其信息。然后读入数据包,传递前20个字节用来初始化IP结构。接下来,简单地打印出我们捕获的信息。下面我们运行一下。
小试牛刀
运行前面的代码,看看我们从发送的原始数据包中提取了什么样的信息。强烈建议在Windows机器上进行此测试,这将能够看到TCP、UDP和ICMP,并允许进行一些非常整洁的测试(比如打开浏览器)。如果读者手边只有Linux,那么执行前面的ping测试以查看它的实际运行情况。我们分别在windows下和linux下运行一下。
在windows下
打开一个命令行窗口,用python运行一下上面编写的脚本。能够得到预期的输出。 
在linux下
在命令终端运行上述脚本(注意使用sudo,否则提示无权限),然后从另一个主机上ping一下linux主机,也可以得到预期的输出,如下图。 
说明
这里有点小问题,原书中的代码,打印出来在Protocol后面跟着的是协议名称,比如ICMP、UDP、TCP(如下图),可是我写的代码打印出来是协议的代号,即上面代码中定义的1、6、17。  偷懒一下,直接用BeyondCompare比对一下看看哪里的问题。找到原因了,init函数中的最后一行代码,应该是在try的except里面执行,而不是在外面,缩进一下,然后重新执行,结果正常了,如下图。  到这里,我们也看到了工具的局限性:我们只看到了ICMP协议的响应,因为我们有意构建用于主机发现的扫描器,这是完全可以接受的。接下来的章节中,我们将使用与解码IP头相同的技术来解码ICMP消息。
|