讲一下 AFL 的插桩原理
插桩的本质:反馈导向的模糊测试
AFL 的插桩(instrumentation)就是一套用于收集代码覆盖率的探针。通过这些探针,AFL 可以知道某个输入执行了哪些代码路径
它的工作流程是这样的:
- 编译时插桩:用 AFL 提供的特殊编译器(
afl-clang-fast
或afl-gcc
)来编译目标程序 - 运行时反馈:当程序执行时,插桩代码会向 AFL 反馈代码执行路径信息
- 智能变异:AFL 根据这些反馈,判断哪些输入“有价值”(即探索了新的代码路径),然后对这些有价值的输入进行更多的变异
这个反馈循环是 AFL 高效的关键。它使得 AFL 能够自动绕过复杂的输入校验,深入到程序更深层次的逻辑中,从而找到隐藏的漏洞
插桩的原理实现
AFL 的插桩非常轻量级,它采用了一种基于基本块(Basic Block)的简单而巧妙的方案。
1. 什么是基本块?
在程序中,一个基本块是一段连续的代码,它只有一个入口点(第一条指令)和一个出口点(最后一条指令),且中间没有任何分支跳转
你可以把基本块看作是代码中的最小“执行单元”
2. AFL 的插桩步骤
AFL 在编译时,会在每个基本块的入口插入一段代码。这段代码会做两件事:
- 获取当前基本块的 ID:AFL 在编译时会给每个基本块分配一个唯一的随机 ID
- 记录基本块的 ID:AFL 维护一个共享内存区域,通常是一个大小为 64KB 的位图(bitmap)
当程序执行到一个新的基本块时,插入的代码会执行以下操作:
- 获取当前基本块的 ID(假设是
current_id
) - 获取上一个执行的基本块的 ID(假设是
prev_id
)。AFL 用一个全局变量来保存这个prev_id
- 计算一个哈希值:
index = current_id XOR prev_id
- 将这个
index
映射到位图的某个位置,并将该位置的值加 1 - 更新
prev_id
,使其等于current_id