文章目录
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,16到23除以8等于2…依次类推。也就是说,前八次调用FLIP_BIT这个宏时,是_arf[0],也就是第0个字节,后八次调用FLIP_BIT这个宏时,是_arf[1],再往后亦然,达到了一个循环遍历_ar数组的目的。*(这里是比较巧妙的)
等式整体:
等式左边是为了控制指向的字节数。等式右边相当于提供掩码,其值会从10000000变化到010000000,然后一步一步变为000000001,目的是为了控制该字节翻转的bit位置。然后通过异或运算,达到指定bit位的翻转。
总结一下,这个宏起到的作用就是,对_ar指向的数组的每个字节的每个bit进行翻转。为了优化代码,用的运算全是位运算,所以比较难以理解。
#define FLIP_BIT(_ar, _b) do { \
u8* _arf = (u8*)(_ar); \
u32 _bf = (_b); \
_arf[(_bf) >> 3] ^= (128 >> ((_bf) & 7)); \
} while (0)
定义完毕FLIP_BIT这个宏之后,通过循环完成对FLIP_BIT的反复调用:
for (stage_cur = 0; stage_cur < stage_max; stage_cur++) {
stage_cur_byte = stage_cur >> 3;
FLIP_BIT(out_buf, stage_cur);
......
}
因为我们前面是把M2区域单独提出来,放在一个缓冲区内进行变异,并没有对testcase本身进行任何改变。所以一次变异执行完之后,会调用common_fuzz_stuff,编写修改后的testcase,然后再运行一次程序,处理结果。用户可以通过SIGUSR1信号,放弃当前输入,直接goto abandon_entry.
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
后面这段的作用是猜解token。
if (!dumb_mode && (stage_cur & 7) == 7) {
u32 cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);
if (stage_cur == stage_max - 1 && cksum == prev_cksum) {
/* If at end of file and we are still collecting a string, grab the
final character and force output. */
if (a_len < MAX_AUTO_EXTRA) a_collect[a_len] = out_buf[stage_cur >> 3];
a_len++;
if (a_len >= MIN_AUTO_EXTRA && a_len <= MAX_AUTO_EXTRA)
maybe_add_auto(a_collect, a_len);
} else if (cksum != prev_cksum) {
/* Otherwise, if the checksum has changed, see if we have something
worthwhile queued up, and collect that if the answer is yes. */
if (a_len >= MIN_AUTO_EXTRA && a_len <= MAX_AUTO_EXTRA)
maybe_add_auto(a_collect, a_len);
a_len = 0;
prev_cksum = cksum;
}
/* Continue collecting string, but only if the bit flip actually made
any difference - we don't want no-op tokens. */
if (cksum != queue_cur->exec_cksum) {
if (a_len < MAX_AUTO_EXTRA) a_collect[a_len] = out_buf[stage_cur >> 3];
a_len++;
}
}
猜解的方式为:
假设有一段字符串,为xxxxxxxxTCP************
x和可以是任何字符
如果改变了xxxxxxxxx或者*********这两段字符串,都不会造成程序执行流的变化,而改变TCP这个字符串中的任何一个字符,都会导致程序执行流的变化,那么就将TCP定义为一个tokon
猜解token的目的是为了保证关键字符串不被破坏,比如TCP,改变为TAP,肯定会造成错误。
之后进行的2个bit,4个bit,8个bit,16个bit,32个bit的翻转,基本思想都差不多,这里就不多赘述了。
ARITHMETIC INC/DEC
这个阶段是进行加减运算的变异.
这个阶段也是根据bit数划分为若干个阶段。
首先是arith 8/8阶段。
第一步遍历M2区域,也就是out_buf中的所有字节,将其赋值给orig,然后判断这个字节在eff_map中是否不为0,如果为0,则stage_max -= 2 *ARITH_MAX.进入下一轮循环。如果不为0,则继续执行后续代码。
for (i = 0; i < len; i++) {
u8 orig = out_buf[i];
/* Let's consult the effector map... */
if (!eff_map[EFF_APOS(i)]) {
stage_max -= 2 * ARITH_MAX;
continue;
}
......
}
然后表示出当前正在操作的字节:
stage_cur_byte = i;
然后进行对orig字节的加减运算变异。
首先明白一点:
加法变异的代码为:r = orig ^ (orig + j);
减法变异的代码为:r = orig ^ (orig - j);
那么下面的代码逻辑就很清楚了,令j从1到ARITH_MAX遍历,
然后将将orig ^ (orig + j)的值赋值给r。
需要注意的是,需要调用could_be_bitflip函数对r进行一次判断,判断r是否可以通过bitflip方式得到,如果可以,则说明这是个冗余变异,不会被采纳。
for (j = 1; j <= ARITH_MAX; j++) {
u8 r = orig ^ (orig + j);
/* Do arithmetic operations only if the result couldn't be a product
of a bitflip. */
if (!could_be_bitflip(r)) {
stage_cur_val = j;
out_buf[i] = orig + j;
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;
} else stage_max--;
r = orig ^ (orig - j);
if (!could_be_bitflip(r)) {
stage_cur_val = -j;
out_buf[i] = orig - j;
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;
} else stage_max--;
out_buf[i] = orig;
}
arith 16/8,32/8阶段,原理都是差不多的,只是会多考虑大小端这一个因素。
INTERESTING VALUES
这个阶段主要将out_buf中的字节替换成AFL预设的一些"interesting values"。替换语料在config.h中。
#define INTERESTING_8 \
-128, /* Overflow signed 8-bit when decremented */ \
-1, /* */ \
0, /* */ \
1, /* */ \
16, /* One-off with common buffer size */ \
32, /* One-off with common buffer size */ \
64, /* One-off with common buffer size */ \
100, /* One-off with common buffer size */ \
127 /* Overflow signed 8-bit when incremented */
#define INTERESTING_16 \
-32768, /* Overflow signed 16-bit when decremented */ \
-129, /* Overflow signed 8-bit */ \
128, /* Overflow signed 8-bit */ \
255, /* Overflow unsig 8-bit when incremented */ \
256, /* Overflow unsig 8-bit */ \
512, /* One-off with common buffer size */ \
1000, /* One-off with common buffer size */ \
1024, /* One-off with common buffer size */ \
4096, /* One-off with common buffer size */ \
32767 /* Overflow signed 16-bit when incremented */
#define INTERESTING_32 \
-2147483648LL, /* Overflow signed 32-bit when decremented */ \
-100663046, /* Large negative number (endian-agnostic) */ \
-32769, /* Overflow signed 16-bit */ \
32768, /* Overflow signed 16-bit */ \
65535, /* Overflow unsig 16-bit when incremented */ \
65536, /* Overflow unsig 16 bit */ \
100663045, /* Large positive number (endian-agnostic) */ \
2147483647 /* Overflow signed 32-bit when incremented */
同样分为 8/8(按字节替换),16/8(按两个字节替换),32/8(按四个字节替换)。
首先循环,用8bit变量orig承接out_buf.
然后判断这个字节在eff_map中是否有效,如果无效,则stage_max -= sizeof(interesting_8),进入下一次循环。如果有效,继续执行内部循环。
for (i = 0; i < len; i++) {
u8 orig = out_buf[i];
/* Let's consult the effector map... */
if (!eff_map[EFF_APOS(i)]) {
stage_max -= sizeof(interesting_8);
continue;
}
......
}
设置当前操作字节为i:
stage_cur_byte = i;
然后进入内层循环:
首先判断当前变异是否可以通过单比特翻转,加减运算变异得到。
for (j = 0; j < sizeof(interesting_8); j++) {
/* Skip if the value could be a product of bitflips or arithmetics. */
if (could_be_bitflip(orig ^ (u8)interesting_8[j]) ||
could_be_arith(orig, (u8)interesting_8[j], 1)) {
stage_max--;
continue;
}
.......
}
如果不可以,则用interesting_8对目标进行替换。
stage_cur_val = interesting_8[j];
out_buf[i] = interesting_8[j];
然后惯例调用common_fuz_stuff运行变异后的testcase:
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
out_buf[i] = orig;
stage_cur++;
2字节替换和4字节替换的原理也是一样的。
不过,2字节替换会检查该变异能否通过单字节变异得到,以此筛选冗余变异;4字节替换也会检查能否通过2字节替换和单字节替换得到。此外,2字节替换和4字节替换都会注意到大小端序的问题。
DICTIONARY STUFF
这个阶段是deterministic fuzzing的最后一个阶段,本阶段主要基于用户提供的extras token(通过maybe_add_auto、load_extras、save_auto得到)来进行变异,变异规则为替换或者插入。
用户可以通过-x参数来设置引用的词典,如果没有设置则跳过这个阶段。
首先判断extras_cnt是否为空,如果为空则说明用户没有提供extra token,会直接跳过这个阶段。
if (!extras_cnt) goto skip_user_extras;
DICTIONARY STUFF这个阶段又分为三个子阶段。
user extras(over):用用户自定义的tokens去替换
user extras(insert):插入用户自定义的tokens
auto extras(over):用自动生成的tokens进行替换
user extras(over)阶段主要是将out_buf替换为用户提供的tokens。
如果满足下面这个if语句中的条件,则会跳过这次替换,进入下一次循环。
if ((extras_cnt > MAX_DET_EXTRAS && UR(extras_cnt) >= MAX_DET_EXTRAS) ||
extras[j].len > len - i ||
!memcmp(extras[j].data, out_buf + i, extras[j].len) ||
!memchr(eff_map + EFF_APOS(i), 1, EFF_SPAN_ALEN(i, extras[j].len))) {
stage_max--;
continue;
}
如果不满足上述条件,则说明此时可以进行替换:
last_len = extras[j].len;
memcpy(out_buf + i, extras[j].data, last_len);
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
然后进行user extras(insert)阶段,与over阶段不同的是,这个阶段采取插入tokens的方法:
for (i = 0; i <= len; i++) {
stage_cur_byte = i;
for (j = 0; j < extras_cnt; j++) {
if (len + extras[j].len > MAX_FILE) {
stage_max--;
continue;
}
/* Insert token */
memcpy(ex_tmp + i, extras[j].data, extras[j].len);
/* Copy tail */
memcpy(ex_tmp + i + extras[j].len, out_buf + i, len - i);
if (common_fuzz_stuff(argv, ex_tmp, len + extras[j].len)) {
ck_free(ex_tmp);
goto abandon_entry;
}
stage_cur++;
}
第三个阶段是用自动生成的tokens进行替换,操作基本与user extras(over)阶段相同,只是对象不同而已。
stage_name = "auto extras (over)";
stage_short = "ext_AO";
stage_cur = 0;
stage_max = MIN(a_extras_cnt, USE_AUTO_EXTRAS) * len;
stage_val_type = STAGE_VAL_NONE;
orig_hit_cnt = new_hit_cnt;
for (i = 0; i < len; i++) {
u32 last_len = 0;
stage_cur_byte = i;
for (j = 0; j < MIN(a_extras_cnt, USE_AUTO_EXTRAS); j++) {
/* See the comment in the earlier code; extras are sorted by size. */
if (a_extras[j].len > len - i ||
!memcmp(a_extras[j].data, out_buf + i, a_extras[j].len) ||
!memchr(eff_map + EFF_APOS(i), 1, EFF_SPAN_ALEN(i, a_extras[j].len))) {
stage_max--;
continue;
}
last_len = a_extras[j].len;
memcpy(out_buf + i, a_extras[j].data, last_len);
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;
}
/* Restore all the clobbered memory. */
memcpy(out_buf + i, in_buf + i, last_len);
}
new_hit_cnt = queued_paths + unique_crashes;
stage_finds[STAGE_EXTRAS_AO] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_EXTRAS_AO] += stage_max;
RAMDOM HAVOC
从这里开始的变异阶段就不属于Deterministic fuzz阶段了。
RAMDOM HAVOC的意思是随机混乱,顾名思义,这个阶段是各种变异策略的随机组合。
首先判断splice_cycle这个标识符是否有效,如果无效,则标记此阶段为havoc;如果有效,则标记此阶段为splice。
if (!splice_cycle) {
stage_name = "havoc";
stage_short = "havoc";
stage_max = (doing_det ? HAVOC_CYCLES_INIT : HAVOC_CYCLES) *
perf_score / havoc_div / 100;
} else {
static u8 tmp[32];
perf_score = orig_perf;
sprintf(tmp, "splice %u", splice_cycle);
stage_name = tmp;
stage_short = "splice";
stage_max = SPLICE_HAVOC * perf_score / havoc_div / 100;
}
然后通过20个case,来进行各种变异策略的执行:
for (stage_cur = 0; stage_cur < stage_max; stage_cur++) {
u32 use_stacking = 1 << (1 + UR(HAVOC_STACK_POW2));
stage_cur_val = use_stacking;
for (i = 0; i < use_stacking; i++) {
switch (UR(15 + 2 + (region_level_mutation ? 4 : 0))) {
case 0:
/* Flip a single bit somewhere. Spooky! */
FLIP_BIT(out_buf, UR(temp_len << 3));
break;
case 1:
/* Set byte to interesting value. */
out_buf[UR(temp_len)] = interesting_8[UR(sizeof(interesting_8))];
break;
case 2:
/* Set word to interesting value, randomly choosing endian. */
if (temp_len < 2) break;
if (UR(2)) {
*(u16*)(out_buf + UR(temp_len - 1)) =
interesting_16[UR(sizeof(interesting_16) >> 1)];
} else {
*(u16*)(out_buf + UR(temp_len - 1)) = SWAP16(
interesting_16[UR(sizeof(interesting_16) >> 1)]);
}
break;
case 3:
/* Set dword to interesting value, randomly choosing endian. */
if (temp_len < 4) break;
if (UR(2)) {
*(u32*)(out_buf + UR(temp_len - 3)) =
interesting_32[UR(sizeof(interesting_32) >> 2)];
} else {
*(u32*)(out_buf + UR(temp_len - 3)) = SWAP32(
interesting_32[UR(sizeof(interesting_32) >> 2)]);
}
break;
case 4:
/* Randomly subtract from byte. */
out_buf[UR(temp_len)] -= 1 + UR(ARITH_MAX);
break;
case 5:
/* Randomly add to byte. */
out_buf[UR(temp_len)] += 1 + UR(ARITH_MAX);
break;
case 6:
/* Randomly subtract from word, random endian. */
if (temp_len < 2) break;
if (UR(2)) {
u32 pos = UR(temp_len - 1);
*(u16*)(out_buf + pos) -= 1 + UR(ARITH_MAX);
} else {
u32 pos = UR(temp_len - 1);
u16 num = 1 + UR(ARITH_MAX);
*(u16*)(out_buf + pos) =
SWAP16(SWAP16(*(u16*)(out_buf + pos)) - num);
}
break;
case 7:
/* Randomly add to word, random endian. */
if (temp_len < 2) break;
if (UR(2)) {
u32 pos = UR(temp_len - 1);
*(u16*)(out_buf + pos) += 1 + UR(ARITH_MAX);
} else {
u32 pos = UR(temp_len - 1);
u16 num = 1 + UR(ARITH_MAX);
*(u16*)(out_buf + pos) =
SWAP16(SWAP16(*(u16*)(out_buf + pos)) + num);
}
break;
case 8:
/* Randomly subtract from dword, random endian. */
if (temp_len < 4) break;
if (UR(2)) {
u32 pos = UR(temp_len - 3);
*(u32*)(out_buf + pos) -= 1 + UR(ARITH_MAX);
} else {
u32 pos = UR(temp_len - 3);
u32 num = 1 + UR(ARITH_MAX);
*(u32*)(out_buf + pos) =
SWAP32(SWAP32(*(u32*)(out_buf + pos)) - num);
}
break;
case 9:
/* Randomly add to dword, random endian. */
if (temp_len < 4) break;
if (UR(2)) {
u32 pos = UR(temp_len - 3);
*(u32*)(out_buf + pos) += 1 + UR(ARITH_MAX);
} else {
u32 pos = UR(temp_len - 3);
u32 num = 1 + UR(ARITH_MAX);
*(u32*)(out_buf + pos) =
SWAP32(SWAP32(*(u32*)(out_buf + pos)) + num);
}
break;
case 10:
/* Just set a random byte to a random value. Because,
why not. We use XOR with 1-255 to eliminate the
possibility of a no-op. */
out_buf[UR(temp_len)] ^= 1 + UR(255);
break;
case 11 ... 12: {
/* Delete bytes. We're making this a bit more likely
than insertion (the next option) in hopes of keeping
files reasonably small. */
u32 del_from, del_len;
if (temp_len < 2) break;
/* Don't delete too much. */
del_len = choose_block_len(temp_len - 1);
del_from = UR(temp_len - del_len + 1);
memmove(out_buf + del_from, out_buf + del_from + del_len,
temp_len - del_from - del_len);
temp_len -= del_len;
break;
}
case 13:
if (temp_len + HAVOC_BLK_XL < MAX_FILE) {
/* Clone bytes (75%) or insert a block of constant bytes (25%). */
u8 actually_clone = UR(4);
u32 clone_from, clone_to, clone_len;
u8* new_buf;
if (actually_clone) {
clone_len = choose_block_len(temp_len);
clone_from = UR(temp_len - clone_len + 1);
} else {
clone_len = choose_block_len(HAVOC_BLK_XL);
clone_from = 0;
}
clone_to = UR(temp_len);
new_buf = ck_alloc_nozero(temp_len + clone_len);
/* Head */
memcpy(new_buf, out_buf, clone_to);
/* Inserted part */
if (actually_clone)
memcpy(new_buf + clone_to, out_buf + clone_from, clone_len);
else
memset(new_buf + clone_to,
UR(2) ? UR(256) : out_buf[UR(temp_len)], clone_len);
/* Tail */
memcpy(new_buf + clone_to + clone_len, out_buf + clone_to,
temp_len - clone_to);
ck_free(out_buf);
out_buf = new_buf;
temp_len += clone_len;
}
break;
case 14: {
/* Overwrite bytes with a randomly selected chunk (75%) or fixed
bytes (25%). */
u32 copy_from, copy_to, copy_len;
if (temp_len < 2) break;
copy_len = choose_block_len(temp_len - 1);
copy_from = UR(temp_len - copy_len + 1);
copy_to = UR(temp_len - copy_len + 1);
if (UR(4)) {
if (copy_from != copy_to)
memmove(out_buf + copy_to, out_buf + copy_from, copy_len);
} else memset(out_buf + copy_to,
UR(2) ? UR(256) : out_buf[UR(temp_len)], copy_len);
break;
}
/* Values 15 and 16 can be selected only if there are any extras
present in the dictionaries. */
case 15: {
if (extras_cnt + a_extras_cnt == 0) break;
/* Overwrite bytes with an extra. */
if (!extras_cnt || (a_extras_cnt && UR(2))) {
/* No user-specified extras or odds in our favor. Let's use an
auto-detected one. */
u32 use_extra = UR(a_extras_cnt);
u32 extra_len = a_extras[use_extra].len;
u32 insert_at;
if (extra_len > temp_len) break;
insert_at = UR(temp_len - extra_len + 1);
memcpy(out_buf + insert_at, a_extras[use_extra].data, extra_len);
} else {
/* No auto extras or odds in our favor. Use the dictionary. */
u32 use_extra = UR(extras_cnt);
u32 extra_len = extras[use_extra].len;
u32 insert_at;
if (extra_len > temp_len) break;
insert_at = UR(temp_len - extra_len + 1);
memcpy(out_buf + insert_at, extras[use_extra].data, extra_len);
}
break;
}
case 16: {
if (extras_cnt + a_extras_cnt == 0) break;
u32 use_extra, extra_len, insert_at = UR(temp_len + 1);
u8* new_buf;
/* Insert an extra. Do the same dice-rolling stuff as for the
previous case. */
if (!extras_cnt || (a_extras_cnt && UR(2))) {
use_extra = UR(a_extras_cnt);
extra_len = a_extras[use_extra].len;
if (temp_len + extra_len >= MAX_FILE) break;
new_buf = ck_alloc_nozero(temp_len + extra_len);
/* Head */
memcpy(new_buf, out_buf, insert_at);
/* Inserted part */
memcpy(new_buf + insert_at, a_extras[use_extra].data, extra_len);
} else {
use_extra = UR(extras_cnt);
extra_len = extras[use_extra].len;
if (temp_len + extra_len >= MAX_FILE) break;
new_buf = ck_alloc_nozero(temp_len + extra_len);
/* Head */
memcpy(new_buf, out_buf, insert_at);
/* Inserted part */
memcpy(new_buf + insert_at, extras[use_extra].data, extra_len);
}
/* Tail */
memcpy(new_buf + insert_at + extra_len, out_buf + insert_at,
temp_len - insert_at);
ck_free(out_buf);
out_buf = new_buf;
temp_len += extra_len;
break;
}
/* Values 17 to 20 can be selected only if region-level mutations are enabled */
/* Replace the current region with a random region from a random seed */
case 17: {
u32 src_region_len = 0;
u8* new_buf = choose_source_region(&src_region_len);
if (new_buf == NULL) break;
//replace the current region
ck_free(out_buf);
out_buf = new_buf;
temp_len = src_region_len;
break;
}
/* Insert a random region from a random seed to the beginning of the current region */
case 18: {
u32 src_region_len = 0;
u8* src_region = choose_source_region(&src_region_len);
if (src_region == NULL) break;
if (temp_len + src_region_len >= MAX_FILE) {
ck_free(src_region);
break;
}
u8* new_buf = ck_alloc_nozero(temp_len + src_region_len);
memcpy(new_buf, src_region, src_region_len);
memcpy(&new_buf[src_region_len], out_buf, temp_len);
ck_free(out_buf);
ck_free(src_region);
out_buf = new_buf;
temp_len += src_region_len;
break;
}
/* Insert a random region from a random seed to the end of the current region */
case 19: {
u32 src_region_len = 0;
u8* src_region = choose_source_region(&src_region_len);
if (src_region == NULL) break;
if (temp_len + src_region_len >= MAX_FILE) {
ck_free(src_region);
break;
}
u8* new_buf = ck_alloc_nozero(temp_len + src_region_len);
memcpy(new_buf, out_buf, temp_len);
memcpy(&new_buf[temp_len], src_region, src_region_len);
ck_free(out_buf);
ck_free(src_region);
out_buf = new_buf;
temp_len += src_region_len;
break;
}
/* Duplicate the current region */
case 20: {
if (temp_len * 2 >= MAX_FILE) break;
u8* new_buf = ck_alloc_nozero(temp_len * 2);
memcpy(new_buf, out_buf, temp_len);
memcpy(&new_buf[temp_len], out_buf, temp_len);
ck_free(out_buf);
out_buf = new_buf;
temp_len += temp_len;
break;
}
}
}
关于这些case的作用,引用了这篇文章:
https://cata1oc.github.io/2022/03/12/AFL%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%9005/#RANDOM-HAVOC%EF%BC%88%E9%9A%8F%E6%9C%BA%E6%AF%81%E7%81%AD%EF%BC%89%E9%98%B6%E6%AE%B5
/* 1.外层循环stage_max轮stage(最小为16)
2.内层循环use_stacking轮,即一次stage中变化的次数(这是个随机的值,范围在2~14之间)
3.进入switch语句,分支条件根据extras的数量随机生成一个数(范围在0到16或0到14之间,其中case15和case16
只有存在extras时才有可能触发):
case0:
case1: 随机选中interesting_8[]中的某个byte随机替换out_buf中的某个byte
case2: 随机选中interesting_16[]中的某个word随机替换out_buf中的某个word(大小端序随机选择)
case3: 随机选中interesting_32[]中的某个dword随机替换out_buf中的某个dword(大小端序随机选择)
case4: 随机选中out_buf中的某个byte,减去一个随机的值(范围1~35)
case5: 随机选中out_buf中的某个byte,加上一个随机的值(范围1~35)
case6: 随机选中out_buf中的某个word,减去一个随机的值(大小端随机选择)
case7: 随机选中out_buf中的某个word,加上一个随机的值(大小端随机选择)
case8: 随机选中out_buf中的某个dword,减去一个随机的值(大小端随机选择)
case9: 随机选中out_buf中的某个dword,加上一个随机的值(大小端随机选择)
case10: 随机选中out_buf中的某个byte与范围在1~255中的一个随机值进行异或
case11: 随机选中out_buf中的某个byte进行删除
case12: 同case11,稍微增加一丢丢删除byte的概率,从而使得case更加精炼
case13: 随机选中out_buf中的某个位置,插入随机长度的内容:
a.75%的概率,插入一段out_buf中随机选择的一段内容
b.25%的概率,插入一段相同的随机数字(50%概率是随机生成的,50%概率是从out_buf中随机选取的一个byte)
case14: 随机选中out_buf中的某个位置,覆盖随机长度的内容:
a.75%的概率,覆盖一段out_buf中随机选择的一段内容
b.25%的概率,覆盖一段相同的随机数字(50%概率是随机生成的,50%概率是从out_buf中随机选取的一个byte)
case15: 随机选中out_buf中的某个位置,覆盖成extra token(token来自用户提供的extras和自动生成的a_extras)
case16: 随机选中out_buf中的某个位置,插入extra token(token来自用户提供的extras和自动生成的a_extras)
AFLNET的源码中,多出来了case17到20,这些case只有当启用了区域级变异才能选择。
SPLICING
这是由一整轮没有结果触发的最后手段策略。它获取当前输入文件,随机选择另一个输入,并在某个偏移处将它们拼接在一起,然后依靠破坏代码来进行变异。
因为之前的变异结果不佳才会调用这个阶段,所以首先要对in_buf进行clean。
if (in_buf != orig_in) {
ck_free(in_buf);
in_buf = orig_in;
len = M2_len;
}
然后随机选择一个queue:
do { tid = UR(queued_paths); } while (tid == current_entry);
splicing_with = tid;
target = queue;
while (tid >= 100) { target = target->next_100; tid -= 100; }
while (tid--) target = target->next;
确保目标具有合适的长度:
while (target && (target->len < 2 || target == queue_cur)) {
target = target->next;
splicing_with++;
}
if (!target) goto retry_splicing;
将目标testcase读取到一个新的缓冲区:
fd = open(target->fname, O_RDONLY);
if (fd < 0) PFATAL("Unable to open '%s'", target->fname);
new_buf = ck_alloc_nozero(target->len);
ck_read(fd, new_buf, target->len, target->fname);
close(fd);
在new_buf的第一个字节和最后一个字节之间找到一个合适的位置。
然后通过locate_diffs对in_buf和new_buf进行比较,定位两个buffer不同的地方,找到之后就从此处开始拼接两个buffer。
locate_diffs(in_buf, new_buf, MIN(len, target->len), &f_diff, &l_diff);
if (f_diff < 0 || l_diff < 2 || f_diff == l_diff) {
ck_free(new_buf);
goto retry_splicing;
}
在不同部分的第一个字节和最后一个字节之间随即寻找位置:
split_at = f_diff + UR(l_diff - f_diff);
开始进行拼接:
len = target->len;
memcpy(new_buf, in_buf, split_at);
in_buf = new_buf;
ck_free(out_buf);
out_buf = ck_alloc_nozero(len);
memcpy(out_buf, in_buf, len);
有所发现
上面说过,如果之前的变异效果不是很好,才会调用splice过程,如果效果好,有所发现,那么就会来到下面这个地方。
首先设置ret_val = 0。
然后设置splicing_with = -1,表示不经历splice过程。
然后对于当前的queue的信息进行更新:
如果当前queue校准成功,切queue_cur->was_fuzzed = 0,则设置queue_cur->was_fuzzed = 1.然后更新was_fuzzed_map里的有关当前queue的信息,如果当前queue是favored的,则pending_favored减一。(同一个testcase,经历的fuzz次数越多,价值越少)
最后做一些缓冲区和数据结构的清理,然后返回ret_val.
#endif /* !IGNORE_FINDS */
ret_val = 0;
abandon_entry:
splicing_with = -1;
/* Update pending_not_fuzzed count if we made it through the calibration
cycle and have not seen this entry before. */
if (!stop_soon && !queue_cur->cal_failed && !queue_cur->was_fuzzed) {
queue_cur->was_fuzzed = 1;
was_fuzzed_map[get_state_index(target_state_id)][queue_cur->index] = 1;
pending_not_fuzzed--;
if (queue_cur->favored) pending_favored--;
}
//munmap(orig_in, queue_cur->len);
ck_free(orig_in);
if (in_buf != orig_in) ck_free(in_buf);
ck_free(out_buf);
ck_free(eff_map);
delete_kl_messages(kl_messages);
return ret_val;
fuzz_one函数到此结束。