第4章:目标程序编译——为ARM/MIPS架构编译C/C++测试程序
好,到了真正动手的环节了。前面几章我们聊了Fuzz的基本原理和工具选型,但有个问题一直绕不开——你拿什么去Fuzz?
说白了,你得先有一个能在目标架构上跑起来的程序。我见过不少新手,拿着x86上编译的二进制直接怼到QEMU里,结果跑都跑不起来,更别提Fuzz了。嗯,这一章我们就来解决这个问题。
4.1 交叉编译工具链的选择
要为ARM或MIPS编译程序,你首先得有一套交叉编译工具链。我个人习惯用两种方式:
- 官方工具链:ARM官方提供的
arm-none-eabi-gcc(裸机)或aarch64-linux-gnu-gcc(Linux用户态),MIPS的话用mips-linux-gnu-gcc。 - Buildroot / Yocto:如果你需要定制整个根文件系统,这两个项目能帮你一键生成工具链+库+内核。
不过对于Fuzz测试,我建议直接用Linaro或Mentor Graphics提供的预编译工具链,省时省力。我在项目中踩过坑——自己从源码编译glibc,结果版本不对导致asan跑不起来,折腾了两天。
# ARM 32位
sudo apt install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
# ARM 64位
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
# MIPS 32位
sudo apt install gcc-mips-linux-gnu g++-mips-linux-gnu
4.2 开启Sanitizer:ASAN和UBSan
Fuzz测试的核心目标之一就是找内存错误和未定义行为。Sanitizer就是你的探针。我建议在编译目标程序时,一定要开启ASAN(AddressSanitizer)和UBSan(UndefinedBehaviorSanitizer)。
为什么?你想想看,一个堆溢出在普通运行时可能不崩溃,但ASAN能精准捕获它。我在一次CTF比赛中,就是靠ASAN报出的heap-buffer-overflow定位到了漏洞点,否则光靠crash回溯根本找不到入口。
4.2.1 编译命令示例
# ARM 32位 + ASAN + UBSan
arm-linux-gnueabihf-gcc -fsanitize=address -fsanitize=undefined \
-g -O1 -o test_program_arm test_program.c
# MIPS 32位 + ASAN + UBSan
mips-linux-gnu-gcc -fsanitize=address -fsanitize=undefined \
-g -O1 -o test_program_mips test_program.c
-O1,既保留可读性又不会太慢。
4.2.2 Sanitizer的架构兼容性
| Sanitizer | ARM 32位 | ARM 64位 | MIPS 32位 | MIPS 64位 |
|---|---|---|---|---|
| ASAN | ✅ 支持 | ✅ 支持 | ✅ 支持(需较新GCC) | ⚠️ 部分支持 |
| UBSan | ✅ 支持 | ✅ 支持 | ✅ 支持 | ✅ 支持 |
| LSan | ⚠️ 需手动启用 | ✅ 支持 | ❌ 不支持 | ❌ 不支持 |
| TSan | ❌ 不支持 | ⚠️ 实验性 | ❌ 不支持 | ❌ 不支持 |
看到这个表你可能会问:为什么MIPS的ASAN支持这么差?其实是因为MIPS的地址空间布局和x86/ARM差异较大,ASAN的shadow memory映射机制需要特殊处理。我建议MIPS平台优先用UBSan,它覆盖的未定义行为检查已经能抓到大部分逻辑漏洞了。
4.3 静态链接 vs 动态链接
这是个老生常谈的问题,但在异架构Fuzz场景下,选择会直接影响你的工作效率。
4.3.1 动态链接
默认情况下,交叉编译出来的程序会动态链接目标架构的C库。这意味着:
- 你需要把
libc.so.6、ld-linux.so.3等库文件一起拷贝到模拟环境或真机中。 - 好处是二进制体积小,编译快。
- 坏处是——库版本不匹配时,程序直接报错
FATAL: kernel too old或cannot find libc.so.6。
我曾经在Fuzz一个MIPS路由器固件时,动态链接的libc版本和QEMU模拟的内核不兼容,折腾了一下午才发现是ld加载器版本不对。
4.3.2 静态链接
静态链接把所有依赖都打包进二进制里。对于Fuzz测试,我强烈推荐这种方式:
- 一次编译,到处运行——只要CPU架构匹配,就能跑。
- 避免了库搜索路径、版本冲突等问题。
- 缺点是二进制体积大(尤其开了ASAN后),编译时间也长。
# 静态链接 + ASAN(ARM 32位)
arm-linux-gnueabihf-gcc -fsanitize=address -static \
-g -O1 -o test_program_arm_static test_program.c
# 静态链接 + ASAN(MIPS 32位)
mips-linux-gnu-gcc -fsanitize=address -static \
-g -O1 -o test_program_mips_static test_program.c
4.4 知识结构总览
下面这张图把本章的核心逻辑串起来了。你可以把它当作一个决策流程图:
4.5 实战:一个完整的编译脚本
说了这么多,不如直接给个能用的脚本。这是我个人常用的编译模板:
#!/bin/bash
# cross_compile_fuzz.sh - 为ARM/MIPS编译带Sanitizer的Fuzz目标
ARCH=$1 # arm / aarch64 / mips
SOURCE=$2 # 源文件
OUTPUT=$3 # 输出文件名
case $ARCH in
arm)
CC=arm-linux-gnueabihf-gcc
CXX=arm-linux-gnueabihf-g++
;;
aarch64)
CC=aarch64-linux-gnu-gcc
CXX=aarch64-linux-gnu-g++
;;
mips)
CC=mips-linux-gnu-gcc
CXX=mips-linux-gnu-g++
;;
*)
echo "Unsupported arch: $ARCH"
exit 1
;;
esac
$CC -fsanitize=address -fsanitize=undefined \
-static -g -O1 \
-o "${OUTPUT}_${ARCH}" "$SOURCE"
echo "编译完成: ${OUTPUT}_${ARCH}"
用法很简单:./cross_compile_fuzz.sh arm test.c test_program,就会生成一个test_program_arm的静态链接二进制,自带ASAN+UBSan。
-fsanitize-recover=address让程序在检测到错误后继续运行,而不是直接退出。这样一轮Fuzz能收集到更多crash样本。
4.6 避坑指南
最后,分享几个我踩过的坑:
- 静态链接ASAN在MIPS上可能失败——某些旧版GCC的ASAN静态链接实现有bug,会链接时报错
undefined reference to __asan_report_error。解决方案:升级GCC到8.0以上,或者改用动态链接+手动拷贝libasan.so。 - 不要用-O0——不开优化时,Sanitizer插入的检查代码会极度膨胀,导致二进制体积爆炸(我见过一个hello world编译出50MB)。
-O1是最佳平衡点。 - QEMU用户态模拟时,记得映射Sanitizer的shadow内存——ASAN需要一大块虚拟地址空间做shadow memory。QEMU默认可能不够,启动时加
-R 2G参数扩大地址空间。