AFLnet变异策略分析

本文详细分析了AFLnet的fuzz_one函数,重点介绍了其在状态感知模式下的策略改进,包括如何选择M2区域进行变异,以及deterministicfuzz阶段的SIMPLEBITFLIP、ARITHMETICINC/DEC、INTERESTINGVALUES和DICTIONARYSTUFF等步骤。此外,还讨论了随机混乱(RAMDOMHAVOC)阶段的各种变异策略,如位翻转、字节替换、字插入和删除等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

fuzz_one

fuzz_one函数是AFLNET的变异策略实现的函数,函数很长,接近两千行,我们分阶段进行分析。
Let’s start the journey.

初始化阶段

在这里插入图片描述
函数注释为:
Take the current entry from the queue, fuzz it for a while. This function is a tad too long… returns 0 if fuzzed successfully, 1 if skipped or bailed out.
大致意思就是:
对当前testcase队列进行变异,变异成功return 0,变异失败return 1.

首先判断是否处于状态感知模式,如果是,就直接跳到后面的AFLNET_REGIONS_SELECTION部分。

  //Skip some steps if in state_aware_mode because in this mode
  //the seed is selected based on state-aware algorithms
  if (state_aware_mode) goto AFLNET_REGIONS_SELECTION;

判断pending_favored是否为1,如果为1,则对于已经fuzz过的queue(queue_cur->was_fuzzed == 1)或者不是favored的queue(queue_cur->favored != 1),则会进行UR是否小于SKIP_TO_NEW_PROB这个判断。
在这里插入图片描述
UR(100)的作用是生成一个0~99之间的随机数。

static inline u32 UR(u32 limit) {
   

  if (unlikely(!rand_cnt--)) {
   

    u32 seed[2];

    ck_read(dev_urandom_fd, &seed, sizeof(seed), "/dev/urandom");

    srandom(seed[0]);
    rand_cnt = (RESEED_RNG / 2) + (seed[1] % RESEED_RNG);

  }

  return random() % limit;

}

SKIP_TO_NEW_PROB的值为99.
0~99之间的数小于99的概率为99%,所以这里的判断有99%的概率为1。
所以这三个连续判断的意思就是,对于已经fuzz过的queue(queue_cur->was_fuzzed == 1)或者不是favored的queue(queue_cur->favored != 1),有99%的概率会直接return 1,相当于跳过这些queue。

if ((queue_cur->was_fuzzed || !queue_cur->favored) &&
        UR(100) < SKIP_TO_NEW_PROB) return 1;

如果pending_favored不为0,那么则会执行下面这个else if,意思大概就是:
首先判断是不是dumb_mode,如果不是,则判断当前queue是不是favored,如果不是,则判断queued_paths是否大于10,如果是,则进入内层if块。
内层if语句块是对于queue_cycle是否大于1(循环次数是否大于1),以及queue_cur->was_fuzzed是否为0(当前queue是否已经被fuzz过)的判断。
如果满足这两个条件,则说明这个queue是新的,则有75%的概率跳过该queue。
如果不满足,说明这个queue是旧的,则有95%的概率跳过该queue。

else if (!dumb_mode && !queue_cur->favored && queued_paths > 10) {
   

    /* Otherwise, still possibly skip non-favored cases, albeit less often.
       The odds of skipping stuff are higher for already-fuzzed inputs and
       lower for never-fuzzed entries. */

    if (queue_cycle > 1 && !queue_cur->was_fuzzed) {
   

      if (UR(100) < SKIP_NFAV_NEW_PROB) return 1;

    } else {
   

      if (UR(100) < SKIP_NFAV_OLD_PROB) return 1;

    }

  }

afl是通过控制下面三个全局变量的值,来控制上述概率的,这样做的目的是通过概率控制fuzz偏好,使得一些可能更有价值的queue获得更多的fuzz机会:

#define SKIP_TO_NEW_PROB    99 /* ...when there are new, pending favorites */
#define SKIP_NFAV_OLD_PROB  95 /* ...no new favs, cur entry already fuzzed */
#define SKIP_NFAV_NEW_PROB  75 /* ...no new favs, cur entry not fuzzed yet */

如果不是在tty模式下,则输出提示信息并撒互信stdout缓冲区:

  if (not_on_tty) {
   
    ACTF("Fuzzing test case #%u (%u total, %llu uniq crashes found)...",
         current_entry, queued_paths, unique_crashes);
    fflush(stdout);
  }

AFLnet对于AFL的策略改进(重点)

正在分析源码的同学肯定注意到了,从上面的代码开始,后面AFLnet和AFL的代码就有较大差别了。原因是因为AFLnet使用具有协议感知能力的变异算子增强了AFL的fuzz_one函数。为了理解这个差别,我们先看AFLnet的论文。

给定状态 s 和消息序列 M,AFLNET 通过变异生成一个新序列M’。为了保证变异的序列仍然行使选择的状态 s,AFLNET 将原始序列 M 拆分为三部分:

  1. 前缀M1需要能够达到选定状态 s
  2. 候选子序列M2包含所有可以在 M2之后执行但仍保留在 s 中的消息
  3. 后缀M3就是剩余的子序列.
    也即<M1,M2,M3>=M.变异消息序列为M’=<M1,mutate(M2),M3>.

通过保持原始子序列M1,M’仍将达到状态 s,这是模糊测试工具当前关注的状态。变异的候选子序列mutate(M2) 在选择的状态 s 上产生一个可选的消息序列。在我们最初的实验中,我们观察到替代请求可能“现在”不可观察,但会传播到以后的响应(, we observed that the alternative requests may not be observable “now”, but propagate to later responses)。因此,AFLNET 继续执行后缀M3.

这样做具有什么优点呢?
1:基于突变的方法可以在没有目标协议规范的前提下利用真实网络流量的有效trace来生成可能有效的新序列。相比而言,基于生成的方法需要详细的协议规范,包括具体的消息模板和协议状态机。
2:基于突变的方法可能进化出特别有趣的消息序列的语料库。引起发现新状态、状态转换或者程序分支的生成序列,会被添加到语料库进行进一步的模糊测试

AFLNET_REGIONS_SELECTION

上面说了,如果是状态感知魔术,则会跳到AFLNET_REGIONS_SELECTION这个部分。
在这里插入图片描述
首先初始化了两个变量,用于记录M2消息序列的区域,M2_start_region_ID是M2消息序列开始的地方,M2_region_count是M2消息序列的长度。

  u32 M2_start_region_ID = 0, M2_region_count = 0;

如果在状态感知模式下,则会随机选择M2区域:

else {
   
    /* Select M2 randomly */
    u32 total_region = queue_cur->region_count;
    if (total_region == 0) PFATAL("0 region found for %s", queue_cur->fname);

    M2_start_region_ID = UR(total_region);
    M2_region_count = UR(total_region - M2_start_region_ID);
    if (M2_region_count == 0) M2_region_count++; //Mutate one region at least
  }

如果在状态感知模式下,会要进行M2区域的划分:

 /* In state aware mode, select M2 based on the targeted state ID */
    u32 total_region = queue_cur->region_count;
    if (total_region == 0) PFATAL("0 region found for %s", queue_cur->fname);

    if (target_state_id == 0) {
   
      //No prefix subsequence (M1 is empty)
      M2_start_region_ID = 0;
      M2_region_count = 0;

      //To compute M2_region_count, we identify the first region which has a different annotation
      //Now we quickly compare the state count, we could make it more fine grained by comparing the exact response codes
      for(i = 0; i < queue_cur->region_count ; i++) {
   
        if (queue_cur->regions[i].state_count != queue_cur->regions[0].state_count) break;
        M2_region_count++;
      }

通过target state id确定M2开始的地方:

//Identify M2_start_region_ID first based on the target_state_id
      for(i = 0; i < queue_cur->region_count; i++) {
   
        u32 regionalStateCount = queue_cur->regions[i].state_count;
        if (regionalStateCount > 0) {
   
          //reachableStateID is the last ID in the state_sequence
          u32 reachableStateID = queue_cur->regions[i].state_sequence[regionalStateCount - 1];
          M2_start_region_ID++;
          if (reachableStateID == target_state_id) break;
        } else {
   
          //No annotation for this region
          return 1;
        }
      }

然后确定M2_region_count:

//Then identify M2_region_count
      for(i = M2_start_region_ID; i < queue_cur->region_count ; i++) {
   
        if (queue_cur->regions[i].state_count != queue_cur->regions[M2_start_region_ID].state_count) break;
        M2_region_count++;
      }

然后通过construct_kl_messages构造kl_message:

kl_messages = construct_kl_messages(queue_cur->fname, queue_cur->regions, queue_cur->region_count);

然后按照M2区域的开始位置和长度对消息序列进行划分。
找到M2区域的边界,并用M2_prev指向M2区域开始位置的前一个位置,M2_next指向M2区域结束位置的后一个位置。

liter_t(lms) *it;

  M2_prev = NULL;
  M2_next = kl_end(kl_messages);

  u32 count = 0;
  for (it = kl_begin(kl_messages); it != kl_end(kl_messages); it = kl_next(it)) {
   
    if (count == M2_start_region_ID - 1) {
   
      M2_prev = it;
    }

    if (count == M2_start_region_ID + M2_region_count) {
   
      M2_next = it;
    }
    count++;
  }

首先将M2_pre赋值给it,然后通过while循环,遍历整个M2区域。然后为变异做好准备,构建好缓冲区,并更新out_buf。之后会对out_buf里的东西进行变异。

  /* Construct the buffer to be mutated and update out_buf */
  if (M2_prev == NULL) {
   
    it = kl_begin(kl_messages);
  } else {
   
    it = kl_next(M2_prev);
  }
  u32 in_buf_size = 0;
  while (it != M2_next) {
   
    in_buf = (u8 *) ck_realloc (in_buf, in_buf_size + kl_val(it)->msize);
    if (!in_buf) PFATAL("AFLNet cannot allocate memory for in_buf");
    //Retrieve data from kl_messages to populate the in_buf
    memcpy(&in_buf[in_buf_size], kl_val(it)->mdata, kl_val(it)->msize);

    in_buf_size += kl_val(it)->msize;
    it = kl_next(it);
  }

  orig_in = in_buf;

  out_buf = ck_alloc_nozero(in_buf_size);
  memcpy(out_buf, in_buf, in_buf_size);

记录缓冲区长度,保证后期变异正确:
在这里插入图片描述

PERFORMANCE SCORE

从这里开始就是PERFORMANCE SCORE阶段:
在这里插入图片描述
首先调用calculate_score函数,计算得分:

orig_perf = perf_score = calculate_score(queue_cur);

对于以下三种情况,会直接跳转到havoc_stage去运行:

1:skip_deterministic不为0,意思是跳过deterministic阶段。
2:queue_cur->was_fuzzed不为0,意思是当前queue被fuzz过了。
3:queue_cur->passed_det不为0,意思是当前queue执行过deterministic阶段。
4:如果exec路径校验和使确定性模糊超出此主实例的范围,则跳过确定性模糊,这个我不太清楚是什么意思。

  if (skip_deterministic || queue_cur->was_fuzzed || queue_cur->passed_det)
    goto havoc_stage;
  if (master_max && (queue_cur->exec_cksum % master_max) != master_id - 1)
    goto havoc_stage;

如果不跳过deterministic fuzz阶段,则设置doing_det为1,表示正在进行deterministic fuzz。

Deterministic fuzz阶段

deterministic fuzz阶段包括若干个子阶段:

SIMPLE BITFLIP(+dictionary construction)

bitflip(比特翻转),根据目标大小的不同,分为多个阶段:

bitflip 1/1 && collect tokens -> 按位取反 && 搜集token
bitflip 2/1 相邻两位进行取反
bitflip 4/1 相邻四位进行取反
bitflip 8/8 && effector map -> 按字节取反 && 构建effector map
bitflip 16/8 连续两byte翻转
bitflip 32/8 连续四byte翻转

比特的翻转是怎么做的呢,具体实现方式是通过一个宏实现的。
这个宏非常精妙,用一行代码完成了字节的翻转。需要仔细分析才能明白他到底是在做什么。

等式右边:
1:_bf & 7,会得到0~7之间的一个整数。
2:十进制128转换成二进制也就是10000000,总共八位
3:128 >> ((_bf) & 7),也就是将10000000右移0~7位,具体是多少位要看_bf这个值是多少
等式左边:
1:_arf是一个byte大小的指针,初始化时指向_ar的第一个字节
2:_arf大小为一个byte,就是8个bit。所以应把_bf >> 3,看作除以8。stage_cur这个数,也就是_b,从0到7除以8等于0,8到15除以8等于1&#

### 关于 AFLNet 源码解析 #### 灰盒网络协议模糊测试工具概述 AFLNet 是一种灰盒网络协议模糊测试工具,旨在通过改进 American Fuzzy Lop (AFL) 来支持对复杂网络协议的高效探索和测试[^2]。 #### 主要组件分析 AFLNet 的核心在于其能够处理复杂的输入结构并有效地遍历目标应用程序的状态空间。以下是几个关键组成部分: - **种子生成器**:负责创建初始有效的通信会话作为起始点。 - **变异引擎**:采用多种策略来修改现有的有效载荷数据包,包括但不限于位翻转、字节替换以及基于语义的理解来进行更高级别的变换操作。 - **路径敏感反馈机制**:利用执行过程中收集到的信息指导后续变体的选择过程,从而提高覆盖率。 ```python def mutate_payload(payload): """示例函数展示如何可能实现payload的简单变异""" mutated = bytearray(payload) # 随机选择位置进行单个字节更改 idx_to_change = random.randint(0, len(mutated)-1) new_val = chr(random.getrandbits(8)) mutated[idx_to_change] = ord(new_val) return bytes(mutated) ``` #### 安装与配置说明 对于希望深入研究 AFLNet 工作原理的研究人员来说,可以从 GitHub 或其他托管平台上获取最新版本的源代码仓库,并按照官方文档指示完成编译环境搭建工作。通常情况下这涉及到安装依赖项如 LLVM 编译套件等前置条件设置。 #### 实践案例分享 为了更好地理解该工具的实际应用场景及其优势所在,可以参考一些已发表的技术报告或学术论文,在其中往往包含了详细的实验设计思路及结果讨论部分。例如 ChatAFL 就是在此基础上进一步创新的例子之一,它引入了大型语言模型(LLMs),实现了更加智能化的消息序列预测功能[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值