定义数据包IP头结构体

使用模块 socket & ctypes & struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class IP(Structure):
_fields_ = [
# 头长度, 4位
("ih1", c_ubyte, 4),
# 版本号, 4位. 目前采用的IP协议的版本号,一般的值为0100(IPv4),0110(IPv6)
("version", c_ubyte, 4),
# 服务类型, 8位
("tos", c_ubyte, 8),
# 包总长, 16位
("len", c_ushort, 16),
# 标识符, 16位
("id", c_ushort, 16),
# 标志, 3位
# 片偏移, 13位
("offset", c_ushort, 16),
# 生存时间, 8位. 当IP包经过每一个沿途的路由器的时候,每个沿途的路由器会将IP包的TTL值减少1。
# 如果TTL减少为0,则该IP包会被丢弃
("ttl", c_ubyte, 8),
# 协议, 8位. 1:ICMP 2:IGMP 6:TCP 17:UDP 88:IGRP 89:OSPF
("protocol_num", c_ubyte, 8),
# 头部校验, 16位
("sum", c_ushort, 8),
# 发送地址, 32位
("src", c_uint32, 32),
# 目标地址, 32位
("dst", c_uint32, 32)
]

def __new__(cls, socket_buffer=None):
# 将原始缓冲区中的数据填充到结构中
return cls.from_buffer_copy(socket_buffer)

def __init__(self, socket_buffer=None):
self.protocol_map = {1: "ICMP", 6: "TCP", 17: "UDP"}

# socket.inet_ntoa: 将32位形式的IP地址转换成字符串形式的IP地址,非线程安全
self.src_address = socket.inet_ntoa(struct.pack("@I", self.src))
self.dst_address = socket.inet_ntoa(struct.pack("@I", self.dst))

try:
self.protocol = self.protocol_map[self.protocol_num]
except:
self.protocol = str(self.protocol_num)

new() & init()

  1. new() 静态方法. 实例化开始后,在init()之前调用. 没有重写,会自动调用父类;

  2. new()会返回cls的实例,然后该类的init()方法作为构造方法会接收这个实例(即self)作为自己的第一个参数,然后依次传入new()方法中接收的位置参数和命名参数;

  3. 如果new()没有返回cls(即当前类)的实例,那么当前类的init()方法是不会被调用的;

  4. 如果new()返回其他类(自定义类或系统类均可)的实例,那么只会调用被返回的那个类的构造方法.

模块ctypes

  1. Structures和Unions必须继承Structure和Union基础类,它们都在ctypes模块中定义;

  2. 每一个子类必须定义fields属性,fields是一个二维的tuples列表,包含着每个field的name及type;

  3. field类型必须是一个ctypes类型,如c_int,或者任何其他的继承ctypes的类型,如Structure, Union, Array, 指针等.

模块struct

  1. pack(fmt, v1, v2, …)
    按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流)

  2. unpack(fmt, string)
    按照给定的格式(fmt)解析字节流string,返回解析出来的tuple

  3. calcsize(fmt)
    计算给定的格式(fmt)占用多少字节的内存

获取数据包IP头数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)

#如果在Windows上,发送IOCTL信号到网卡驱动上以启用混杂模式
if os.name == "nt":
sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)

try:
while True:
raw_buffer = sniffer.recvfrom(65565)[0]
ip_header = IP(raw_buffer)

except KeyboardInterrupt:
if os.name == "nt":
sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)

修改代码适应不同平台

  1. 在书中给的例子中,结构体中变量src和dst是c_long类型,但c_ulong在i386是4bytes而在amd64是8bytes, 会运行失败。所以换成了c_uint32, 可以不考虑平台.

  2. 获取头数据时,书中所给代码ip_header = IP(raw_buffer[0:20])会造成运行失败,原因是在i386上头基本数据长度是20字节,而在amd64上是32字节。所有平台都可通过的写法是IP(raw_buffer)。

扩展 -> 内存对齐

基本类型不包括struct/class/uinon

原则:

  1. 基本类型数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding).
    比如: int在32位机为4字节, 则要从4的整数倍地址开始存储。

  2. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部”最宽基本类型成员”的整数倍地址开始存储.
    比如: struct a里存有struct b, b里有char,int ,double等元素,那b应该从8的整数倍开始存储。

  3. 结构体的总大小,也就是sizeof的结果.必须是其内部最大成员的”最宽基本类型成员”的整数倍, 如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。

  4. sizeof(union),以结构里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地址。

参考资料




Published with Hexo and Theme by Kael
X