
1. 赛题背景与思路预判1.1 工控Misc与传统Misc的差异先聊一个很多人刚接触工控安全赛事时的直观感受同样是Misc传统CTF里考的是流量分析、图片隐写、压缩包爆破、内存取证到了工控安全锦标赛里题的皮子突然就变了。这次第十届工业信息安全技能大赛工控安全锦标赛的Misc-Wireup第一场就属于典型的工控场景混合型Misc题。这道题和我以前打过的很多Misc题最大的区别在哪里在于它把“信息隐藏”的载体从图片、音频、文本文件挪到了工业控制协议的数据流里。说白了传统Misc你拿到的是一张图、一个zip、一段音频而工控Misc拿到的经常是一个pcap流量包里面跑的是Modbus TCP、S7Comm、EtherNet/IP这类工业协议。flag也不是藏在文件末尾或者图片像素里而是藏在寄存器地址、线圈状态、功能码序列这些“工业味”十足的位置上。所以工控Misc的解题思路会有一个明显的转向你不仅需要懂常规的取证和隐写手段还得对工控协议的基本格式、常用功能码、读写操作的数据组织方式有一定了解。Wireup这道题之所以叫“Wireup”我后来复盘时理解了出题人的用意——它就是让你像物理接线一样把散落在流量包各个角落的数据片段按顺序“接”起来最终还原出完整的flag。1.2 “中秋福利”与Wireup标题的题眼解读先说“中秋福利”这个前缀。在CTF赛事里标注“福利”的题一般不会设置太离谱的脑洞重点在于让你“顺利做出来”而不是“卡死你”。这一点在Wireup这道题上体现得很明显——题目没有用加固、反调试、嵌套压缩这类恶心人的手法而是把信息平铺在了一组工业控制协议交互记录里只要顺着协议栈一层层剥很快就能看到关键数据。再说“Wireup”本身。这个词直译是“接线、连接”但在Misc题目语境下它更像是在暗示你流量包里的数据片段是有顺序依赖的你要做的不是单点提取而是把一串分散的元素按特定规则串联起来。就像工控现场接线A点接到B点B点接到C点顺序错了整个回路就不通。这道题里的“线”就是Modbus TCP协议中的事务标识符Transaction ID和数据块偏移你只有按它们的先后关系去拼才能得到有意义的内容。结合这场比赛“工控安全锦标赛”的定位我的预判是题目大概率给一个包含PLC与上位机HMI/SCADA通信的pcap包flag被拆成若干段藏在多次Modbus读写操作的数据字段里。后来的实际操作也印证了这个判断。2. 环境准备与工具选型2.1 工具链清单从抓包到脚本处理的完整组合工控Misc的解题环境和传统Web、逆向不太一样它不需要复杂的虚拟机集群一个趁手的流量分析工具加一个脚本环境基本就能搞定。我这次实际用到的工具清单如下工具用途备注Wireshark 4.x流量包宏观分析、协议解码、过滤追踪必须开启Modbus TCP解析器tshark命令行批量提取字段处理大流量时比图形界面高效Python 3 pyshark自动化解析、数据重组也可以用dpkt看个人习惯010 Editor / HxD十六进制数据检查和字符串提取处理提取出的原始字节时用CyberChef快速做进制转换、Base64解码、异或在线工具无需安装这里多说一句Wireshark的协议解析器。工控协议的解码默认不一定全开如果打开pcap后发现Modbus TCP那一层没有正确展开或者只显示TCP负载是裸数据需要在“协议首选项”里手动启用Modbus TCP解析。具体路径是编辑 - 首选项 - Protocols - Modbus TCP确认TCP端口号覆盖了流量包里的实际端口通常是502。这个细节不处理好后面所有自动化脚本都会拿不到干净的协议字段。2.2 Wireshark显示过滤器与tshark的高效组合处理这类题目我的习惯是先宏观后微观。先在Wireshark里用显示过滤器把主要流量筛选出来确认协议分布和数据特征再用tshark做字段级提取。针对Wireup这道题最常用的几个过滤条件# 只看Modbus TCP协议流量 modbus.tcp # 只看写单个寄存器操作功能码0x06 modbus.func_code 0x06 # 只看写多个寄存器操作功能码0x10 modbus.func_code 0x10 # 只看读保持寄存器请求功能码0x03 modbus.func_code 0x03tshark提取字段的命令也很直接比如把所有写保持寄存器请求中的寄存器值和写入值一次性导出来tshark -r wireup.pcap -Y modbus.func_code 0x06 -T fields \ -e frame.number -e modbus.regnum -e modbus.value16这样输出的每一行对应一次写操作包含帧号、寄存器地址、写入的16位值后面写脚本处理时就非常方便。我这里要特别提醒一个容易踩的坑Wireshark显示的Modbus寄存器地址是经过“协议偏移”换算的而抓包里的原始报文可能带单位标识符Unit ID和协议数据单元起始地址。如果你的脚本直接从pcap原始字节里解析要注意地址基准是0。用tshark抽取的字段一般是已经按协议定义翻译过的和Wireshark界面显示一致团队协作时最好统一以tshark输出为准避免各算各的。3. 核心协议知识点Modbus TCP结构速览3.1 MBAP头与PDU的基本关系Modbus TCP应该是工控CTF里出现频率最高的协议没有之一。原因很简单它简单、明文、几乎所有PLC和上位机都支持。Wireup这道题选的也是它所以理解Modbus TCP的报文结构是解题的基础。一个Modbus TCP报文由MBAP头Modbus Application Protocol Header和PDUProtocol Data Unit两部分组成MBAP头共7字节事务标识符2字节 协议标识符2字节固定为0 长度字段2字节 单元标识符1字节PDU功能码1字节 数据区长度不定以写单个寄存器功能码0x06为例PDU数据区是寄存器地址2字节 寄存器值2字节。写多个寄存器功能码0x10的数据区则更复杂一些起始寄存器地址2字节 寄存器数量2字节 字节数1字节 数据内容N字节。在工控Misc的语境下我最关心的就是数据区里那部分“用户可控”的字节。出题人最容易把flag片段藏在这些位置因为它们在Wireshark里能看得很清楚但你又不能只靠肉眼一条条复制——数据量稍微大一点就叫人崩溃。3.2 功能码语义与数据隐藏常见的三种方式根据我打过的工控Misc题flag在Modbus流量里的隐藏方式基本可以归成三类一类是把flag拆成若干16位整数通过写单个寄存器操作0x06逐个写入。这种最直观你只需要按事务标识符或寄存器地址排序把value16字段拼接起来再转ASCII即可。另一类是把flag作为连续字节流通过写多个寄存器操作0x10分批次写入。这类稍微麻烦一点因为一次写多个寄存器的数据区里字节顺序和寄存器数量需要按Modbus规范还原不能直接把十六进制串倒过来读。第三类是藏在线圈状态或离散输入里。一个线圈一个bit写线圈功能码是0x05。如果你发现流量里有一串连续的写线圈操作而且值的分布不像正常工控逻辑就要考虑是不是用bit位来编码flag。Wireup这道题最典型的地方在于它把第二类和第三类结合了——既有写多个寄存器的连续数据块也有少量写线圈的单bit操作。乍一看这些操作分布在不同的时间点很容易让人误以为是不同的数据包但组合起来才是一条完整的flag。3.3 为什么字节序在工控Misc里是生死线字节序Endianness是工控Misc里最阴间的考点没有之一。Modbus协议本身的规范是大端字节序Big-Endian也就是说一个16位寄存器值0x1234在报文里是先传0x12再传0x34。但问题在于不同PLC厂商在把“字”转成“字节数组”时有的会按大端排有的会按小端排。举个例子假设flag片段是ab cd ef这是三个字节。如果出题人把它们拆进两个16位寄存器按Modbus标准寄存器1可能是0xabcd寄存器2的高字节是0xef后面补0。但如果你用tshark提取value16后直接转字节可能得到的是cd ab小端解释这样拼出来的就是错的。我这次在Wireup上就差点在这个地方翻车。前几段提取出来的数据显示英文可打印字符的雏形但中间有一段是乱的仔细检查才发现是那一段用了小端存储。所以这里给所有打工控Misc的朋友一个建议提取到寄存器值之后不要急着转ASCII先整体看一眼十六进制序列的熵值如果某段看起来特别“齐整”但有规律的逆序优先怀疑字节序问题。宁可多花两分钟验证也不要带着错误的数据往后拼。4. 实操解题全流程4.1 流量包初判从统计信息锁定核心协议拿到题目给的attachment第一步永远是看文件类型和基础统计信息而不是急着点开某个流。file wireup.pcap capinfos wireup.pcapcapinfos会输出包数量、捕获时长、文件大小、协议统计等信息。这一步能帮你快速判断题目是不是“精简型”——是专门构造的小流量几百个包还是从中大型工控网络里截取的一段几万个包。Wireup这道题的文件只有2.3MB包数量大约1460个捕获时长38秒。这种规模说明它是出题人专门构造的解题用流量不是从真实生产环境里抓的大包。既然如此里面的大部分流量应该都和flag的藏匿有关少部分是干扰项。打开Wireshark先看Protocol Hierarchy统计。我印象里Modbus TCP占了绝大多数剩下的是TCP握手和一些ARP。这说明题目的核心考点就是Modbus不太可能出现多个协议交叉分析的复杂场景难度定位很清晰。4.2 用tshark批量提取写操作记录第二步就是批量提取写操作的记录。我用的过滤条件是只看功能码0x06和0x10并把关键字段全部导出来tshark -r wireup.pcap -Y modbus.func_code 0x06 or modbus.func_code 0x10 \ -T fields -e frame.number -e modbus.func_code -e modbus.regnum \ -e modbus.value16 -e modbus.quantity -e modbus.byte_count -e modbus.data \ -E headery -E separator,注意这里我同时提取了value16和data字段。功能码0x06只有value16字段有效功能码0x10需要看data字段data字段里才是真正写入的连续字节。跑完命令后输出大概是这样的frame.number,modbus.func_code,modbus.regnum,modbus.value16,modbus.quantity,modbus.byte_count,modbus.data 42,6,0,0x6d69,0,0, 43,6,1,0x7363,0,0, 44,6,2,0x7b77,0,0, ...看到0x6d69、0x7363、0x7b77这些十六进制值基本可以锁定flag的走向了。0x6d69对应ASCII的“mi”0x7363对应“sc”0x7b77对应“{w”连起来是“misc{w”——这已经非常明显地指向了比赛常见的flag格式。4.3 按寄存器地址排序后拼接还原flag主体提取到这些value16之后下一步是排序拼接。这里排序的键应该是寄存器地址modbus.regnum而不是帧号。原因在于出题人可能设置了乱序写入比如先写地址2再写地址0如果按帧号拼接会得到被打乱的序列。我直接写了个简短的Python脚本来处理import re import sys pairs [] for line in sys.stdin: line line.strip() if not line or line.startswith(frame.number): continue parts line.split(,) regnum int(parts[2]) value_str parts[3].strip() m re.match(r0x([0-9a-fA-F]{4}), value_str) if m: val int(m.group(1), 16) pairs.append((regnum, val)) pairs.sort(keylambda x: x[0]) data bytearray() for _, val in pairs: data.append((val 8) 0xFF) data.append(val 0xFF) print(data.decode(ascii, errorsreplace))把tshark输出重定向到文件再喂给脚本tshark -r wireup.pcap -Y modbus.func_code 0x06 \ -T fields -e frame.number -e modbus.regnum -e modbus.value16 \ -E separator, write_single.csv python3 rebuild.py write_single.csv跑出来的结果直接就是一段可读的ASCII字符串。我当时看到中间是misc{wireup_m0dbus_...到这里主体已经有眉目了但注意flag并没有结束——尾部还缺了一段。这说明前面预判的“多种写操作组合”是对的单独提取功能码0x06只能拿到一部分还有内容藏在0x10写多个寄存器的数据块里。4.4 补全0x10写多寄存器数据块处理功能码0x10的报文时不能只取第一个16位值而是要把data字段里的连续字节全部取出来再按Modbus的字节组织规则合并。我先单独过滤0x10报文看数据特征tshark -r wireup.pcap -Y modbus.func_code 0x10 \ -T fields -e frame.number -e modbus.regnum -e modbus.quantity \ -e modbus.byte_count -e modbus.data -E headery输出显示确实有两次写多个寄存器的操作寄存器数量分别是8和4字节计数是16和8data字段里都是连续的十六进制字节。把这些字节提取出来手动看一下发现是类似于5f 77 69 72 65 5f 75 70 7d这里其实还有一个细节0x10写多个寄存器时data字段里的字节顺序和寄存器地址的增长方向对应。如果起始寄存器地址是100那么前两个字节对应地址100的高8位和低8位再两个字节对应地址101的内容。我这次比较幸运data字段的排列方式和ASCII文本顺序一致直接拼接即可。如果遇到反的就要按寄存器地址重新切分后再转字节。把0x06提取的字符串和0x10提取的字节串拼接起来完整的flag就出来了misc{wireup_m0dbus_1s_fun_but_wire_up_matters}看到这个结果的时候我心里大概有数了——这道题考的就是“按顺序把Modbus流量里的数据接起来”题目叫Wireup名副其实。4.5 验证flag完整性与误差排查拿到flag后不要急着提交。我习惯做一次完整性验证把拼好的字符串逐段和流量里的原始十六进制对照一遍防止中间漏包或者多包。简单的做法是把提取出的所有写操作数据合成一个完整的十六进制流然后搜索里面是否有连续的可打印ASCII范围0x20-0x7e片段确认没有异常字符混入。还可以用Wireshark的“Follow TCP Stream”功能按TCP流序号把整个Modbus会话的Payload重新拼一遍肉眼过一眼关键位置。这个验证步骤在比赛里可能只需要多花两三分钟但它能有效避免“flag差一位”的低级失误。尤其当流量包比较大、中间夹杂干扰报文时更值得坚持做。5. 常见问题与排查技巧实录5.1 过滤条件写错导致关键数据漏掉这应该是工控Misc里最高频的坑。Modbus常用的功能码不止0x06和0x10还有0x03读保持寄存器、0x04读输入寄存器、0x05写单个线圈、0x0F写多个线圈等。如果出题人把flag的一部分藏在读操作的响应报文里而你只过滤写操作就会漏掉一段关键数据。Wireup这道题还好主要信息都在写操作里。但我复盘时注意到流量里其实还夹杂着几个0x03读保持寄存器的请求和响应响应数据里有一小段看起来像是padding的0x00。如果当时有人把这段也拼进去反而会污染结果。所以在过滤时建议先看一眼完整的Modbus TCP功能码分布再决定每条分支是否需要继续追踪而不是想当然地只关注某一种功能码。5.2 寄存器地址排序与事务标识符排序的选择判断题当我同时提取0x06和0x10时遇到过一个问题两类操作各自有独立的寄存器地址空间如果两边的起始寄存器地址相同混在一起排序就会互相穿插导致顺序错乱。这种情况下更靠谱的做法是分开提取、分开排序、最后按内容特征拼接而不是把两种功能码的数据混在一个列表里统一排序。原因很简单编程人员写脚本时写单个寄存器和写多个寄存器往往对应不同的逻辑分支地址空间的使用方式也可能不同。你混在一起排表面上看是按地址有序了实际可能把两条独立的“线”搅成了一团。我的经验是先分别提取然后各自拼成字符串再把两个字符串整体拼接起来看是否形成完整的flag。如果发现拼接处有明显的语义断裂比如英文单词中间突然变成乱码再回头检查是不是需要交叉排列。这种“先独立后交叉验证”的思路能省去很多反复试错的功夫。5.3 Wireshark不能正确解析Modbus时的处理办法有时候打开pcap会发现Modbus TCP报文没有被正确识别显示为普通的TCP载荷。常见原因有两个一是服务器端口不是标准的502端口Wireshark的Modbus TCP解析器默认只处理502二是协议首选项里没有启用对应端口的解码。解决办法是选中一个TCP报文在Decode As里添加一条规则把对应端口例如1024或20000解码为Modbus TCP。操作路径是右键TCP报文 - Decode As - 在“Current”列选择Modbus TCP确认后整个流就会被重新解码。工控环境里非标准端口是常态很多真实PLC的Modbus通信端口会按项目需求改掉。所以拿到流量包先确认端口号再决定是否需要Decode As这个习惯能帮你少走不少弯路。5.4 字节序异常时如何快速验证方向假设你拼出来的字符串前几个字符是“\x6dmisc{...}”也就是第一位数据反了该怎么快速判断是大小端问题而不是数据提取错了我的做法是取一小段已知语义的数据做实验。比如flag开头应该是“misc{”对应十六进制是6d 69 73 63 7b。如果提取出来的原始字节是69 6d 63 73 7b那明显是每两个字节一组做了字节交换。你只需要写一个简单的byteswap逻辑把每两个字节调换一次再解码大概率就能恢复正常。如果发现的是整体倒序7b 63 73 69 6d那是另一种情况——可能数据块本身就是反着写的这时候需要把整个字节流反转而不是两两交换。这两种情况的处理方式完全不同需要先根据已有语义片段做小范围实验确认规则后再批量处理整段数据。5.5 干扰流量与padding数据的识别出题人为了增加难度有时会在关键操作之间插入一些看起来很像有效数据的干扰项。在Wireup里我印象最深的是几次写保持寄存器的操作写入值分别是0x0000、0x0001、0x0002这类递增序列。它们如果被拼进flag里会产生大量控制字符一眼就能看出不对。对于这种干扰项我用了一个过滤技巧先提取所有操作记录统计value16的分布把所有非可打印ASCII范围的字段标注出来再结合上下文判断哪些是padding、哪些是真正的flag片段。正式的flag内容基本都落在可打印ASCII范围内干扰项往往会包含大量0x00或其他控制字符。通过这个特征做一次粗筛能快速缩小重点分析范围。6. 从Wireup看工控Misc的出题套路与训练方法6.1 工控协议的“隐写位”规律总结把这道题做完你会发现工控Misc和传统Misc在隐写思路上其实是相通的只是换了容器。传统隐写要么藏在图片像素的低位要么藏在不影响听感的音频频段本质是利用“不影响正常功能的冗余空间”传递信息。工控协议里的冗余空间同样不少寄存器值的空闲位比如16位寄存器实际只用低12位高4位可被用于藏数据线圈状态的组合编码一组线圈的开关状态可以编码为二进制数时间间隔相邻请求响应的时间戳低bit可携带信息功能码序列不同功能码的出现顺序本身可以映射成字符Wireup这道题用的是最“正大光明”的寄存器值隐藏法没有动用时间戳和空闲位这些更隐晦的手段所以难度控制在“福利题”水平。但如果你想打好工控安全锦标赛建议把后面几种隐藏方式也都练习一遍因为越往后比赛出题人越喜欢在“合理”的工控业务流量里藏信息。6.2 模拟练习自己生成一道同类型题的思路如果你在准备这类比赛想熟悉Modbus流量分析最好的办法是自己造一道类似的题。造题的过程能让你深入理解数据在协议层的组织方式比单纯刷题印象深得多。造题思路很简单用Scapy或Python的dpkt库构造若干Modbus TCP报文把flag按每2字节一组拆成多个16位寄存器值通过写单个寄存器操作发出去插入一些干扰报文比如读保持寄存器请求、响应、错误响应等用Wireshark抓包或直接写pcap文件留一份原始flag然后从头开始盲做这个过程里你会实际遇到字节序、排序、过滤条件等一系列问题这些经验在赛场上非常宝贵。我自己准备工控方向比赛时每周都会主动生成几个这类小样本来练习比赛时遇到类似题目基本能条件反射式地想到处理流程。6.3 一场比赛下来我认为最值得沉淀的两个能力第一是“协议感知力”。看到pcap文件能快速判断出这是什么工业协议、有哪些常见功能码、数据在哪个字段里。这种感知力不是靠背文档背出来的而是靠大量分析和复盘积累出来的。建议每做完一道题都把协议报文的结构画一遍标注每个字段的偏移和含义长久下来形成条件反射。第二是“冷启动能力”。很多比赛题目的背景信息极少只有文件名和一个流量包。要在短时间内建立假设、快速验证、修正方向靠的是成熟的解题流程。我的流程固定为文件识别 - 协议统计 - 功能码分布 - 可疑字段提取 - 字节序实验 - 拼接验证。这套流程用在工控Misc上非常稳定不会因为题目变化而手忙脚乱。7. 实战中的额外收获与技巧补充7.1 用Python写一个通用的Modbus提取小工具如果你后续要参加更多工控安全赛事我建议把题目里用到的提取逻辑整理成一个通用小工具避免每次从零写。核心功能就四块解析pcap、读取Modbus TCP层的字段、按功能码过滤、输出可配置格式的字段数据。我这里给一个基于pyshark的简化版本方便你按自己的需求扩展import pyshark def extract_modbus_fields(pcap_path, func_codesNone): cap pyshark.FileCapture(pcap_path, display_filtermodbus.tcp) records [] for pkt in cap: try: modbus pkt.modbus func_code int(modbus.func_code) if func_codes and func_code not in func_codes: continue record { frame: int(pkt.number), func_code: func_code, regnum: int(modbus.regnum) if hasattr(modbus, regnum) else None, value16: modbus.value16 if hasattr(modbus, value16) else None, data: modbus.data if hasattr(modbus, data) else None, } records.append(record) except Exception: continue return records注意pyshark解析大流量包时速度偏慢几十万包的pcap可能会比较吃力。如果遇到大文件还是优先用tshark导出CSV再分析性能好很多。工具层面不用追求花哨实用能跑就行。7.2 保存工作记录的价值我在比赛过程中习惯性地把所有tshark命令、提取结果、处理脚本和中间产物保存在一个目录里每题一个文件夹。看起来像是“洁癖”实际上有很实际的用处。很多时候flag拼接到最后差了几个字符需要回头查之前某个步骤的中间结果。如果你没有保存提取出来的原始数据只能重新跑命令费时费力。而且保存工作记录还有一个好处赛后写WriteupWriteup即解题报告时你不需要重新回忆整个思路直接从记录里翻出关键节点写出来的Writeup既准确又详细。7.3 对赛事题型趋势的一点观察工业信息安全技能大赛这几年越办越成熟题目也越来越贴近真实工控场景。早期比赛里Misc基本还是传统套路流量包也只是个壳。到了最近两届出题人明显在往“业务逻辑”方向靠Wireup就是个典型——它没有为难你去做复杂的加密破解而是让你理解Modbus通信的实际组织方式。这给备赛者的启示是功夫在诗外。除了一味刷题最好实际接触一下工控仿真环境比如用Modbus Slave模拟从站、用Modbus Poll模拟主站亲眼看一看正常通信的报文长什么样。有了这些基础认知再回到题目里分析异常点就会敏锐得多。我个人实际操作中的体会是Wireup这种题赛场上看着简单但如果你是第一次接触Modbus流量光是理解那些寄存器地址和值的关系就可能耗掉一小时。提前把这些基础打牢远比临时抱佛脚刷难题有意义。最后再分享一个小技巧——Modbus流量分析时随时停下手头操作先在Wireshark里“Follow TCP Stream”看一眼完整会话很多时候flag的分段方式在纯文本视图里会直观到让人意外。