上周在工作中遇到一个诡异的问题:代码完全没动过,加了一些编译选项之后,编译正常,二进制也能运行,业务流程却失败了。经过一些排查之后,发现是 PIE 与 ld 链接资源文件的机制产生了冲突,很值得在此做一下记录,顺便复习下链接相关的知识。
问题出现
需求是这样子的:给某个内部的 CGI 应用加上一些安全编译选项,然后重新编译二进制。理清楚都有哪些选项需要开启之后,我就屁颠屁颠地去 Makefile
里修改 CFLAGS
了~ 一顿操作猛如虎,然后 make
一发,完美通过!试着跑了下编译出来的二进制,也正常运行,没有 crash 也没有 coredump。正准备美滋滋地交差时,发现这部分业务出现了问题,流程跑不通——哦吼,完蛋!这玩意可是好几年没人动的陈年老代码,估计部门里都没人熟悉。无奈,只能硬着头皮开始查问题……
排查问题
首先,这个模块没有很详尽的日志,这是最棘手的一点。如果有详尽的日志,可能十几分钟就能发现关键问题点。
由于这次新开了栈溢出检测、NX 等机制,第一个怀疑的就是:以前可能已经存在栈溢出等问题,但是问题被隐藏了,这次只是把旧有问题给暴露了出来。于是,我把 coredump 开了起来,跑了一下业务流程——但是,并没有如期的 coredump 出现,第一个思路以失败告终。
既然没有 coredump,而且编译出来的文件可以正常运行,我又怀疑:会不会这个 CGI 程序以前就是靠着 Bug 跑起来的,但是编译选项破坏了之前赖以正常运行的 Bug,导致业务流程失败。比如类似这种情况:之前会将未初始化的变量置 0,现在不再置 0 了,导致出问题。但是这些陈年老代码逻辑非常复杂,涉及的模块也比较机密,问来问去,没问到很熟悉的人;加上模块本身是 C 写的,加大了调试的麻烦程度。因此,我难以通过白盒的形式排查问题,第二个思路暂时难以进行。
在暂时没思路的情况下,我选择二分法逐一排查新增的编译选项。最后锁定在 PIE 相关的选项上,但即便如此,仍然没有头绪:为什么开启地址无关之后,进程也没 crash,却反倒会影响业务流程呢?
于是我决定祭出黑盒分析大杀器:strace
。这下子被我抓到了把柄:业务流程退出前的最后一段逻辑,是在 mmap
一块超大的内存,被系统拒绝了,最后因为「内存不足」而中断业务流程。
至此其实已经足以把两个发现给关联起来了:PIE 导致的链接流程变化进而影响某个长度符号的计算。但我还是功力不足,没有发现两者的关联,只能进一步分析。根据 strace
拿到的关键系统调用路径,结合代码,我来到了 mmap
超大内存的源码处。其逻辑类似于这样子:
extern uint8 _binary_data_start[];
extern uint8 _binary_data_size[];
void handle_input(...) {
char *buf = malloc(&_binary_data_size);
memcpy(buf, &_binary_data_start, &_binary_data_size);
// ...
}
这段代码让我很是疑惑:
- 为什么代码中找不到
_binary_data_start
、_binary_data_size
这两个符号的定义? - 为什么
malloc
的大小居然是取_binary_data_size
符号的地址?
代码看不懂,我决定上大杀器:IDA Pro。我分别拿了一份正常运转的二进制跟一份出问题的二进制,来到了这一段关键的 malloc
逻辑,借助神之 F5,我看到了这样子的反汇编结果:
char *buf = malloc(0x55555555); // 有问题的二进制
char *buf = malloc(0x115); // 没问题的二进制
至此,距离真相就差一步。由于 _binary_data_size
符号被优化掉了,我给其加了 volatile
后重新编译。然后用 readelf
分别看了两个二进制,这才发现核心原因:在正常的二进制下,_binary_data_size
是一个绝对符号,且其地址刚好就在 0x115
上;而在增加 PIE
选项的二进制下,_binary_data_size
被重定位到了 0x55555555
地址上,这导致代码逻辑里拿到的 _binary_data_size
地址变成了一个超大的值,进一步导致业务流程因为「内存不足」而退出。由于代码中对这种场景有进行处理,因此最终效果是业务流程走不通,但也没有 coredump 发生。
解决方案
其实这两个符号是通过 ld
把资源文件直接链接进来时,由 ld
自动创建的。同时带进来的,还有一个 _binary_data_end
符号,标志着资源文件内容的结束。因此,最简单的做法就是,通过 end - start
来获取 size
。另外的可能的方案,或许只能通过自定义 ld
脚本来实现了。但技术方案肯定是越简单越容易维护,最终我选择的也是直接 end - start
来代替 size
这一条简便的解决路径。
参考资料:Why doesn't a linked binary file's _size symbol work correctly?
题图来自 Photo by Alexander Sinn on Unsplash