Python初学者咨询:用RAW Socket与struct.unpack解析HTTP头的格式字符串
用Raw Socket提取HTTP头的实操指南(结合struct.unpack)
嘿,作为Python初学者想搞Raw Socket抓HTTP头,这个思路挺有意思的!咱们一步步来理清楚:HTTP是基于TCP传输的,TCP又封装在IP包里,所以得先把IP头部和TCP头部用struct.unpack()解析出来,跳过这些头部后才能拿到TCP负载里的HTTP数据——毕竟HTTP头是文本格式,struct是用来处理IP/TCP那些二进制结构的,不能直接用它解HTTP头哦。
1. 先解析IPv4头部
IPv4头的基础长度是20字节(不带选项的情况),对应的struct格式字符串是!BBHHHBBH4s4s,每个字符的含义我给你拆解下:
!:代表网络字节序(大端序),网络传输的数据都是用大端序,必须加上这个保证解析正确B:1字节的「版本+头部长度」(前4位是IP版本,后4位是头部长度,单位是4字节)B:1字节的服务类型(一般不用管)H:2字节的IP包总长度H:2字节的标识(用于分片重组)H:2字节的标志+片偏移(同样和分片有关)B:1字节的TTL(生存时间)B:1字节的协议号(TCP是6,UDP是17,我们只关心TCP)H:2字节的头部校验和(验证头完整性)4s:4字节的源IP(二进制格式,需要用socket.inet_ntoa()转成字符串IP)4s:4字节的目的IP(同上)
给你一段示例代码:
import struct import socket # 假设raw_socket接收到的数据包存在packet变量里 ip_header = packet[:20] ip_fields = struct.unpack('!BBHHHBBH4s4s', ip_header) # 解析IP版本和头部实际长度 version_ihl = ip_fields[0] ip_version = version_ihl >> 4 # 右移4位取版本(IPv4就是4) ihl = version_ihl & 0xF # 取后4位,乘以4得到IP头总长度(处理带选项的IP包) protocol = ip_fields[6] # 判断是不是TCP包(值为6) src_ip = socket.inet_ntoa(ip_fields[8]) dst_ip = socket.inet_ntoa(ip_fields[9])
2. 再解析TCP头部
TCP头的最小长度是20字节(不带选项),对应的格式字符串是!HHLLBBHHH,含义如下:
!:还是网络字节序H:2字节的源端口H:2字节的目的端口(HTTP常用80,HTTPS是443,但HTTPS加密没法直接解析)L:4字节的序列号L:4字节的确认号B:1字节的「数据偏移+保留位」(前4位是TCP头长度,单位是4字节)B:1字节的标志位(我们关心PSH标志,值为0x08,代表有应用层数据要交付)H:2字节的窗口大小H:2字节的校验和H:2字节的紧急指针
示例代码:
# 跳过IP头,定位到TCP头的起始位置 tcp_header_start = ihl * 4 tcp_header = packet[tcp_header_start:tcp_header_start+20] tcp_fields = struct.unpack('!HHLLBBHHH', tcp_header) src_port = tcp_fields[0] dst_port = tcp_fields[1] # 解析TCP头实际长度(处理带选项的TCP包) data_offset_reserved = tcp_fields[4] tcp_header_length = (data_offset_reserved >> 4) * 4 # 检查PSH标志,确认有应用层数据 flags = tcp_fields[5] has_payload = (flags & 0x08) != 0 # PSH标志为真,说明包里有HTTP数据
3. 提取TCP负载中的HTTP头
HTTP头是纯文本,以\r\n\r\n作为头和体的分隔符,所以我们跳过IP和TCP头后,拿到负载数据解码成字符串,再分割出头部分即可:
# 定位到TCP负载的起始位置 payload_start = tcp_header_start + tcp_header_length payload = packet[payload_start:] # 只处理HTTP端口(80)且有负载的包 if has_payload and (dst_port == 80 or src_port == 80): try: # 把二进制负载解码成UTF-8字符串(HTTP默认用UTF-8) http_data = payload.decode('utf-8') # 找到HTTP头的结束位置 header_end_idx = http_data.find('\r\n\r\n') if header_end_idx != -1: http_header = http_data[:header_end_idx] print("提取到的HTTP头:\n", http_header) except UnicodeDecodeError: # 遇到非UTF-8编码的情况,忽略或尝试其他编码(比如gbk) print("无法解码为UTF-8,跳过该包")
几个关键注意点
- 不是所有TCP包都有HTTP数据:只有PSH标志位被设置的包,才会携带应用层的HTTP请求/响应数据
- HTTPS(443端口)的流量是加密的,没法用这种方式直接解析,初学者先从HTTP(80端口)入手
- IP/TCP头可能带有选项:所以一定要用解析出来的
ihl*4和tcp_header_length来计算头部实际长度,不要硬取20字节 - 解码失败的情况:有些HTTP数据可能用了其他编码,遇到
UnicodeDecodeError可以尝试gbk等编码,或者直接跳过
内容的提问来源于stack exchange,提问作者Akop Akopov
相关产品推荐
相关产品推荐

