基于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 random

def decode(individual):
    return int("".join(str(bit) for bit in individual), 2)

def fitness(individual):
    x = decode(individual)
    return x ** 2

def 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-gccafl-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 模型为例)
image-20250716171022377

其中:

  • 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 潜力大;
  • 在一些高级调度策略中也会被优先处理。
posted @ 2025-07-17 16:33  stribik  阅读(22)  评论(0)    收藏  举报