ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从CTF实战解析Modbus协议:Wireshark与Python脚本攻防技巧

2026/8/2 11:53:29 拓冰建站 浏览量
从CTF实战解析Modbus协议:Wireshark与Python脚本攻防技巧 1. 项目概述一次关于工业协议与安全攻防的深度对话如果你玩过CTFCapture The Flag比赛特别是偏向网络流量分析或工控安全的题目那么“Modbus协议”这个名字你一定不陌生。它不像HTTP、TCP那样家喻户晓但在工业控制领域它就像普通话一样通用。最近在CTFHub上一道围绕Modbus协议流量分析的题目引起了我的注意它要求我们从一份抓取的网络流量包中找到隐藏的Flag。这道题的精妙之处在于它不仅仅考察你对协议格式的“死记硬背”更考验你如何将协议知识转化为实际的数据提取和逆向分析能力。题目本身提供了两种截然不同的解题思路一种依赖专业的协议分析工具进行“可视化”操作另一种则通过编写Python脚本进行“自动化”解析后者更能体现一名安全研究员的工程化思维。今天我就来详细拆解这道题不仅带你复现解题过程更会深入探讨Modbus协议本身以及在这两种解法背后我们所需要掌握的核心技能和思维逻辑。无论你是刚接触工控安全的新手还是想精进流量分析技巧的老兵相信这篇从实战出发的总结都能给你带来启发。2. Modbus协议核心机制与流量特征解析在动手分析流量包之前我们必须先理解我们面对的是什么。Modbus是一种应用层报文传输协议位于TCP/IP协议栈之上。它采用经典的“请求-响应”模型结构异常简单清晰这也是其能在工业环境盛行数十年的原因。2.1 协议数据单元PDU与封装Modbus协议的核心是协议数据单元PDU它由两部分构成功能码Function Code和数据域Data Field。功能码1字节指示服务器执行何种操作。例如0x01代表读取线圈状态0x03代表读取保持寄存器0x10代表预置多个寄存器写操作。在CTF题目中涉及数据读取如0x03和写入如0x10、0x06的功能码是重中之重。数据域N字节其格式完全由功能码决定。对于读请求通常包含起始地址和寄存器数量对于读响应则包含字节计数和实际的寄存器值。当Modbus运行在TCP/IP网络上时即Modbus TCPPDU前面会加上一个7字节的MBAP头Modbus Application Protocol Header从而构成完整的ADUApplication Data Unit。MBAP头包含了事务标识符、协议标识符固定为0x0000代表Modbus、长度字段和单元标识符常作为从站地址。因此我们在Wireshark中看到一个Modbus TCP数据包其应用层数据就是MBAP头7字节 PDU功能码数据域。2.2 题目流量中的关键线索回到CTFHub的这道题我们拿到的流量包通常是.pcap或.pcapng格式里必然充斥着Modbus TCP的通信数据。解题的第一步永远是全局观察。用Wireshark打开后直接在过滤栏输入modbus进行筛选。这时你会看到一系列连续的请求和响应。我们需要重点关注以下几点异常的功能码正常的工控读写操作有其规律。如果出现大量非常规的、尤其是写操作如0x06写单个寄存器0x10写多个寄存器这很可能就是出题人隐藏数据的方式。题目常将Flag的ASCII码或经过简单编码后的值写入到某个或某几个寄存器中。响应数据中的异常值对于读请求的响应数据域中返回的寄存器值每个寄存器2字节是核心分析目标。Flag可能被直接转换成十六进制数值存放在这里也可能需要你将多个寄存器的值拼接、转换后才能得到。通信的节奏与模式有时Flag并非存在于单一数据包。它可能被分割成多个部分通过多次写操作存入不同地址的寄存器或者需要通过组合多次读操作的响应才能拼凑完整。观察通信的先后顺序和地址变化至关重要。注意在真实的工控渗透测试中随意使用写功能码如0x060x10是极度危险的行为可能导致设备状态改变甚至引发安全事故。在CTF环境中我们是在一个完全受控的模拟场景下进行分析学习。3. 解法一基于Wireshark的手动分析与数据提取这是最直观、最“经典”的解法适合快速上手和理解数据流。我们完全依靠Wireshark这个瑞士军刀来完成。3.1 流量过滤与会话跟踪打开流量包后首先应用过滤器modbus。为了更清晰地看到对话我们可以右键任意一个Modbus数据包选择追踪流-TCP流。Wireshark会弹出一个新窗口以ASCII或十六进制形式展示这个TCP连接中的所有数据。对于Modbus十六进制视图更合适。在原始十六进制数据中你需要能辨认出Modbus TCP的帧结构。通常一个请求帧看起来像这样示例0000 0000 0006 01 03 0010 0001我们来拆解一下0000: 事务标识符不重要。0000: 协议标识符固定。0006: 长度字段表示其后还有6个字节。01: 单元标识符从站地址。03: 功能码0x03读取保持寄存器。0010: 起始地址十六进制代表寄存器地址16。0001: 寄存器数量读取1个寄存器。对应的响应帧可能为0000 0000 0005 01 03 02 1234前面的MBAP头类似。03: 功能码。02: 字节计数表示后面有2个字节的数据。1234: 读取到的寄存器值2字节十六进制为0x1234。解题实操你需要像侦探一样滚动查看整个TCP流。寻找那些“写寄存器”功能码0x06或0x10的请求。找到后重点看请求帧中“数据域”部分这里包含了要写入的值。或者寻找“读寄存器”功能码0x03或0x04的响应重点看响应帧中“数据域”部分返回的值。3.2 数据转换与Flag拼接假设我们在某个写请求0x06中发现要写入的寄存器地址是0x0001值为0x666c十六进制。这看起来像ASCII码。我们将0x66和0x6c分别转换成十进制再查ASCII表0x66是‘f’0x6c是‘l’。这很可能就是Flag的前两个字母“fl”。接下来你需要在整个会话流中按顺序找出所有类似的写操作或读响应提取出每个寄存器值2字节将其拆分为两个单字节分别转换为ASCII字符。如果寄存器值直接就是可读字符的编码拼接起来就能得到Flag。有时数据可能不是直接的ASCII。可能是Base64编码后的字符串被分段写入也可能是Flag的十六进制表示。这就需要你根据上下文和CTF题目的常见套路进行尝试比如将提取出的所有十六进制字节串联起来然后尝试解码为字符串。实操心得Wireshark的“导出分组字节流”功能非常好用。当你锁定了一个包含关键数据的TCP流后可以在“追踪TCP流”窗口将显示格式改为“原始数据”然后点击“另存为…”将整个流的原始字节保存为一个文件。之后可以用xxd、hexdump或Python脚本进行更灵活的分析这比在Wireshark界面里手动抄录要高效准确得多。4. 解法二基于Python Scapy的自动化脚本解析手动分析对于学习和小型流量包是有效的但不够优雅也容易出错。作为一名安全研究员编写脚本自动化处理才是王道。这里我们使用Python的Scapy库它是一个强大的交互式数据包处理程序可以轻松地对pcap文件进行解析、构造和发送。4.1 环境准备与Scapy基础首先确保安装了Scapypip install scapy。Scapy内置了对Modbus协议的基本支持但为了更精细地解析我们可能需要直接操作原始负载。核心思路是使用rdpcap()函数读取pcap文件。遍历所有数据包筛选出Modbus TCP协议的数据包通常基于TCP端口502以及应用层数据特征。从数据包中提取出原始应用层数据即MBAP头PDU。按照Modbus协议格式手动解析这些字节流提取出我们关心的功能码、地址、数据。4.2 脚本编写与关键代码解析下面是一个解构该题目的Python脚本框架包含了两种常见的Flag隐藏方式从写操作请求中提取、从读操作响应中提取的解析逻辑from scapy.all import * import struct def parse_modbus_packet(pkt): 解析单个数据包中的Modbus TCP数据 if pkt.haslayer(TCP) and pkt[TCP].dport 502 or pkt[TCP].sport 502: # 提取TCP负载即Modbus ADU tcp_payload bytes(pkt[TCP].payload) if len(tcp_payload) 8: # MBAP头至少7字节加1字节功能码 return None # 解析MBAP头事务ID协议ID长度单元ID trans_id, proto_id, length, unit_id struct.unpack(HHHB, tcp_payload[:7]) # Modbus PDU起始于第8字节 pdu tcp_payload[7:7length-1] # length字段包括单元ID所以PDU长度为length-1 if not pdu: return None func_code pdu[0] data pdu[1:] # 返回一个结构化的字典便于后续处理 return { src_ip: pkt[IP].src, dst_ip: pkt[IP].dst, trans_id: trans_id, unit_id: unit_id, func_code: func_code, data: data, raw: tcp_payload } return None def extract_flag_from_write(packets): 从写寄存器请求功能码0x06或0x10中提取Flag flag_parts [] for pkt_info in packets: if pkt_info is None: continue # 0x06: Write Single Register, 0x10: Write Multiple Registers if pkt_info[func_code] in [0x06, 0x10]: data pkt_info[data] # 对于0x06数据格式为地址2字节 值2字节 if pkt_info[func_code] 0x06 and len(data) 4: _, value struct.unpack(HH, data[:4]) # 忽略地址取value # 将16位值拆分为两个8位字节并转换为字符 flag_parts.append(chr((value 8) 0xFF)) flag_parts.append(chr(value 0xFF)) # 对于0x10数据格式更复杂包含地址、数量、字节计数和实际值 # 需要根据具体题目格式调整解析逻辑 # 示例假设数据格式为 地址(2) 数量(2) 字节数(1) 值(N) elif pkt_info[func_code] 0x10 and len(data) 5: # 跳过地址和数量定位到值 byte_count data[4] values_data data[5:5byte_count] # 每两个字节一个寄存器值 for i in range(0, len(values_data), 2): if i1 len(values_data): val (values_data[i] 8) | values_data[i1] flag_parts.append(chr((val 8) 0xFF)) flag_parts.append(chr(val 0xFF)) return .join(flag_parts) def extract_flag_from_read_response(packets): 从读寄存器响应功能码0x03或0x04中提取Flag flag_parts [] for pkt_info in packets: if pkt_info is None: continue # 0x03: Read Holding Registers, 0x04: Read Input Registers if pkt_info[func_code] in [0x03, 0x04]: data pkt_info[data] # 读响应格式字节计数1字节 寄存器值N*2字节 if len(data) 1: byte_count data[0] values_data data[1:1byte_count] for i in range(0, len(values_data), 2): if i1 len(values_data): val (values_data[i] 8) | values_data[i1] # 同样将16位值拆为两个ASCII字符 flag_parts.append(chr((val 8) 0xFF)) flag_parts.append(chr(val 0xFF)) return .join(flag_parts) # 主程序 if __name__ __main__: # 1. 读取pcap文件 packets rdpcap(ctfhub_modbus.pcapng) # 替换为你的文件名 # 2. 解析所有Modbus包 modbus_packets [] for pkt in packets: parsed parse_modbus_packet(pkt) if parsed: modbus_packets.append(parsed) # 可选打印包信息用于调试 # print(fFunc: {hex(parsed[func_code])}, Data: {parsed[data].hex()}) # 3. 尝试两种提取方式 flag_from_write extract_flag_from_write(modbus_packets) print(f从写操作提取的Flag: {flag_from_write}) flag_from_read extract_flag_from_read_response(modbus_packets) print(f从读响应提取的Flag: {flag_from_read}) # 4. 综合判断通常一种方式即可 # 根据输出结果选择正确的Flag脚本逻辑解读parse_modbus_packet函数是核心它负责从原始网络包中剥离TCP/IP头部提取出Modbus ADU并初步解析MBAP头和功能码。extract_flag_from_write和extract_flag_from_read_response函数则针对不同的功能码按照协议规范解析数据域。关键点在于理解寄存器值16位如何与ASCII字符8位对应。常见的出题手法是将一个字符的ASCII码放在寄存器的高8位另一个字符放在低8位或者反之。因此我们需要用位操作(val 8) 0xFF和val 0xFF来拆分。主程序流程清晰读包 - 解析 - 尝试两种提取策略 - 输出结果。注意事项这个脚本是一个通用框架。实际题目中Flag的隐藏方式可能千变万化。例如寄存器值可能不是直接的ASCII而是需要异或、加减某个常数或者需要按特定顺序如先低字节后高字节组合。因此在得到初步提取的字符串后如果看起来是乱码就需要结合CTF经验如观察是否有flag{或CTF{前缀进行进一步的解码或逆向。5. 进阶技巧与深度排查指南掌握了基本解法后我们来看看在实战中可能遇到的“坑”以及如何系统化地排查。5.1 常见问题与排查思路提取出的字符串是乱码或不全检查字节序Modbus协议通常使用大端序Big-Endian即高位字节在前。但在某些特定题目或设备中可能会使用小端序。尝试交换每个寄存器值内部两个字节的顺序或者交换字符拼接的顺序。检查编码Flag可能不是ASCII而是UTF-8、Base64或Hex编码的字符串。尝试将提取出的字节流用bytes.fromhex()、base64.b64decode()等方法解码。检查提取范围你可能遗漏了某些数据包。确认过滤条件是否正确是否包含了所有相关的TCP流可能不止一个连接。在Wireshark中可以尝试过滤tcp.port 502来捕获所有进出502端口的数据。脚本运行报错或解析不出数据确认Scapy版本与加载方式确保Scapy正确安装。有时需要以管理员/root权限运行脚本或者使用scapy.all的导入方式。检查pcap文件路径和权限确保脚本中的文件名正确且Python有权限读取该文件。调试解析函数在parse_modbus_packet函数中增加打印语句输出每个包的TCP端口、负载长度和原始字节确认是否成功捕获到了Modbus数据。有时数据包可能被分片需要重组但CTF题目通常不会这么复杂。Flag格式不符CTF的Flag通常有固定格式如flag{...}、CTF{...}等。如果你提取出的字符串包含类似结构但中间是乱码可能是加密或编码。观察乱码部分是否有规律尝试简单的凯撒密码、单表替换或XOR解密密钥可能藏在流量其他部分如某个固定的寄存器值。5.2 从协议分析到漏洞挖掘的思维延伸解这道题的目的远不止于拿到Flag。它训练了我们几种关键的安全分析能力协议逆向能力即使没有完整的协议文档通过观察流量样本也能推断出字段含义和报文结构。这对于分析私有协议或未知工控协议至关重要。数据关联能力需要将请求和响应配对理解一次操作对系统状态的改变并跟踪数据流。自动化思维面对成百上千个数据包手动分析效率低下且易错。编写脚本不仅能解决当前问题其代码框架稍作修改就能复用于其他类似的Modbus分析任务甚至扩展到其他协议。在真实的工控安全评估中对Modbus协议的分析常常用于发现以下问题未授权访问Modbus TCP通常缺乏身份认证任何能访问502端口的主机都可能读写寄存器。敏感信息泄露通过读寄存器功能码0x030x04可能获取设备状态、工艺参数甚至密码等敏感信息。拒绝服务与状态篡改恶意写寄存器功能码0x060x10可能导致设备停机、工艺参数紊乱引发安全事故。因此这道CTF题目是一个绝佳的缩影它将工控协议的基础知识、网络流量分析的实战技巧和安全研究的自动化思维紧密结合在一起。通过这两种解法的对比与实践你收获的不仅仅是一个Flag更是一套应对类似安全挑战的方法论。下次再遇到Modbus或其他工控协议流量时你就能从容地打开Wireshark或者自信地敲下几行Python代码让数据自己开口说话。