字符串交叉引用原理

字符串在固件里到底是怎么存的?交叉引用又是怎么形成的?

这两个问题,是我做固件逆向时最先搞明白的事。说实话,刚开始我也踩过坑——对着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的字符串表就是典型。

我画了张图,帮你理清这个关系:

字符串交叉引用形成机制 代码区(.text) LDR R0, =0x10000 BL printf ... 字符串区(.rodata) 0x10000: "Hello\0" 0x10006: "Warning: \0" 0x10010: "Error_Code: %d\0" 直接引用 指针表(.data) str_table[0] = 0x10000 str_table[1] = 0x10006 间接引用 LDR R0, =str_table LDR R0, [R0, #4] 指向

你想想看,如果代码里直接写了地址0x10000,那逆向工具就能轻松建立交叉引用。但如果是通过指针表间接访问,工具就得先解析指针表,才能找到谁用了哪个字符串。

我的经验:遇到间接引用时,别急着手动追。先看看指针表的结构是不是固定的,很多时候厂商会用一个结构体数组来管理字符串,找到这个数组的基址,交叉引用就全出来了。

静态分析与动态分析的区别

这个问题,我当年面试时被问过。现在想想,其实就一句话:静态分析是猜,动态分析是看。

对比维度 静态分析 动态分析
原理 扫描二进制,找字符串引用点 运行时监控,看实际访问了哪些地址
优点 覆盖全,一次扫描所有代码路径 结果准确,没有假阳性
缺点 遇到间接跳转、动态计算地址就抓瞎 只能覆盖执行到的路径
工具 IDA Pro、Ghidra、radare2 QEMU、JTAG调试器、Frida
适用场景 固件整体分析、漏洞挖掘 协议逆向、状态机验证

我举个例子你就明白了。有一次我分析一个打印机固件,里面有个字符串"AdminPassword"。静态分析时,IDA告诉我这个字符串只有一处引用。但我用QEMU跑起来,输入错误密码后,发现实际上有三个地方都用了它——只是另外两处是通过函数指针间接调用的,静态分析没抓到。

避坑提醒:我曾经完全依赖静态分析,结果漏掉了一个关键的交叉引用。后来养成了习惯——静态分析先扫一遍,再用动态分析验证。两者结合,才能把字符串的引用关系摸透。

静态分析怎么找交叉引用?说白了就是扫描代码段,看哪些指令的立即数或者偏移量指向了字符串区。比如ARM的LDR Rd, =label,或者x86的LEA指令。工具会把这些指令标记为"引用点"。

动态分析就简单粗暴了——直接在字符串地址上下硬件断点,谁访问了它,一抓一个准。我在用JTAG调试时经常这么干,效果立竿见影。

核心要点:

  • 字符串存储方式决定了交叉引用的复杂度
  • 直接引用好处理,间接引用需要额外解析
  • 静态分析覆盖面广,但可能漏掉动态生成的引用
  • 动态分析准确,但只能覆盖执行路径
  • 实际项目中,我建议两种方法配合使用

嗯,这一章的内容就这些。字符串交叉引用看着简单,但它是后续状态机恢复的基础。你把这个搞明白了,后面分析固件里的状态流转就会顺畅很多。