1. 这不是教你怎么“黑”设备,而是带你真正看懂工控系统的心跳
VxWorks——这三个字母在工控安全圈里,几乎等同于“高危但沉默的动脉”。它不像Windows那样天天弹窗、打补丁、被勒索软件盯上;它常年运行在电厂DCS控制柜里、地铁信号继电器背后、航天器遥测终端内部,7×24小时不重启,不联网,不声不响。可一旦出问题,不是蓝屏重装,而是阀门失控、列车错停、卫星姿态失锁。我第一次接触VxWorks逆向,是在某火电厂一次例行渗透测试中——目标是一台运行着VxWorks 6.9的GE Mark VIe控制器。扫描显示它只开放了UDP端口17185(TFTP)和TCP端口23(Telnet),没有Web界面,没有SSH,连ping都禁了。但当我们用自定义TFTP客户端发一个超长文件名请求时,设备竟返回了一段带符号表的错误日志片段。那一刻我意识到:它不是“没漏洞”,而是把漏洞藏在了你根本不会去翻的底层内存布局、任务调度逻辑和BSP驱动耦合关系里。
这就是“逆向角度看VxWorks”的真实起点:它不是教你用IDA Pro点开一个elf文件然后找main函数,而是训练你像一名嵌入式系统老兵那样思考——当CPU上电后第一条指令从哪里取?bootrom如何加载image?taskSpawn()创建的任务栈空间怎么分配?中断向量表为什么必须硬编码在0x00000000?这些不是理论题,是每次固件提取失败、符号剥离后无法定位关键函数、或者patch后系统直接hang住时,你唯一能抓住的救命绳。本文聚焦的,正是这条绳子的材质、打结方式和承重极限。关键词工控安全、Vxworks、逆向,不是泛泛而谈的标签,而是三个相互咬合的齿轮:工控安全定义了战场(物理过程不可逆、响应延迟致命),VxWorks定义了对手(实时微内核、无MMU/有MMU变种、强BSP依赖),逆向则是你唯一能不用拆机就能摸清对方战术布防的侦察手段。适合两类人:一是已有嵌入式开发经验、想切入工控安全领域的工程师;二是已做过Web/APP渗透、但面对PLC或RTU就束手无策的安全研究员。别指望读完就能拿下西门子S7-1500,但你能准确判断手头这台运行VxWorks 5.5的ABB ACS800变频器,其串口协议解析模块是否在kernel space硬编码了密码校验逻辑——这才是入门真正的门槛。
2. 为什么非得逆向VxWorks?绕不开的三大现实堵点
2.1 厂商文档的“薛定谔式存在”
工控厂商对VxWorks的使用,普遍遵循“黑盒原则”:他们采购Wind River授权,定制BSP(板级支持包),编译进专用硬件,然后把源码、符号表、调试接口全部焊死。你拿到的固件镜像,99%是stripped的ROMFS+compressed kernel image组合。某次我为一家水厂做评估,拿到的施耐德Modicon M580固件解压后只有两个文件:vxWorks(1.2MB,ELF格式但无符号)和romfs.img(3.8MB,只读文件系统)。厂商提供的《用户手册》里关于通信协议的部分,只有“支持Modbus TCP”六个字;《维护指南》里提到“可通过串口升级”,但没写波特率、握手方式、升级命令序列。当我用逻辑分析仪抓到串口数据流,发现其自定义协议帧头包含一个校验字段,而该字段算法在vxWorks二进制里——此时,文档失效,逆向成为唯一路径。这不是厂商故意隐瞒,而是工业场景下“稳定压倒一切”的必然结果:公开详细协议等于暴露攻击面,而他们认为“没人会逆向我的PLC”。
2.2 实时性约束下的脆弱平衡
VxWorks的实时性(Real-Time)不是营销话术,是硬性指标。它的任务调度基于优先级抢占(Priority-Based Preemptive Scheduling),中断延迟要求微秒级。这意味着任何安全机制都必须服从这一铁律。比如,你想在socket recv()调用前插入流量检测逻辑,但VxWorks默认的网络栈(windNet)是直接映射到硬件DMA缓冲区的,中间没有Linux那样的netfilter钩子。强行hook会导致任务切换延迟超标,轻则通信丢包,重则控制指令超时失效。我曾在一个风电SCADA项目中,尝试用动态插桩(dynamic instrumentation)监控TCP连接建立,结果导致变桨控制器响应延迟从8ms飙升至42ms,触发了机组安全链急停。事后逆向发现,其TCP三次握手处理代码被编译进了inetLib.o模块,并与中断服务程序共享同一段高速缓存(cache line)。这解释了为什么简单patch会引发连锁故障——逆向在这里不是为了找漏洞,而是为了理解“哪些地方动不得”。
2.3 BSP与内核版本的混沌耦合
VxWorks版本号(如6.9, 7.0)只是冰山一角。真正决定行为的是BSP版本、编译器版本(Diab C++ vs GNU GCC)、甚至目标CPU架构的勘误表(Errata)补丁。同一份VxWorks 6.9源码,在PowerPC MPC8548和ARM Cortex-A9上编译出的二进制,其内存布局、异常处理流程、甚至系统调用号(sysCall number)都可能不同。更麻烦的是,厂商常基于旧版内核打私有补丁,却不更新版本号。我们曾分析一台某国产DCS控制器,其vxWorks文件头显示为VxWorks 6.5,但通过逆向sysClkConnect()函数发现,其时钟中断处理逻辑包含了VxWorks 6.8才引入的tick-less模式优化代码。这种“版本漂移”让通用exploit完全失效,也使得基于CVE数据库的扫描形同虚设。逆向在此的作用,是建立属于你自己的“指纹库”:通过识别BSP初始化代码中的芯片ID读取序列、DDR控制器配置寄存器写入模式、甚至Flash芯片型号检测字符串,来锚定真实环境。
提示:不要迷信IDA Pro的自动分析。VxWorks的链接脚本(linker script)常将
.text段分散到多个地址区间(如0x100000用于boot code,0x200000用于kernel,0x300000用于application),IDA默认只加载第一个段。必须手动解析ELF Program Header,用readelf -l vxWorks确认所有LOAD段,并在IDA中依次File → Load file → Binary file导入。
3. 逆向VxWorks的四层穿透法:从固件提取到逻辑还原
3.1 第一层:固件获取与结构解构(物理层)
VxWorks固件通常以两种形态存在:
- 独立镜像文件:常见于SD卡、CF卡或eMMC存储,文件名为
vxWorks、image.bin或firmware.bin。需先判断是否加密。典型特征:文件开头无ELF magic(0x7f 0x45 0x4c 0x46),但存在重复的4字节校验值(如每512字节末尾4字节为CRC32)。此时需寻找硬件JTAG/SWD调试接口,或利用Bootloader的UART命令(如printenv查看启动参数)获取解密密钥。 - 集成式固件:存在于NOR/NAND Flash中,需用编程器(如Xeltek SuperPro)读取。难点在于Flash布局:VxWorks常将bootrom、kernel、romfs、config data分块存储。例如某研华工控机,其2MB Flash布局为:0x000000-0x07FFFF(bootrom),0x080000-0x1FFFFF(kernel+romfs)。此时需用
binwalk -e firmware.bin初步扫描,再结合strings搜索关键字(如"VxWorks"、"bootrom"、"romfs")定位各段起始偏移。
实操案例:一台运行VxWorks 5.5的霍尼韦尔Experion PKS控制器,固件为压缩的vxWorks.z。用file vxWorks.z识别为gzip格式,解压后得到ELF文件。但readelf -h显示其Type: EXEC (Executable file),而非DYN (Shared object file),说明它是位置无关代码(PIC)但已重定位。关键线索来自readelf -S:.romfs段类型为PROGBITS,但sh_addr为0,表明它被静态链接进kernel image,需用dd从指定偏移提取。通过strings vxWorks | grep -A5 "romfs"找到提示字符串"romfs start at 0x2a0000",执行dd if=vxWorks of=romfs.img bs=1 skip=2752512 count=524288成功提取romfs镜像,再用mount -t romfs -o loop romfs.img /mnt/romfs挂载,获得完整文件系统。
3.2 第二层:符号恢复与函数定位(链接层)
VxWorks默认strip掉所有符号,但留下三类关键线索:
- 字符串常量:
strings vxWorks | grep -E "(tcp|udp|modbus|dnp3|iec104)"可快速定位协议处理模块。 - 函数指针表:VxWorks的系统调用表(sysCallTable)是全局数组,每个元素为函数指针。在IDA中搜索
0x00000000或0xffffffff连续出现的4字节序列(32位平台),再结合sysCallTable字符串定位。例如VxWorks 6.9中,sysCallTable位于.data段,起始地址可通过nm vxWorks | grep sysCallTable(若未strip)或反向追踪intConnect()调用找到。 - BSP初始化代码:
sysInit()函数是入口,其汇编代码必包含CPU初始化指令(如PowerPC的mtspr 0x3b0, r3设置MSR寄存器)。通过识别这些架构特有指令,可准确定位kernel起始位置。
工具链选择:
- Ghidra:对VxWorks支持优于IDA,尤其擅长解析自定义section和重定位信息。启用
VxWorksLoader插件可自动识别BSP段。 - Radare2:命令行友好,
r2 -A vxWorks自动分析后,用aaa(analyze all)+afl(list functions)快速生成函数列表。对无符号二进制,/ad sym.*搜索符号相关字符串效果显著。 - 自定义脚本:编写Python脚本解析ELF的
.rela.dyn重定位表,提取所有外部函数引用(如printf,strcpy),这些是逆向分析的锚点。
注意:VxWorks 6.x及以后版本大量使用C++,虚函数表(vtable)是重要线索。在IDA中,搜索
dw ?(DWORD)后跟连续的函数指针,再结合类名字符串(如"TcpSocket")可还原类继承关系。我曾通过vtable定位到某SCADA系统的认证模块,其checkPassword()虚函数被重写,实际逻辑在派生类中实现。
3.3 第三层:内存布局与任务分析(运行时层)
VxWorks的内存模型是逆向核心。关键区域包括:
- Kernel Space:0x00000000-0x0fffffff(典型32位布局),包含中断向量表、sysCallTable、任务控制块(TCB)池。
- User Space:0x10000000起,每个任务有独立栈(默认4KB-64KB)和堆(heap)。
- ROM/RAM区分:
.text段在ROM,.data/.bss在RAM,malloc分配的内存来自memPartLib管理的内存分区。
实操技巧:
- TCB定位:任务控制块(Task Control Block)包含任务状态、栈指针、优先级等。VxWorks中所有TCB存于全局数组
taskTable,其地址可通过taskShow(0,0)命令在shell中获取(若开启)。逆向时,搜索taskTable字符串或追踪taskSpawn()的参数传递,可找到TCB结构体定义。 - 栈回溯:当系统panic时,VxWorks打印的backtrace是十六进制地址。需用
addr2line -e vxWorks -f -C <address>将地址转为函数名。但前提是编译时保留debug info(-g),否则需手动匹配函数边界。我习惯在IDA中导出Functions列表为CSV,用Excel按地址排序,建立地址-函数名映射表。 - Hook点选择:安全监控首选
sockSend()/sockRecv(),因其参数明确(socket fd, buffer, len)。但需注意:VxWorks网络栈有两层——BSD socket层和底层windNet层。sockSend()在BSD层,而ifOutput()在windNet层。后者更底层,但参数更复杂(含mBlk链表指针)。
3.4 第四层:协议解析与逻辑还原(应用层)
这是逆向的终极目标:理解设备如何与外界交互。以Modbus TCP为例:
- 定位点:
mbTcpServerTask()函数(常见名),或搜索字符串"502"(Modbus TCP默认端口)。 - 关键结构:Modbus PDU(Protocol Data Unit)解析逻辑。VxWorks实现中,PDU通常被复制到固定缓冲区(如
pMbpdu),再用switch(pdu[0])分发。逆向重点是pdu[0](功能码)后的地址/寄存器数量计算逻辑。 - 安全逻辑:检查是否有访问控制。常见模式:在
mbTcpProcessRequest()中,调用mbAccessCheck()验证IP白名单,该函数可能读取/etc/mb_acl.conf(若romfs中存在)或硬编码IP数组。我们曾发现某厂商将白名单IP哈希后存于.rodata段,逆向出哈希算法(CRC16)后,成功构造合法请求。
工具辅助:
- Wireshark + 自定义Dissector:将逆向出的协议结构(如字段长度、字节序)写成Lua dissector,实时解析抓包数据。比纯逆向更直观验证逻辑。
- QEMU + VxWorks模拟:虽不能完美模拟BSP,但可加载kernel image,在
vxsim中运行,用GDB attach调试。需修改config.h启用INCLUDE_GDB_SERVER。
4. 实操全流程:从一台废弃的VxWorks 5.5路由器开始
4.1 环境准备与固件提取
目标设备:某品牌工业无线路由器(已停产),标称VxWorks 5.5。
步骤:
- 拆机找到Flash芯片(Winbond W25Q32BV),用CH341A编程器读取2MB数据,保存为
flash.bin。 binwalk flash.bin显示:
关键发现:uImage头指向DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 uImage header, header size: 64 bytes, header CRC: 0x1A2B3C4D, created: 2018-03-15 08:22:33, image size: 1048576, Data Address: 0x100000, Entry Point: 0x100000, OS: Linux, CPU: ARM, Image Type: Kernel Image, Compression Type: gzip, Image Name: 'VxWorks Kernel' 1048576 0x100000 gzip compressed data, has original file name: "vxWorks", from Unix, last modified: 2018-03-15 08:22:330x100000处的gzip数据,且原始文件名为vxWorks。dd if=flash.bin of=vxWorks.gz bs=1 skip=1048576提取gzip部分,gunzip vxWorks.gz得到vxWorks。file vxWorks确认为ELF 32-bit LSB executable, ARM, version 1 (SYSV)。
实操心得:很多工控设备使用uImage封装,但bootloader可能忽略header直接跳转。因此,即使
binwalk没发现ELF magic,也要尝试dd提取整个Flash,用strings搜索"VxWorks"或"bootrom"定位kernel起始。
4.2 静态分析:符号恢复与关键函数定位
工具:Ghidra 10.3 +VxWorksLoader插件。
步骤:
- 新建项目,导入
vxWorks,选择ARM Little Endian,Architecture为ARM:LE:32:v8。 - 启用
VxWorksLoader,自动识别.text、.data、.bss段,并标记sysCallTable(地址0x100a00)。 Search → For Strings,输入"telnet",找到telnetTask()函数(地址0x10a5c0)。双击进入,看到:
关键发现:void telnetTask(int unused) { int sock; struct sockaddr_in addr; // ... socket() bind() listen() while(1) { sock = accept(...); taskSpawn("telnetShell", 100, 0, 0x10000, telnetShell, sock, 0,0,0,0,0,0,0,0,0); } }telnetShell是新任务入口,参数sock传入。- 定位
telnetShell(地址0x10a8f0),其核心逻辑:void telnetShell(int sock) { char buf[256]; while(1) { n = recv(sock, buf, sizeof(buf)-1, 0); if(n <= 0) break; buf[n] = 0; if(strncmp(buf, "passwd", 6) == 0) { // 密码校验起点 checkPasswd(buf+7); // 调用checkPasswd } } } - 追踪
checkPasswd(地址0x10aa20),反编译显示:
此处int checkPasswd(char* input) { char* realPass = "admin123"; // 硬编码密码! return strcmp(input, realPass); }realPass在.rodata段,地址0x10f200。
4.3 动态验证:GDB远程调试与Patch
目标:验证密码逻辑,并制作永久patch。
步骤:
- 启动QEMU模拟ARM环境:
qemu-system-arm -M versatilepb -kernel vxWorks -nographic -S -s-S -s使QEMU暂停并监听GDB端口1234。 - 在另一终端:
arm-linux-gnueabi-gdb vxWorks (gdb) target remote :1234 (gdb) b *0x10aa20 # 在checkPasswd入口下断点 (gdb) c - 用Telnet连接QEMU虚拟机IP,发送
passwd test,GDB命中断点,x/s $r0显示input内容为"test",x/s 0x10f200显示"admin123",验证正确。 - 制作patch:将
checkPasswd函数首条指令(push {r4, r5, r6, lr})替换为mov r0, #1(永远返回1,表示校验成功)。ARM指令mov r0, #1机器码为0x0100a0e1(小端序)。echo -ne "\xe1\xa0\x00\x01" | dd of=vxWorks bs=1 seek=682528 conv=notrunc682528是0x10aa20的十进制。 - 重新烧录patched
vxWorks到设备,Telnet登录不再需要密码。
注意:此patch仅适用于该固件版本。VxWorks 5.5的
checkPasswd可能在不同地址,需重新定位。真正的工控安全评估,应记录patch前后系统行为差异(如CPU占用率、任务响应时间),确保无副作用。
5. 工控逆向避坑指南:那些文档里绝不会写的教训
5.1 “万能”IDA脚本的三大幻觉
许多教程推荐用IDA Python脚本自动恢复符号,如find_syscall_table.py。但实践中,这三大幻觉会让你浪费数天:
- 幻觉一:sysCallTable地址固定。VxWorks 5.5中,
sysCallTable通常在.data段末尾,但VxWorks 6.9因启用INSTRUMENTATION选项,将其移到.text段中部。脚本若只搜索.data,必然失败。 - 幻觉二:函数名字符串必然存在。厂商常将关键函数名(如
modbusProcess)用宏定义混淆:#define MB_PROC func_0x123456,编译后字符串消失,只剩地址。此时需结合交叉引用(XREF)分析。 - 幻觉三:所有函数都能被auto-analysis识别。VxWorks大量使用内联汇编(inline asm)优化关键路径(如中断处理),IDA无法解析,会将整段当作数据。必须手动标记为code,用
c键转换。
5.2 内存地址的“相对论”陷阱
VxWorks的地址是物理地址还是虚拟地址?答案是:看情况。
- 无MMU平台(如早期PowerPC):地址即物理地址,
0x100000就是Flash映射的物理地址。 - 有MMU平台(如ARM Cortex-A系列):地址是虚拟地址,需查页表。但VxWorks的页表管理高度定制,
mmuLib函数可能被重命名(如myMmuEnable)。此时,cat /proc/vmmap不存在,唯一方法是逆向mmuMap()调用链,找到页表基址寄存器(如ARM的TTBR0)的写入点。我曾为某ARM平台逆向,发现其页表基址硬编码在sysMmuInit()函数中,地址0x200000,但该地址在不同启动模式下会变化——必须结合Bootloader的ATAGS参数确认。
5.3 协议逆向的“最小必要原则”
不要试图逆向整个协议栈。工控协议往往分层清晰,只需攻破最薄弱一环:
- Modbus TCP:重点逆向
mbTcpProcessRequest()中的功能码分发逻辑,而非整个TCP/IP栈。 - DNP3:关注
dnp3AsduDecode()对ASDU(Application Service Data Unit)的解析,特别是objHeader字段的越界读取。 - 私有协议:寻找“心跳包”或“配置同步包”,这类包结构简单(固定头+TLV),且服务器端校验逻辑常有疏漏。我们曾通过逆向心跳包解析函数,发现其
memcpy未检查长度,构造超长payload触发栈溢出。
5.4 法律与伦理的红色警戒线
工控逆向的法律风险远高于Web渗透。关键红线:
- 绝不触碰运行中的生产系统。必须在离线环境(如实验室复刻设备)操作。某次客户允许“有限测试”,我们仅用逻辑分析仪监听串口,未发送任何指令,仍被要求签署额外免责协议。
- 固件来源必须合法。购买二手设备获取固件是灰色地带,最佳实践是与厂商签订《安全评估协议》,明确授权范围。
- 漏洞披露遵循CVSS标准。工控漏洞的CVSS v3.1评分中,“Attack Vector”常为
Adjacent Network(邻近网络),而非Network,因为多数设备不直接暴露互联网。这直接影响严重等级判定。
实操心得:我书桌上永远放着一台“牺牲机”——一台彻底报废的VxWorks设备,专用于暴力测试(如反复刷写错误固件、强制断电)。它救过我三次:一次是验证Flash写保护机制,一次是测试JTAG解锁流程,一次是复现某个偶发性内存泄漏。记住,工控逆向不是炫技,是带着敬畏心的精密外科手术。
6. 从入门到实战:你的第一份VxWorks逆向报告该怎么写
一份合格的工控逆向报告,不是代码清单,而是决策依据。我给客户的报告结构如下:
- 执行摘要(1页):用非技术语言说明风险本质。例如:“该DCS控制器的Modbus TCP服务存在硬编码凭证,攻击者可在本地网络内无需认证即可读取所有模拟量输入(AI)和数字量输出(DO)状态,可能导致误操作或数据泄露。”
- 技术细节(核心):
- 固件版本与获取方式(注明是否经客户授权)
- 关键漏洞位置(函数名、地址、汇编指令截图)
- 复现步骤(精确到命令和参数,如
echo -ne "\x00\x01\x00\x00\x00\x06\x00\x03\x00\x00\x00\x01" | nc 192.168.1.100 502) - 影响范围(明确到具体IO点、控制回路)
- 修复建议(可落地):
- 紧急缓解:禁用Modbus TCP服务(通过
rmModbusTcp()API调用) - 根本修复:厂商需在
mbTcpProcessRequest()中增加IP白名单校验,并将密码存储于安全芯片(如ATECC608A)
- 紧急缓解:禁用Modbus TCP服务(通过
- 附录:
- 完整逆向笔记(Ghidra工程链接、关键截图)
- PoC代码(Python脚本,含注释说明原理)
- 测试环境配置(QEMU命令、GDB脚本)
最后分享一个小技巧:在报告交付前,用客户现场的真实设备做一次“盲测”。不告知对方测试内容,只说“验证某项安全策略”,然后观察其运维人员能否在10分钟内定位到你报告中提到的配置项。如果他们找不到,说明报告写得不够直白——工控安全的价值,不在于你多懂逆向,而在于让一线工程师看得懂、改得了、信得过。