文章目录
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 拆分为三部分:
- 前缀M1需要能够达到选定状态 s
- 候选子序列M2包含所有可以在 M2之后执行但仍保留在 s 中的消息
- 后缀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&#