字符串交叉引用原理
字符串在固件里到底是怎么存的?交叉引用又是怎么形成的?
这两个问题,是我做固件逆向时最先搞明白的事。说实话,刚开始我也踩过坑——对着IDA里一堆地址发呆,不知道哪个字符串被谁用了。后来慢慢摸清了门道,才发现原理其实不复杂。
字符串在二进制文件中的存储方式
固件里的字符串,存储方式就那么几种。我归纳了一下,常见的有三类:
| 存储方式 | 特点 | 典型场景 |
|---|---|---|
| 连续静态区 | 所有字符串紧挨着放,末尾'\0'分隔 | RTOS固件、Bootloader |
| 表结构索引 | 字符串指针数组 + 字符串池 | UEFI、Linux内核 |
| 压缩/编码存储 | UTF-8、UTF-16甚至自定义编码 | IoT设备、打印机固件 |
我最常遇到的是第一种。你看下面这个例子,就是典型的连续静态区:
// 固件二进制中的原始字节(十六进制)
0x00010000: 48 65 6C 6C 6F 00 57 61 72 6E 69 6E 67 3A 20 00
0x00010010: 45 72 72 6F 72 5F 43 6F 64 65 3A 20 25 64 00 00
// 对应的字符串
0x00010000: "Hello\0"
0x00010006: "Warning: \0"
0x00010010: "Error_Code: %d\0"
嗯,这里要注意——字符串末尾的'\0'不是摆设。我在分析一个路由器固件时,就因为忽略了空终止符,把两个字符串当成一个读了,结果状态机解析全乱套。
交叉引用的形成机制
交叉引用说白了就是:代码里哪个地方用到了这个字符串。
形成机制其实就两种:
- 直接引用:代码里直接硬编码了字符串的地址。比如ARM的LDR指令,直接把地址加载到寄存器。
- 间接引用:通过全局指针表或者偏移量计算得到地址。UEFI的字符串表就是典型。
我画了张图,帮你理清这个关系:
你想想看,如果代码里直接写了地址0x10000,那逆向工具就能轻松建立交叉引用。但如果是通过指针表间接访问,工具就得先解析指针表,才能找到谁用了哪个字符串。
静态分析与动态分析的区别
这个问题,我当年面试时被问过。现在想想,其实就一句话:静态分析是猜,动态分析是看。
| 对比维度 | 静态分析 | 动态分析 |
|---|---|---|
| 原理 | 扫描二进制,找字符串引用点 | 运行时监控,看实际访问了哪些地址 |
| 优点 | 覆盖全,一次扫描所有代码路径 | 结果准确,没有假阳性 |
| 缺点 | 遇到间接跳转、动态计算地址就抓瞎 | 只能覆盖执行到的路径 |
| 工具 | IDA Pro、Ghidra、radare2 | QEMU、JTAG调试器、Frida |
| 适用场景 | 固件整体分析、漏洞挖掘 | 协议逆向、状态机验证 |
我举个例子你就明白了。有一次我分析一个打印机固件,里面有个字符串"AdminPassword"。静态分析时,IDA告诉我这个字符串只有一处引用。但我用QEMU跑起来,输入错误密码后,发现实际上有三个地方都用了它——只是另外两处是通过函数指针间接调用的,静态分析没抓到。
静态分析怎么找交叉引用?说白了就是扫描代码段,看哪些指令的立即数或者偏移量指向了字符串区。比如ARM的LDR Rd, =label,或者x86的LEA指令。工具会把这些指令标记为"引用点"。
动态分析就简单粗暴了——直接在字符串地址上下硬件断点,谁访问了它,一抓一个准。我在用JTAG调试时经常这么干,效果立竿见影。
核心要点:
- 字符串存储方式决定了交叉引用的复杂度
- 直接引用好处理,间接引用需要额外解析
- 静态分析覆盖面广,但可能漏掉动态生成的引用
- 动态分析准确,但只能覆盖执行路径
- 实际项目中,我建议两种方法配合使用
嗯,这一章的内容就这些。字符串交叉引用看着简单,但它是后续状态机恢复的基础。你把这个搞明白了,后面分析固件里的状态流转就会顺畅很多。