第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
注意:Sanitizer会显著增加二进制体积和运行时开销。ASAN大约会拖慢2-3倍,UBSan影响小一些。Fuzz时建议用-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
我的建议:Fuzz测试阶段用静态链接,减少环境依赖。等到漏洞验证或利用开发阶段,再切回动态链接模拟真实环境。

4.4 知识结构总览

下面这张图把本章的核心逻辑串起来了。你可以把它当作一个决策流程图:

异架构Fuzz目标程序编译决策流程 选择目标架构 ARM 32/64位 MIPS 32/64位 开启Sanitizer ASAN + UBSan(ARM全支持) 开启Sanitizer 优先UBSan(ASAN部分支持) 链接方式选择 Fuzz阶段:静态链接 链接方式选择 Fuzz阶段:静态链接 输出:带Sanitizer的静态链接二进制 可直接在QEMU/真机上运行Fuzz 提示:MIPS平台建议先用UBSan验证逻辑正确性,再开启ASAN做深度内存检测

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。

小技巧:如果你在Fuzz过程中发现Sanitizer的报错信息不够详细,可以加上-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参数扩大地址空间。