基于AFLFast的fuzz自动化漏洞挖掘(1)
基于AFLFast的fuzz自动化漏洞挖掘(1)
前言:无意间了解到这样的一个课题,这里算是一个前瞻性的基础知识积累,后面也会陆续把涉及到的论文一并讲解了,还有涉及到的代码、环境这些。挺杂的,总之开始吧。
遗传算法(Genetic Algorithm, GA)
最最开始之前,要了解几个基本知识:遗传算法、fuzz、现行传统自动化漏洞挖掘的问题等等。简单补充一个前提性的东西。
算法核心:
顾名思义,遗传算法来自生物进化——遗传、变异、自然选择。遗传算法的目标也很简单:从一群解(个体)中“进化”出最优(或者近似最优)。
说白了,就是怎么去寻找最优解。
核心组成:
这里我们将简单说说遗传算法的5个核心元素:
元素 | 含义 | 示例 |
---|---|---|
个体(Individual) | 一个解,用数组、字符串等形式编码 | [1, 0, 1, 1, 0] |
种群(Population) | 一群个体(解) | 多个 [1,0,...] |
适应度函数(Fitness) | 衡量个体好坏的函数 | 函数最大值、路径最短等 |
选择(Selection) | 按适应度选择“好个体”进入下一代 | 轮盘赌、锦标赛等方法 |
变异 / 交叉(Mutation / Crossover) | 用于产生新个体,增加多样性 | 随机翻转、交叉组合等 |
我的理解是:前两个元素构成了基本种群,而适应度函数类似选择规则,选择实现了种群的迭代,而变异和交叉则为出现新的最优解提供了可能。
基本的GA流程大致如下:
[初始化种群] → [评估适应度] → [选择父代] → [交叉/变异产生子代] → [替换进入新一代] → 循环直到停止
最简单的算法实例:
目标:求最大化 f(x) = x²,x ∈ [0, 31](用 5-bit 二进制编码)
import randomdef decode(individual):return int("".join(str(bit) for bit in individual), 2)def fitness(individual):x = decode(individual)return x ** 2def select(population):population.sort(key=fitness, reverse=True)return population[:2] # 保留适应度最高的两个个体def crossover(parent1, parent2):point = random.randint(1, len(parent1)-2)return parent1[:point] + parent2[point:], parent2[:point] + parent1[point:]def mutate(individual, rate=0.1):return [bit if random.random() > rate else 1-bit for bit in individual]# 初始化
POP_SIZE = 6
GENE_LENGTH = 5
population = [[random.randint(0, 1) for _ in range(GENE_LENGTH)] for _ in range(POP_SIZE)]# 进化
for generation in range(10):population = sorted(population, key=fitness, reverse=True)print(f"第{generation}代 最佳 = {decode(population[0])}, f(x) = {fitness(population[0])}")parents = select(population)offspring = []while len(offspring) < POP_SIZE:p1, p2 = random.choice(parents), random.choice(parents)c1, c2 = crossover(p1, p2)offspring.append(mutate(c1))if len(offspring) < POP_SIZE:offspring.append(mutate(c2))population = offspring
根据我们的分析我们可以很容易得知 f(x) = x²这个函数在给定的区间上是单调递增的,但是如何使用我们的遗传算法实现呢?
结合我们一开始说的基本要素还有流程简单说说:
1、定义种群:
population = [[random.randint(0, 1) for _ in range(GENE_LENGTH)] for _ in range(POP_SIZE)]
我们用基因长度表示每个解,这个种群中有6个个体,每个是一个5位长度的基因组成的。
population = [[0, 1, 0, 1, 1], # → 11[1, 0, 0, 0, 1], # → 17...
]
#这样的数组有五个
2、适应度函数评估:
def decode(individual):return int("".join(str(bit) for bit in individual), 2)def fitness(individual):x = decode(individual)return x ** 2
decode处理五位基因长度实现。fitness完成运算
3、选择:
很显然,在这个任务下我们的选择判断很简单:
def select(population):population.sort(key=fitness, reverse=True)return population[:2]
排序,选择最优秀的两个个体作为父代(精英选择的思想)
4、交叉:
def crossover(parent1, parent2):point = random.randint(1, len(parent1)-2)return parent1[:point] + parent2[point:], parent2[:point] + parent1[point:]
随机选择一个交叉点,将两个父代的基因交换,产生两个子代
5、变异:
def mutate(individual, rate=0.1):return [bit if random.random() > rate else 1-bit for bit in individual]
- 每一位基因有
10%
概率发生变异(翻转 0 ↔ 1); - 模拟生物“突变”行为,增加基因多样性;
- 防止陷入局部最优。
6、迭代与传递:
offspring = []
while len(offspring) < POP_SIZE:p1, p2 = random.choice(parents), random.choice(parents)c1, c2 = crossover(p1, p2)offspring.append(mutate(c1))if len(offspring) < POP_SIZE:offspring.append(mutate(c2))population = offspring
- 从精英父代中随机选两人交叉、变异,生成子代;
- 用这些子代构成新一代种群(取代旧的种群);
- 整个进化进入下一轮。
依次迭代直到满足条件为止。
Fuzzing(模糊测试)
前面我们已经简单说完了遗传算法的事情,现在来说说fuzzing。
什么是fuzzing?
Fuzzing(模糊测试) 是一种自动化测试技术,它的核心思路是:
向目标程序输入大量随机或变异生成的数据,以触发异常行为(如崩溃、挂起、越界等),从而发现漏洞或缺陷。
核心:触发bug/崩溃/安全漏洞,探索更多的代码路径。
基本流程与组成核心:
+----------------+| 初始输入种子 S |+----------------+|[变异器 Mutator]↓+----------------+| 构造输入 I |+----------------+|[执行器 Executor]↓+---------------------+| 运行程序 F(I) |+---------------------+↓+------------------------+| 监控:崩溃?覆盖路径? |+------------------------+
组件 | 作用 |
---|---|
种子(seed) | 起始输入,通常为合法或示例输入 |
变异器(mutator) | 修改已有输入,生成变异输入 |
执行器(executor) | 执行目标程序,收集结果 |
监控器(monitor) | 监控是否崩溃、路径是否新颖 |
调度器(scheduler) | 决定 fuzz 哪个种子、多少次(如 AFLFast 的精髓) |
分类:
类型 | 描述 | 工具示例 |
---|---|---|
黑盒 Fuzzing | 不知道程序内部,仅靠输入和输出 | Radamsa |
灰盒 Fuzzing | 插桩获取程序路径信息(最实用) | AFL, libFuzzer |
白盒 Fuzzing | 使用符号执行等复杂技术 | KLEE, SAGE |
AFL
接下来这里先侧重讲一下AFL,项目源地址放这里:google/AFL: 美国模糊 lop - 一种面向安全的模糊测试器 — google/AFL: american fuzzy lop - a security-oriented fuzzer
AFL 是一款开源的灰盒模糊测试器(fuzzer),由 Michal Zalewski 开发,它的目标是:
自动向程序输入变异数据,
智能记录执行路径,
引导程序触发新路径或崩溃。
+--------------------+
[1] 种子输入集 → | 变异引擎 Mutator |+--------------------+|↓+--------------------+| 执行目标程序 ForkServer |+--------------------+|+--------------------+| 路径覆盖反馈 Feedback |+--------------------+↓[2] 更新种子队列、记录崩溃、添加新路径
核心机制说明:
1、灰盒:插桩记录路径
AFL 会对被测试程序进行插桩编译(用 afl-gcc
或 afl-clang
):
- 编译后,AFL 能记录程序运行时走过的代码路径;
- 它使用一个 共享内存 bitmap(路径覆盖图),记录执行边(edge coverage);
- 每当 fuzz 出一个“新路径”,就会保存对应的输入。
AFL 的“灰盒”本质:程序内部状态可感知,但只看路径,不需要源码理解能力。
2、变异器(Mutator)
AFL 的输入变异策略分为多个阶段:
bit/byte 翻转(如翻转第 1 位)
字节插入/删除
整数覆盖(插入 0、1、INT_MAX)
Havoc 模式:随机多个操作叠加
Splice 模式:组合两个种子的部分数据(像交叉)
每个种子会根据这些策略生成大量变异输入,尝试触发不同路径或崩溃。
3、执行器(Executor)
AFL 使用一个名为 fork server 的技术,快速地对目标程序进行重复执行:
程序被加载后常驻内存;
每次新输入仅 fork 子进程执行;
大幅提升 fuzz 执行速度(数百倍)
4、路径反馈(Feedback)
核心是 AFL 的路径覆盖表(map[64k]):
每次执行都会将程序运行的代码路径(通过插桩)哈希后映射到这个表;
新的路径(bitmap 中新的位) → 保留对应输入 → 加入种子队列;
AFL 只保留那些“走新路径”的输入,剔除冗余样本。
5、种子调度器(Scheduler)
AFL 会根据每个种子的“重要性”分配不同的 fuzz 能量(次数):
路径稀有 → 分配更多 fuzz 次数;
高速种子优先(执行时间短);
后续 AFLFast 正是优化了这一步(低频路径优先、能量调度更合理)
6、崩溃检测与记录
每当程序运行崩溃(SIGSEGV、SIGABRT):
AFL 自动记录引发崩溃的输入样本;
保存在 crashes/ 文件夹;
可进一步用作漏洞验证或 PoC(Proof of Concept)
简单总结一下:
概念 | 含义 |
---|---|
插桩 | 在程序中添加记录代码路径的语句 |
插桩编译 | 自动为源代码添加监控逻辑再编译 |
AFL 的插桩编译器 | 替代 gcc,为每个代码块自动插入路径记录逻辑 |
作用 | 让 AFL 能检测“当前输入触发了新路径” |
思考:原始的AFL中有GA嘛?
严格的说,是没有的。类似GA但是并非:
对比项 | 遗传算法(GA) | 原始 AFL |
---|---|---|
是否有显式种群(population)? | ✅ 有 | ✅ 有种子队列(输入集合) |
是否使用适应度函数? | ✅ 明确定义适应度打分 | ❌ 没有明确“评分函数”,仅用路径新颖性判断是否保留输入 |
是否进行父代选择? | ✅ 有选择算法(如轮盘赌、锦标赛) | ❌ 只用简单轮询 / 静态调度策略,AFLFast 才引入调度优化 |
是否有交叉操作? | ✅ 有明确 crossover 算子 | ⚠️ 有类似行为(splice 模式),但不是系统性遗传操作 |
是否有变异操作? | ✅ 有突变算子 | ✅ 丰富的 bit/byte 变异器 |
是否进化目标函数? | ✅ 明确的最优化目标 | ⚠️ 模糊目标:尽量多探索路径,发现崩溃 |
是否有一代一代进化? | ✅ 明确的“代数” | ❌ 不分代,基于种子队列循环变异执行 |
终止条件? | ✅ 迭代轮数、误差阈值等 | ❌ 只看时间/崩溃条件等外部限制 |
(搬运gpt)
为什么说AFL像GA?
GA 特性 | AFL 类比行为 |
---|---|
个体(染色体) | 一个输入样本(种子) |
种群 | 种子队列(输入池) |
突变(mutation) | 多种 bit/byte 变异策略 |
交叉(crossover) | splice() 操作组合两个种子 |
适应度选择 | 如果输入带来新路径 → 被保留 |
生存竞争 | 新路径被选中,冗余路径淘汰 |
AFLFast
项目地址:https://github.com/mboehme/aflfast
AFLFast,它是对原始 AFL 的一个重要增强版本,主要目标是提高路径探索效率,加快发现漏洞的速度。
AFLFast(Coverage-based Greybox Fuzzing as Markov Chain) 是一种对 AFL 的优化版,它用更聪明的方式选择种子和分配能量,让 fuzz 更快地覆盖更多代码路径。
简单来说也就一句话:“不是所有种子都值得花一样多精力去 fuzz!”
原始AFL的问题:
默认的种子调度是轮询式,每个 seed 轮流 fuzz;
结果:
- 高频路径被反复 fuzz,浪费时间;
- 低频路径(隐藏 bug 的地方)很少被探索到;
- 整体路径增长变慢,bug 发现效率低。
改进思想:
1. 马尔可夫链建模
AFLFast 把模糊测试过程抽象成马尔可夫链(Markov Chain):
- 每个路径视为一个状态
π_i
; - 每次种子变异是从
π_i → π_j
的状态转移,转移概率为p_ij
; - 目标是以最小代价探索所有状态(路径);
- 因此应优先 fuzz 从未或少 fuzz 过的路径(低频路径 = 潜在新分支)。
2. 频率反比调度(Power Schedule)
根据路径频率分配“能量”(即变异次数):
- 频率高 → 能量低(不再浪费时间);
- 频率低 → 能量高(值得深度挖掘);
- 多种调度策略被提出,例如:
策略 | 说明 |
---|---|
EXPLOIT | 优先 fuzz 覆盖更多新路径的种子 |
EXPLORE | 优先 fuzz 低频路径的种子 |
FAST | 路径频率越高,fuzz 次数指数级减少 |
COE | 混合策略(Covariance-based Exponential) |
AFLFast 默认用的是 FAST
策略。
3. 搜索策略(Search Strategy)增强
AFLFast 修改了 choose_next()
逻辑:
- 种子不是轮询选择,而是按优先级排序;
- 优先选择:
- 执行时间短的(加快迭代);
- 路径深的(更可能通向新分支);
- 低频路径对应的种子。
4. 能量调度公式示意(FAST 示例)
AFL 中的种子能量 = 变异次数,原始是固定值;
在 AFLFast 中:
energy=baseenergy/(1+freq(seed.path))energy = base_energy / (1 + freq(seed.path)) energy=baseenergy/(1+freq(seed.path))
或指数形式:
energy=baseenergy∗exp(−α∗freq)energy = base_energy * exp(-α * freq) energy=baseenergy∗exp(−α∗freq)
freq
表示种子对应路径在历史上被 fuzz 过的频率;α
是可调参数(控制频率影响强度);
这样设计能确保新奇的路径被更充分地探索。
┌──────────────────┐ ┌──────────────────┐│ AFL (baseline) │ │ AFLFast │└──────────────────┘ └──────────────────┘│ │[轮询选择种子] [基于路径频率调度种子]↓ ↓[固定能量分配] [能量与路径频率成反比]↓ ↓[路径覆盖慢] [路径覆盖快、bug 快速暴露]
思考:关于能量的问题:
fuzz 测试资源(时间、次数)应该花在谁身上更划算?
一、抽象理解:什么是“能量”?
在 AFL / AFLFast 中,能量就是你给一个种子分配的变异次数(fuzz 次数)。
也可以理解为:
能量 = 种子被选择后,被 fuzz 的强度(数量)
例如:
- 给一个 seed 分配了 100 能量,就会对它变异生成 100 个输入;
- 给另一个只有 10 能量 → 只生成 10 个变异输入。
所以,“谁的能量高”,谁就被 fuzz 得更深入、更频繁。
二、频率反比调度的核心思想
⚠️ 问题:如果你总是 fuzz 那些常见路径,探索不到新的代码分支,浪费资源。
解决思路:应该给 “探索价值大” 的种子更多能量,即:
触发“稀有路径”的种子 → 应该多 fuzz!
走“高频路径”的种子 → fuzz 少点!
三、公式抽象(以 AFLFast 的 FAST 模型为例)
其中:
base_energy
: 默认能量基准值freq
: 表示 seed 所对应路径的历史命中次数α
: 衰减系数energy
: 分配给该 seed 的变异次数
效果:
- 路径频率
freq
越小 →energy
越大 - 路径频率
freq
越高 →energy
越小
四、伪代码:
for seed in seed_queue:path_id = hash_path(seed) # 路径标识freq = path_counter.get(path_id, 1)energy = BASE / freq # 或 BASE * exp(-alpha * freq)for i in range(int(energy)):new_input = mutate(seed)run_target(new_input)update_path_counter()
关键点:
- 每个路径有自己的访问频率计数;
- seed 的路径频率越低,它对应的能量越高;
- 动态更新 freq → 调整资源分配。
五、实际 fuzz 中哪些 seed 更“值得”投入能量?
✅ 1. 触发了新路径的种子(Rare Path Seed)
- 对应的路径在 bitmap 中覆盖稀少;
- fuzz 它更有可能“挖到新逻辑”。
👉 AFLFast 会优先调度这类种子 + 提高能量。
✅ 2. 执行时间短的种子
- 目标是单位时间内尝试更多变异;
- 短种子 → 快执行 → 高性价比;
✅ 3. 路径深、复杂逻辑的种子
- 触发路径较深层函数 → 更容易挖出逻辑漏洞;
- 有时需要结合 coverage depth(路径深度)做调度;
✅ 4. 带有未初始化数据或边界输入的种子
- 比如触发边界检查或稀有错误分支的输入;
- 虽然路径频率可能还不低,但 fuzz 潜力大;
- 在一些高级调度策略中也会被优先处理。