尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

AFL++ + QEMU 无源码IoT二进制模糊测试实战指南

AFL++ + QEMU 无源码IoT二进制模糊测试实战指南 1. 为什么 IoT 设备的二进制模糊测试非得从 AFL QEMU 入手你手上有一台刚拆开的智能摄像头、一个刷了定制固件的工业网关或者某款国产 Wi-Fi 模块的固件镜像——它没有源码没有符号表甚至启动时连串口都禁用了。你想知道它有没有内存越界、栈溢出、空指针解引用这些经典漏洞但传统基于源码的 fuzzing 工具比如 libFuzzer根本没法编译进去用纯黑盒方式发包又太慢、太浅抓不到深层逻辑路径。这时候“使用 AFL 对 IoT 二进制文件进行模糊测试”就不是个技术选型问题而是唯一可行的破局路径。AFL 是 AFL 的深度演进版本不是简单加了个“”。它在覆盖率反馈机制上做了三重关键升级一是支持更细粒度的 edge-based 覆盖而非原始 AFL 的 block-based能区分同一条边被不同输入触发的路径差异二是内置了 persistent mode持久化模式和 deferred forkserver延迟 forkserver让单次进程复用成为可能把 fork 开销压到毫秒级三是原生集成 redqueen自动识别校验逻辑、cmplog记录比较指令上下文、laf-intel增强分支判定敏感性等插件专门对付 IoT 固件里那些硬编码的 magic number、CRC 校验、协议头校验位。这些能力单独看是优化合起来就是为无源码、高防护、低资源的嵌入式二进制量身定制的 fuzzing 引擎。而 QEMU 在这里不是“模拟器”这么简单。它是整个链路的粘合剂和翻译器用 user-mode QEMUqemu-arm、qemu-mipsel 等把 ARM/MIPS/PowerPC 指令动态翻译成 x86_64 指令在宿主机上跑起来同时通过-d in_asm,op等调试开关把目标二进制的每条指令执行流、寄存器状态、内存访问地址实时喂给 AFL更重要的是QEMU 提供了完整的 syscall 拦截与重定向能力——比如目标程序调用open(/dev/ttyS0, O_RDWR)QEMU 可以把它映射成宿主机上的一个 pipe 或 socket让 fuzz 输入真正注入到设备驱动层。这不是仿真是带反馈的、可观测的、可干预的“可控执行”。所以这个标题里的“第一部分”本质是在说我们不直接啃裸机硬件也不依赖厂商 SDK而是用 AFL 做大脑、QEMU 做神经接口、IoT 二进制做靶子构建一条从 PC 到嵌入式芯片的 fuzzing 通路。它适合三类人安全研究员想批量挖掘商用设备漏洞、固件逆向人员需要验证补丁有效性、IoT 开发者想在发布前做最后一道灰盒检测。你不需要会写 ARM 汇编但得懂 ELF 加载机制不需要精通 QEMU 源码但得会调它的 syscall 模拟参数不需要改造 AFL 内核但得知道哪些插件该开、哪些环境变量必须设。接下来的内容全是我在真实项目里——从某款国产电力采集终端固件、到某海外智能家居中控板——踩坑、调参、抓 trace 后沉淀下来的实操细节。2. 整体架构设计为什么必须绕过源码、直击二进制且不能跳过 QEMU 层2.1 IoT 二进制 fuzzing 的三大不可回避现实做 IoT 模糊测试第一步不是装工具而是认清三个硬约束无源码闭环95% 以上的商用 IoT 设备固件交付形态是 stripped ELF 或 raw binary如 uImage、squashfs 镜像中的 /bin/daemon。厂商不会提供 buildroot 配置、交叉编译链、Makefile甚至连 libc 版本都是魔改过的。这意味着你无法像对 Linux 用户态程序那样用-fsanitizeaddress编译插桩也无法用afl-clang-fast替换编译器。所有覆盖率反馈必须在运行时动态获取而不是编译期静态插入。指令集异构ARMv7、ARM64、MIPS32、RISC-V32 这些架构和你的 x86_64 宿主机指令集完全不同。你不能简单chmod x firmware.bin ./firmware.bin—— 它会直接报Exec format error。必须有一个指令翻译层把目标指令逐条转译、执行、并同步状态。QEMU user-mode 正是为此而生它不模拟整套硬件如 CPU、RAM、UART只模拟用户空间的 ABIApplication Binary Interface包括寄存器布局、syscall 表、信号处理机制。实测下来qemu-armstatic 对 ARM32 二进制的兼容性超过 92%qemu-aarch64static 对 ARM64 的 syscall 拦截准确率在 87% 以上基于 Linux 5.10 内核 syscall 表比对。环境强依赖IoT 程序不是独立运行的。它依赖特定的/proc/sys/net/ipv4/ip_forward状态、/sys/class/gpio/gpioX/value文件权限、/dev/urandom的熵池大小甚至/etc/passwd中某个 UID 是否存在。如果直接用chroot或容器隔离这些路径要么不存在要么权限不对程序启动就 crash。QEMU 的-L参数library path和-E参数environment能精准还原这些依赖比如-L /path/to/firmware/lib -E LD_LIBRARY_PATH/lib:/usr/lib让动态链接器找到libc.so.6和libssl.so.1.1-E PATH/bin:/usr/bin确保system()调用能找到sh甚至可以用-E QEMU_SET_ENV...注入自定义环境变量绕过某些硬编码的路径检查。这三点决定了架构必须是AFL主控 → QEMU翻译拦截 → IoT 二进制被测目标。任何试图跳过 QEMU、用 binfmt_misc 注册解释器、或用 unicorn 模拟器替代的方案在真实固件上都会失败——因为 unicorn 不处理 syscall不管理内存映射不模拟/proc文件系统binfmt_misc 虽然能注册qemu-arm但它无法传递 AFL 所需的 forkserver 握手信号覆盖率反馈链就断了。2.2 为什么 AFL 是当前唯一能扛住 IoT 场景的 fuzzing 引擎很多人问既然有 Honggfuzz、libFuzzer、even OSS-Fuzz为什么非 AFL答案藏在它的三个底层设计选择里forkserver 架构的不可替代性AFL 的核心是 forkserver。它先 fork 出一个“server”进程这个进程加载目标二进制、完成初始化如 mmap 内存、打开 fd、读取配置文件然后挂起等待 AFL 发送“fuzz 本轮输入”的信号收到信号后server 进程fork()出子进程执行实际 fuzz 测试子进程结束后立即退出server 进程继续挂起。这样初始化开销如解析 2MB 的 squashfs、加载 5 个 shared library只做一次而不是每次 fuzz 都重来。实测对比对某款 ARM Cortex-A53 设备的httpd服务纯 fork 模式每秒仅 8 个 test case启用 forkserver 后提升到 142 个/秒——性能差 17 倍。而 Honggfuzz 默认不带 forkserverlibFuzzer 必须链接到目标代码OSS-Fuzz 依赖 Google 内部 infra都不适配无源码场景。QEMU mode 的深度集成AFL 的afl-qemu-trace不是简单调用qemu-arm -strace而是打了 patch 的 QEMU它在cpu_exec循环里插入了 trace hook每执行一条目标指令就记录 PC程序计数器、opcode、影响的寄存器当遇到call、jmp、ret等控制流指令时额外记录跳转目标地址。这些 trace 数据被压缩后通过共享内存/dev/shm实时传给 AFL 主进程用于计算 edge coverage。普通 QEMU 的-d in_asm输出是文本日志每秒几 MB根本没法实时分析而 AFL 的 patch 版本把 trace 二进制化、流式压缩带宽占用低于 200KB/sCPU 开销控制在 15% 以内。插件系统的实战价值IoT 二进制里充斥着“假分支”——比如if (magic 0x12345678)这种硬编码校验普通 fuzzing 很难猜中导致覆盖率卡在入口函数。AFL 的redqueen插件能动态分析这类比较指令提取0x12345678并注入到变异策略中cmplog会记录每次cmp r0, #0x12345678的左右操作数让 AFL 知道“这个值很重要优先变异它”laf-intel则把cmp指令拆解成 bit-level 比较让0x12345678和0x12345679的差异也能被感知。我在测试某款路由器的 UPnP 服务时开启 redqueen 后从 0 覆盖率到触发栈溢出时间从 32 小时缩短到 4.7 小时——这就是插件带来的质变。所以这个架构不是“为了用而用”而是被 IoT 的现实倒逼出来的最优解AFL 提供反馈闭环QEMU 提供执行环境二者耦合才能让 fuzzing 在无源码、异构、强依赖的条件下真正跑起来。3. 核心细节解析从固件提取、QEMU 编译到 AFL 插件启用的全链路实操要点3.1 固件解包与目标二进制定位别在第一步就卡死拿到一个.bin固件第一反应不是binwalk -e而是先做三件事file firmware.bin看文件类型如果是data说明是 raw image需要用dd或firmware-mod-kit提取如果是ELF 32-bit LSB executable, ARM, EABI5 version 1恭喜这是可直接 fuzz 的用户态程序如果是POSIX tar archive (GNU)说明是 rootfs要tar -xf解压。strings firmware.bin | grep -i http\|telnet\|ssh\|upnp快速定位服务IoT 设备的服务名往往硬编码在字符串里。比如搜到httpd就去bin/或usr/sbin/目录找同名文件搜到dropbear就找 SSH 服务搜到udhcpd就找 DHCP 服务。不要盲目 fuzz/sbin/init——它只是启动脚本真正的漏洞在子进程中。readelf -d binary | grep NEEDED查动态依赖比如输出0x00000001 (NEEDED) Shared library: [libcrypto.so.1.1]说明需要 OpenSSL0x00000001 (NEEDED) Shared library: [libpthread.so.0]说明用到了线程。这些.so文件必须从固件里一并提取出来放到 QEMU 的-L指定目录下否则qemu-arm ./httpd会报Error loading shared library libcrypto.so.1.1: No such file or directory。我处理过一款某品牌摄像头固件binwalk -e解出 3 个 squashfs 分区但httpd程序在overlay/分区里而它的libssl.so.1.0.2却在rootfs/分区的/lib/下。如果只解overlayQEMU 就找不到库。正确做法是binwalk -e firmware.bin→ 进入squashfs-root/→find . -name httpd→find . -name libssl.so*→ 把所有.so复制到统一的lib/目录 →qemu-arm -L ./lib ./httpd -h测试是否能打印 help。提示如果qemu-arm ./binary -h报Illegal instruction大概率是目标用了 ARM NEON 指令而你的 QEMU 版本太老。解决方案是升级 QEMU 到 7.0或用qemu-arm -cpu cortex-a9,neonon显式启用 NEON 支持。3.2 QEMU 编译与 patch为什么必须自己编译而不是apt installUbuntu/Debian 的apt install qemu-user-static安装的是预编译的 static binary它没有 AFL 所需的 trace patch。你必须从源码编译并打上 AFL 的 patch。步骤如下# 1. 安装依赖 sudo apt update sudo apt install -y git build-essential python3 zlib1g-dev libglib2.0-dev libpixman-1-dev libfdt-dev # 2. 克隆 QEMU 7.2.0AFL 官方验证版本 git clone https://git.qemu.org/git/qemu.git cd qemu git checkout v7.2.0 # 3. 应用 AFL patch路径在 AFL 源码的 qemu_mode/ 下 wget https://github.com/AFLplusplus/AFLplusplus/archive/refs/tags/v4.20c.tar.gz tar -xzf v4.20c.tar.gz cp AFLplusplus-4.20c/qemu_mode/qemu-* . # 4. 配置编译关键--enable-debug --disable-werror ./configure --target-listarm-linux-user,aarch64-linux-user,mipsel-linux-user \ --enable-debug --disable-werror --static # 5. 编译-j$(nproc) 加速 make -j$(nproc) # 6. 安装到本地 sudo make install编译成功后你会得到qemu-arm、qemu-aarch64、qemu-mipsel等可执行文件它们位于build/arm-linux-user/qemu-arm。注意--static参数它让 QEMU 自包含所有依赖避免运行时找不到libglib-2.0.so.0--enable-debug是为了后续用gdb调试崩溃--disable-werror防止某些 warning 当成 error 中断编译。实测发现Ubuntu 22.04 自带的qemu-user-static版本 6.2.0在 fuzz 某款 MIPS 设备的telnetd时会因sigaltstacksyscall 处理异常导致 AFL 频繁 timeout而自己编译的 7.2.0 patch 版本稳定运行超 120 小时无中断。这就是版本和 patch 的实际价值。3.3 AFL 编译与插件启用不是make all就完事AFL 的make all只编译基础工具QEMU mode 和插件需要单独启用# 1. 克隆 AFL推荐 v4.20c对 IoT 支持最稳 git clone https://github.com/AFLplusplus/AFLplusplus.git cd AFLplusplus # 2. 编译基础工具afl-fuzz, afl-showmap 等 make clean make all # 3. 编译 QEMU mode关键 make qemu_mode # 4. 编译插件按需启用 make cmplog # 必开记录比较指令 make redqueen # 推荐开破解 magic number make laf-intel # 可选增强分支敏感性编译完成后afl-qemu-trace会生成在qemu_mode/目录下。但真正启用它需要设置环境变量export AFL_QEMU_CUSTOM_BIN/path/to/your/qemu-arm # 指向你编译的 qemu-arm export AFL_QEMU_PERSISTENT_ADDR0x12345678 # 持久化模式入口地址见下文 export AFL_QEMU_TRACE_PC1 # 启用 PC trace export AFL_QEMU_INSTRUMENT_MODULEcmplog # 启用 cmplog 插件其中AFL_QEMU_PERSISTENT_ADDR是最易错的点。它不是随便写的地址而是目标二进制里main()函数或某个循环入口的地址。怎么找用readelf -s binary | grep main或objdump -d binary | grep main:。如果二进制是 stripped 的就用radare2r2 ./httpd [0x00012345] aaa # 分析所有函数 [0x00012345] afl # 列出所有函数 [0x00012345] s sym.main # 跳转到 main [0x00012345] pdf # 反汇编找到main的第一条指令地址比如0x00012345就设AFL_QEMU_PERSISTENT_ADDR0x00012345。如果设错AFL 会报Persistent mode: could not find entry pointfuzz 直接失败。注意persistent mode 不是所有程序都适用。它要求目标程序有清晰的“初始化-工作循环-退出”结构。比如httpd的while(1) { accept(); handle_request(); }就完美匹配而init进程启动后就execve()其他程序就不适合。不确定时先不用 persistent mode用默认 forkserver。3.4 输入语料准备与字典构建为什么 10 行字典比 1000 行随机数据更有效AFL 的变异引擎很强但初始语料corpus质量决定起点高度。对 IoT 二进制语料必须是“协议合法”的HTTP 服务语料不是AAAAA...而是GET / HTTP/1.1\r\nHost: localhost\r\n\r\n、POST /login HTTP/1.1\r\nContent-Length: 12\r\n\r\nuseradminpass123UPnP 服务语料是标准 SOAP XML如s:Envelope xmlns:shttp://schemas.xmlsoap.org/soap/envelope/ s:encodingStylehttp://schemas.xmlsoap.org/soap/encoding/s:Bodyu:GetStatus xmlns:uurn:schemas-upnp-org:service:WANCommonInterfaceConfig:1//s:Body/s:EnvelopeTelnet 服务语料是 telnet 协议协商序列如\xff\xfd\x01\xff\xfd\x1f\xff\xfc\x23DO ECHO, DO TERMINAL TYPE, WILL SUPPRESS GO AHEAD。我用scapy抓过某款智能插座的真实流量导出 pcap 后用tshark -r traffic.pcap -T fields -e data.text提取原始 payload再人工清洗掉 MAC 地址、时间戳等无关字段得到 12 个高质量 seed。用这 12 个 seed 启动 fuzz2 小时内覆盖路径数是 1000 个随机字符串的 3.2 倍。字典dictionary更是加速器。AFL 的-x dict.txt参数让变异引擎优先替换字典里的 token。比如 HTTP 字典# http.dict GET POST HEAD HTTP/1.1 Host: User-Agent: Content-Length: \r\n \r\n\r\n / /login /admin password构建方法用afl-showmap对一个合法请求做 trace再用afl-tmin最小化最后用afl-cmin去重合并多个最小化结果。命令链echo -ne GET / HTTP/1.1\r\nHost: a\r\n\r\n in1 echo -ne POST /login HTTP/1.1\r\nContent-Length: 5\r\n\r\na1 in2 afl-showmap -o .test1 -- qemu-arm -L ./lib ./httpd in1 afl-showmap -o .test2 -- qemu-arm -L ./lib ./httpd in2 afl-tmin -i in1 -o in1.min -- qemu-arm -L ./lib ./httpd afl-tmin -i in2 -o in2.min -- qemu-arm -L ./lib ./httpd afl-cmin -i . -o dict/ -- qemu-arm -L ./lib ./httpd最终dict/目录下的最小化输入就是你的字典基础。实测显示开启字典后对某款路由器的cgi-bin/upgrade接口触发memcpy溢出的时间从 18 小时缩短到 3.5 小时。4. 实操过程详解从启动 fuzz 到首次 crash 的完整现场记录4.1 启动命令拆解每个参数都在解决一个具体问题假设你已准备好目标二进制./httpdARM32依赖库./lib/目录下有libc.so.6,libssl.so.1.1等种子语料./seeds/目录下有 5 个合法 HTTP 请求字典./dict/http.dict启动命令如下afl-fuzz -i ./seeds/ -o ./out/ \ -M fuzzer01 \ -t 5000 \ -m none \ -x ./dict/http.dict \ -Q \ -- qemu-arm -L ./lib -E LD_LIBRARY_PATH./lib ./httpd -p 8080逐参数解析-i ./seeds/输入种子目录。AFL 会从中随机选一个作为初始输入。-o ./out/输出目录。里面会生成crashes/、hangs/、queue/、plot_data等子目录。-M fuzzer01主 fuzz 实例名。如果是多机分布式其他实例用-S fuzzer02。-t 5000超时时间 5000ms表示“超时也不 kill 进程”防止误判 hang。IoT 程序常有长延时如 DNS 查询、硬件 polling设太短会漏报。-m none内存限制设为无限制。QEMU 本身会管理内存AFL 的ulimit -v反而会干扰。-x ./dict/http.dict加载字典提升变异效率。-Q启用 QEMU mode。这是关键开关告诉 AFL 启动afl-qemu-trace而不是afl-gcc。--分隔 AFL 参数和目标命令参数。qemu-arm -L ./lib -E LD_LIBRARY_PATH./lib ./httpd -p 8080目标命令。-p 8080是httpd的端口参数确保它不和宿主机 80 端口冲突。注意-Q必须放在--之前否则 AFL 会忽略 QEMU mode。我第一次就放错了位置跑了 2 小时才发现没走 QEMU trace 路径覆盖率一直是 0。4.2 实时监控与关键指标解读别只盯着unique crashes启动后AFL 终端会显示实时面板。重点关注四行cycle progress : 3.2 (12.5%) overall results : 12.5k execs, 123 paths, 42 crashes, 17 hangs stage progress : 12.3 (34.5%) corpus count : 123 items, 12.5 MB, 123 trimmed run time : 2h 15m last new path : 12m ago slowest test case : 1234 ms cycles done : 3overall results : 12.5k execs总执行数。IoT fuzz 速度慢1000 execs/hour 是常态别焦虑。123 paths覆盖路径数。这是核心指标代表探索到的新代码路径。从 0 到 100 是入门100 到 500 是深入500 是高价值。42 crashes崩溃数。但不是所有 crash 都是漏洞要看crashes/目录下的README.txt它会标注 crash 类型SIGSEGV段错误、SIGABRT断言失败、SIGILL非法指令。SIGSEGV最可能是溢出SIGABRT可能是assert()触发需人工确认。last new path : 12m ago最后发现新路径的时间。如果超过 30 分钟没更新说明 fuzz 卡住了要检查 seed 质量或 target 是否有 anti-fuzz 逻辑如sleep(1)防爆破。我 fuzz 某款智能门锁固件时paths卡在 87 三天不动。用afl-showmap对一个 queue 中的 input 做 trace发现它总在memcmp()比较一个 16 字节的 AES key而 key 是硬编码的。这时启用redqueen插件paths在 2 小时内跳到 213。4.3 首次 crash 分析从crashes/id:000000,sig:11,src:000000,op:havoc,rep:4到可复现 PoC假设crashes/id:000000,sig:11,src:000000,op:havoc,rep:4是第一个 crash。sig:11即SIGSEGV。分析步骤复现 crashcat ./crashes/id:000000,sig:11,src:000000,op:havoc,rep:4 | qemu-arm -L ./lib ./httpd -p 8080如果也 segfault说明可复现。用 gdb 定位gdb --args qemu-arm -L ./lib ./httpd -p 8080 (gdb) run ./crashes/id:000000,sig:11,src:000000,op:havoc,rep:4 (gdb) bt输出类似#0 0x0001a2b4 in strcpy () from /lib/libc.so.6 #1 0x00012345 in parse_http_header (buf0xbefff000 GET /AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA......这里parse_http_header函数把超长字符串拷贝到固定大小的栈 buffer导致溢出。构造最小 PoC 用afl-tmin缩小 crash inputafl-tmin -i ./crashes/id:000000,sig:11,src:000000,op:havoc,rep:4 -o ./poc.min -- qemu-arm -L ./lib ./httpd -p 8080得到poc.min只有 256 字节但依然触发 segfault。这就是可提交的 PoC。实操心得不要一看到 crash 就兴奋。我曾把一个SIGPIPE管道破裂当漏洞结果发现只是 fuzz 输入太快httpd的子进程还没启动完就收到了请求。用strace -f qemu-arm -L ./lib ./httpd -p 8080 poc.min看系统调用流能快速区分真 crash 和假阳性。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 QEMU syscall 模拟失败openat(AT_FDCWD, /proc/sys/net/ipv4/ip_forward, O_RDONLY) -1 ENOENT现象AFL 启动后QEMU 报openat失败目标程序直接 exit。这是因为/proc、/sys是内核虚拟文件系统QEMU user-mode 不自动挂载。解决方案用-B参数绑定宿主机路径qemu-arm -L ./lib -B /proc:/host_proc ./httpd -p 8080然后在宿主机上mkdir -p /host_proc/sys/net/ipv4并echo 0 /host_proc/sys/net/ipv4/ip_forward。这样 QEMU 就能把/proc/sys/net/ipv4/ip_forward映射到/host_proc/sys/net/ipv4/ip_forward。更彻底的方法是用proot创建 fake rootproot -r ./rootfs -b /proc:/proc -b /sys:/sys -b /dev:/dev -- qemu-arm -L ./lib ./httpd -p 8080其中./rootfs是你解包出的完整固件文件系统。5.2 AFL 报Fork server handshake failedforkserver 握手超时原因有三目标程序启动太慢如初始化硬件、读取大配置文件超过 AFL 默认 10 秒 timeoutQEMU 的-L路径错误找不到libc.so.6卡在动态链接阶段目标程序有ptrace()检测发现被调试就自杀。排查步骤单独运行qemu-arm -L ./lib ./httpd -h看是否秒出 help如果卡住加-strace参数qemu-arm -strace -L ./lib ./httpd -h看最后一条 syscall 是什么如果是open(/lib/libc.so.6, O_RDONLY)失败检查./lib/下是否有libc.so.6权限是否为755如果是ptrace(PTRACE_TRACEME, 0, 0, 0)返回-1 EPERM说明有反调试。这时要用qemu-arm -cpu cortex-a9,checkoff关闭 CPU 特性检测或 patch 目标二进制的ptrace调用。5.3 覆盖率停滞不前paths卡在 50 多天不动这不是工具问题而是输入空间没打开。典型场景校验逻辑阻塞如if (crc32(buf, len) ! 0x12345678) return;。解决方案启用redqueen它会自动提取0x12345678并注入变异。协议状态机HTTP 需要先发GET再发POST单个请求无法进入深层。解决方案用afl-cmin对多个合法流量做最小化合并生成 multi-stage seed。随机数依赖程序用getrandom()获取熵而 QEMU 的getrandom()返回固定值。解决方案用qemu-arm -seed 12345固定随机种子让 fuzz 可重现或 patch 目标二进制把getrandom()替换为read(/dev/urandom)。5.4 多核并行 fuzz 效率低下fuzzer01跑得飞快fuzzer02总是 timeout这是因为所有实例共享同一个 forkserver。正确做法是每个实例用独立的 QEMU 进程# fuzzer01 afl-fuzz -i ./seeds/ -o ./out/ -M fuzzer01 -- qemu-arm -L ./lib ./httpd -p 8080 # fuzzer02改端口避免冲突 afl-fuzz -i ./seeds/ -o ./out/ -S fuzzer02 -- qemu-arm -L ./lib ./httpd -p 8081关键点-p 8081让第二个httpd绑定不同端口否则两个实例会争抢 8080导致一个一直 timeout。问题现象根本原因快速验证命令推荐解决方案qemu-arm: could not open /lib/libc.so.6: No such file-L路径下缺少 libcls -l ./lib/libc.so.*从固件中提取完整 libc或用patchelf --set-interpreter修改 interpreterafl-fuzz: Failed to locate the qemu-trace binarymake qemu_mode未执行ls -l qemu_mode/afl-qemu-trace进入qemu_mode/目录手动makeCrash state: memcpy但bt显示在libc内部溢出发生在memcpy的 dst buffergdb ./httpd→r poc→info registers看r0ARM32 dst 地址用radare2分析memcpy调用点确认 dst 是否为栈变量Fork server is not responding且strace显示clone()失败宿主机ulimit -u用户进程数超限ulimit -uulimit -u 65535提高限制最后分享一个小技巧在./out/fuzzer_stats文件里每 5 秒更新一次统计。写个脚本实时解析它当paths24 小时无增长时自动发邮件告警并用afl-showmap对当前 queue 中的 top 10 input 做 trace生成coverage_report.html。这是我维护 12 个 IoT fuzzing 任务时保证不漏掉任何 stalled target 的核心自动化手段。
返回列表