网络嗅探是网络安全分析的基础手段。本文基于Python原始socket实现网络嗅探器,完成IP报文头的结构解析,详细说明代码逻辑与实现原理,并通过实际抓包效果验证功能。
- 文章涵盖原理讲解、代码实现、效果演示等内容,帮助理解网络数据包的底层结构与嗅探技术的实现方式,适用于网络安全学习与实践参考。
文章目录
前情提要
从本篇开始,我们要着手开始编写一个流量嗅探器:
嗅探工具的主要目标:是发现目标网络里存活的主机。攻击者希望能找出网络里的所有潜在目标,以便有针对性地开展侦察和渗透;
原理讲解
UDP请求的过程:
-
主机存活:当我们向主机发送一个UDP数据包时,如果主机上的UDP端口没有开启,一般会返回一个ICMP包来提示目标端口不可访问。 -
主机不存在:如果主机根本不存在的话,我们应该不会收到任何信息- 前提是:
选中的UDP端口没有被使用,所以未来避免这种情况,我们应该尽可能多的探测多个端口;
- 前提是:
-
UDP的优势:在整个子网里滥发UDP数据包并等待对方回复ICMP消息的开销很小;- 其主要的工作就是解码并分析各种网络协议的数据头
原始socket嗅探器
这里我们先编写一个最简单的socket嗅探器:只能读取一个数据包
python
import os
import socket
# 定义了需要监听的主机 IP 地址
host = '192.168.1.10'
def main():
# 定义所需的参数socket_protocol
if os.name == 'nt':
socket_protocol = socket.IPPROTO_IP
else:
socket_protocol = socket.IPPROTO_ICMP
# 创建socket对象,并绑定端口
sniffer = socket.socket(socket.AF_INET,socket.SOCK_RAW,socket_protocol)
sniffer.bind((host,0))
# 抓包时,包含IP头;
# socket.IP_HDRINCL=1 时,代表包含IP头
sniffer.setsockopt(socket.IPPROTO_IP,socket.IP_HDRINCL,1)
# 打开混杂模式,监听所有数据包(但是该程序只能读取一个)
if os.name == 'nt':
sniffer.ioctl(socket.SIO_RCVALL,socket.RCVALL_ON)
# 读取收到的数据包
print(f"Receive data: {sniffer.recvfrom(65535)}")
# 如果是Windows,记得关闭混杂模式
if os.name == 'nt':
sniffer.ioctl(socket.SIO_RCVALL,socket.RCVALL_OFF)
if __name__ == '__main__':
main()
代码解释
HOST = '192.168.44.142': 定义了需要监听的主机 IP 地址。- 判断操作系统 : 脚本首先检查了
os.name是否为'nt'(代表 Windows 系统)。- 如果是 Windows,则协议设置为
socket.IPPROTO_IP,这意味着它可以嗅探所有的 IP 数据包。 - 如果是 Linux/Unix,则设置为
socket.IPPROTO_ICMP。因为在 Linux 上捕获所有 IP 数据包通常需要更复杂的设置,这里作为基础脚本,后退了一步只去捕获 ICMP(例如 Ping 请求)数据包。
- 如果是 Windows,则协议设置为
(1)创建并绑定原始套接字 (Raw Socket)
sniffer = socket.socket(...):这里创建了一个原始套接字 (SOCK_RAW)- 普通的套接字(如 TCP/UDP)会由操作系统自动处理网络头信息,而原始套接字允许程序直接访问底层的网络数据包。
sniffer.bind((HOST, 0)):将该套接字绑定到之前设定的公开网卡 IP 上。- 端口指定为
0,因为原始套接字关注的是网络层协议,而不是应用层端口。
(2) 配置捕获 IP 头部信息
sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1):IP_HDRINCL(IP Header Include) 标志位设为 1,是在告诉操作系统:"在返回给我的数据中,请包含完整的 IP 数据包头部信息(源IP、目标IP、协议类型等)",而不仅仅是数据内容本身。
(3)开启网卡的混杂模式 (Promiscuous Mode) - 仅限 Windows
sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON):默认情况下,网卡只会接收发往自己 MAC 地址的数据包。- 开启"混杂模式"后,网卡会接收局域网内流经它的所有数据包,不管是不是发给它的。这一步是通过底层的 I/O 控制 (IOCTL) 命令实现的。
(4)读取数据包
recvfrom(65565):调用 recvfrom 方法接收数据。65565 是缓冲区大小,足够容纳目前最大可能的 IP 数据包。
注意:运行此类涉及原始套接字和混杂模式的代码,通常需要管理员权限 (Win) 或 Root 权限 (Linux)。
效果演示
这里我用Windows进行测试:
未开启"管理员权限"执行:

用管理员执行:

这里我们打开另一个cmd窗口,访问baidu.com,成功抓到第一关握手包:
bash
作用:正在向一个 HTTPS 网站发起连接请求(TCP SYN)
↓
访问某个加密网站
↓
发起 TCP三次握手 的第一个包
这里我们成功编写了最简单的一个socket抓包嗅探器;
但是不觉得少了很多东西吗:
- 解码IP头,得到 源 / 目的 地址端口
- 解码ICMP包,判断目标主机是否存活
所以接下来我们需要做的就是学会如何解码 IP头 以及 ICMP包,并将它们打印出来;
解码IP头结构(Python实现)
虽然当前我们的嗅探器可以捕获到TCP、UDP、ICMP等任何高层协议的IP头,但里面的信息是以二进制形式封装的:

(是不是很难读懂?)
所以我们需要讲这些数据转化为人类看得懂的参数,这里需要使用到struct库 来帮助我们解析:
典型IPv4头结构
但在此之前,我们需要了解IP头的基本构成:

这里我们的目的:提取协议类型、源IP地址和目的IP地址等信息;
python
import ipaddress
import struct
class IP:
def __init__(self,buff=None):
# 使用 struct 模块解析前 20 个字节的 IP 头
# '<' 代表小端序,'B'代表1字节无符号整数,'H'代表2字节,'4s'代表4字节字符串
header = struct.unpack('<BBHHHBBH4s4s',buff[0:20])
# 通过位运算提取版本号 (Version) 和头部长度 (IHL)
self.ver = header[0] >> 4
self.h_len = header[0] & 0xF
self.server_type = 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 转换为人类可读格式 (如 192.168.10.1)
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 KeyError:
self.protocol = str(self.protocol_num)
# 接下来的代码,就是之前的原始socket嗅探器的功能了
def sniff(host):
if os.name == 'nt':
socket_protocol = socket.IPPROTO_IP
else:
socket_protocol = socket.IPPROTO_ICMP
# 创建socket对象,并绑定端口
sniffer = socket.socket(socket.AF_INET,socket.SOCK_RAW,socket_protocol)
sniffer.bind((host,0))
# 抓包时,包含IP头;
# socket.IP_HDRINCL=1 时,代表包含IP头
sniffer.setsockopt(socket.IPPROTO_IP,socket.IP_HDRINCL,1)
# 打开混杂模式,监听所有数据包(但是该程序只能读取一个)
if os.name == 'nt':
sniffer.ioctl(socket.SIO_RCVALL,socket.RCVALL_ON)
# 读取收到的数据包
print(f"[*] 正在 {host} 上嗅探流量.... ")
try:
while True:
# 读取一个数据包
raw_buffer = sniffer.recvfrom(65535)[0]
# 解析IP头
ip_header = IP(raw_buffer[0:20])
print(f"Protocol:{ip_header.protocol} | {ip_header.src_address} --> {ip_header.dst_address}")
except KeyboardInterrupt as e:
print(f"[*] 用户中止嗅探....")
# 关闭嗅探模式,如果Windows
if os.name == 'nt':
sniffer.ioctl(socket.SIO_RCVALL,socket.RCVALL_OFF)
sys.exit()
if __name__ == "__main__":
# 这里必须填你运行代码的本机的真实局域网 IP
host_ip = '192.168.1.10'
sniff(host_ip)
代码解释:
SOCK_RAW(原始套接字):- 作用: 原始套接字允许我们直接读取和伪造 IP 报文头(包含源 IP、目的 IP、TTL 存活时间等)
struct.unpack(解码器的核心):- 作用:根据上图,知道 IP 报文头固定是 20 个字节,且规定了第几个字节代表什么(比如前 4 个 bit 代表 IPv4 还是 IPv6)
socket.IP_HDRINCL:一般情况下,操作系统会自动把 IP 报文头部剥离掉;- 标志位设为 1,是在告诉操作系统:"在返回给我的数据中,请包含完整的 IP 数据包头部信息;
1byte(字节) = 8bit
| 代码变量 | 对应 IP 头字段 | 字节位置 |
|---|---|---|
| self.ver / self.h_len | 版本 / IHL | 0 |
| self.server_type | 服务类型 | 1 |
| self.len | 总长度 | 2-3 |
| self.id | 标识 | 4-5 |
| self.offset | 标志 + 段偏移 | 6-7 |
| self.ttl | TTL | 8 |
| self.protocol_num | 协议 | 9 |
| self.sum | 头校验和 | 10-11 |
| self.src | 源 IP | 12-15 |
| self.dst | 目的 IP | 16-19 |
效果演示
这里我们执行代码:

- TCP 流量:你电脑正在和公网服务器建立 / 维持加密网页(HTTPS)连接
- UDP 流量:你电脑正在进行 DNS 查询、音视频 / 游戏等无连接通信
- 双向通信:每一组请求都对应了服务器的响应,符合网络通信的基本原理
当然,如果想看ICMP包的话,直接ping jd.com 即可:

总结
因为我们没有深入地解码数据包的内容,所以这里只能猜测整个数据流的含义。
但整体来说,还是实现了基本功能;下一篇我们就要实现ICMP包解析等功能了;
期待下次再见;