文章来自USENIX2020
作者来自国防科技大学
文章
AFL分配过多的能量(种子生成的测试用例数量)给执行高频路径的种子,并且无法自适应地调整能量分配,因此浪费了大量的能量。此外,AFLFast中的马尔可夫链模型还不够深刻。本文采用一种对抗性多臂老虎机的变体对AFL的能量调度建模,阐述了种子集的三种状态,并开发了自适应调度算法和基于概率的搜索策略。扩展AFL实现了自适应节能灰盒模糊测试器EcoFuzz,覆盖率为AFL的214%,测试用例的生成数量减少32%,发现了12个软件漏洞和一个物联网设备漏洞。
AFLFast实现了单调的能量调度,但是不能根据fuzz过程灵活地调整能量分配策略,从而增加了发现新路径的成本,并且无法提供对转移概率的详细分析。无法计算已覆盖路径到未覆盖路径的转移概率。实际上,选择种子以及分配能量是博弈论中”探索与利用“的权衡问题,而不是一个简单的概率问题。
贡献如下:
讲了影响AFL效率的几个因素:种子搜索策略,变异策略和能量调度。
简述了AFLFast的工作,具体可见该篇阅读报告。
AFLFast阅读报告
多臂老虎机参考
在经典的多臂老虎机(MAB)问题中有两个假设:每个操作臂的奖励分布是时间无关的,并且操作臂的数量是恒定的。但是将CFG建模为MAB模型,将每个种子视为一个操作臂时,种子的数量是增加的,并且发现新路径的概率(即奖励概率)在降低。所以采用MAB的变体Adversarial Multi-ArmedBandit(AMAB)对CFG建模。
假设正在Fuzz程序A
Assumption 1: 程序A可以执行的路径和崩溃是有限的,分别表示为 n p n_p np和 n c n_c nc 。
Assumption 2: 程序A是无状态的,即每次执行的路径仅取决于输入。
假设2保证奖励概率是独立的,只有种子决定。
Definition 1: 程序A的所有路径集合为 S = 1 , 2 , … , n p S={1,2,…,n_p} S=1,2,…,np,相应的种子集为 T = t 1 , t 2 , … , t n p T={t_1,t_2,…,t_{n_p}} T=t1,t2,…,tnp .
Definition 2: 转移概率 p i , j p_{i,j} pi,j,执行路径 i 的种子 t i t_i ti 生成执行路径 j 的测试用例的概率。 E [ X i , j ] E[X_{i,j}] E[Xi,j]为最小能量期望,为 1 / p i , j 1/p_{i,j} 1/pi,j。
Definition 3: 转移频率 f i , j f_{i,j} fi,j ,从路径 i 转移到路径 j 的频率,为 f i , j = f i ( j ) s ( i ) f_{i,j}={f_i(j) \over s(i)} fi,j=s(i)fi(j) 。 f i ( j ) f_i(j) fi(j) 表示种子 t i t_i ti 生成执行路径 j 的测试用例数量, S ( i ) S(i) S(i) 表示对种子 t i t_i ti 实验(变异)次数。 f i , i f_{i,i} fi,i 表示自转移频率。
S ( i ) = ∑ j = 1 n p f i ( j ) S(i)= \sum _{j=1}^{n_p}f_i(j) S(i)=j=1∑npfi(j)
Definition 4: 变异种子 t i t_i ti 生成执行其它路径(即不包括路径 i)的概率表示为 p i ∗ p_{i*} pi∗
p i ∗ = 1 − p i , i = ∑ j = 1 n p p i , j − p i , i = ∑ j = 1 , j ≠ i n p p i , j p_{i*} =1-p_{i,i}= \sum _{j=1}^{n_p}p_{i,j} - p_{i,i} = \sum _{j=1,j \ne i}^{n_p}p_{i,j} pi∗=1−pi,i=j=1∑nppi,j−pi,i=j=1,j=i∑nppi,j
n个种子的队列表示为 T n T_n Tn ,队列中被fuzz过的种子为 T n + T_n^+ Tn+, 剩下的为 T n − T_n^- Tn− ,目前为止变异次数为 m
每个种子对应一个操作臂,旨在在有限试验中最大化路径覆盖率,选择操作臂代表变异对应的种子 t i t_i ti。
假设当前已经变异了m次,下一次变异获得的奖励为 R i ( m + 1 , T n ) = 1 R_i(m+1, T_n) = 1 Ri(m+1,Tn)=1
操作臂 i 在一次变异中获得奖励的概率为:
P ( R i ( m + 1 , T n ) = 1 ) = ∑ j = n + 1 n p p i , j = 1 − ∑ j = 1 n p i , j P(R_i(m+1, T_n)=1)= \sum _{j=n+1}^{n_p}p_{i,j} = 1 - \sum _{j=1}^{n}p_{i,j} P(Ri(m+1,Tn)=1)=j=n+1∑nppi,j=1−j=1∑npi,j
(这里 [0, n] 表示已发现的路径,[n+1, np] 表示未发现的路径)上式表示奖励概率只取决于当前种子 t i t_i ti 以及已经发现的路径,跟变异次数无关。
可以看到奖励分布是变化的,一旦获得了一个奖励,奖励概率就会下降,称为概率衰减。 队列中的种子数量是增加的,即操作臂数量增加。
种子队列 T n T_n Tn 的状态可以分为三类:
总的来说,在初始状态和探索状态Fuzz每一个种子,在利用状态Fuzz奖励概率最高的种子。队列状态转换图如下:
P R i , n ≈ 1 − ∑ j = 1 n f i , j = 1 − ∑ j = 1 n f i ( j ) s ( i ) P_{R_{i,n}} \approx 1- \sum _{j=1}^{n}f_{i,j} = 1 - \sum _{j=1}^{n}{f_i(j) \over s(i)} PRi,n≈1−j=1∑nfi,j=1−j=1∑ns(i)fi(j)
为了最大化路径覆盖,需要建立有效的机制,该机制利用现有信息来估计每个种子在利用阶段奖励概率以搜索种子,并为种子分配适当的能量以减少浪费。
开发了调度算法AAPS和搜索策略SPEM,加入了状态确定机制。需要注意的是,EcoFuzz没有确定性变异,还去掉了发现新路径是能量加倍的机制。
框架如下图:
算法流程如下图:
需要注意的是,探索阶段的种子选择是按照队列中的顺序,利用阶段的种子选择才用到SPEM。另外,利用阶段,在所有的种子都被选择过或发现新路径之前,每个种子只能被选择一次,而当所有种子都被选择一次后还没发现新路径会重新选择种子直到发现新路径。
此外,作者添加了一个静态分析模块,用于提取某些程序中的魔法字节加入到字典,该模块通过静态分析实现,效率高且不复杂。
优于选择奖励概率高的种子,所以只用关注奖励概率的大小关系而不是具体的值。
P R i , n = p i ∗ − ∑ j = 1 , j ≠ i n p i , j P_{R_{i,n}} = p_{i*} - \sum _{j=1,j \ne i}^{n}p_{i,j} PRi,n=pi∗−j=1,j=i∑npi,j
用 1 − f i i 1-f_{ii} 1−fii 近似代替 p i ∗ p_{i*} pi∗ (频率代替概率),但是 ∑ j = 1 , j ≠ i n p i , j \sum _{j=1,j \ne i}^{n}p_{i,j} ∑j=1,j=inpi,j 会概率衰减,并且种子被发现的越早,衰减越多,因此用种子的索引值定性地表明概率衰减,估算方法如下:
P R i , n ≈ 1 − f i i i P_{R_{i,n}} \approx 1- {f_{ii} \over \sqrt i} PRi,n≈1−ifii
上式表明自转移频率低地种子以及索引值大的种子奖励概率更高。但是只能估计奖励概率的大小关系,不能确定所选种子的最小能量,为此,提出了一种基于平均成本的自适应能量调度算法。
由于无法计算发现新路径的最低能量,因此开发了一种调度算法来单调近似。
下图为执行次数和路径数量的关系图”
曲线 s 表示两者之间的关系,可以看到倒数在逐渐下降。点(e1, p1)表明执行 e1 次后发现了 (p1 - p0)条路径,则平均成本为
C ( p 1 , e 1 , p 0 ) = e 1 p 1 − p 0 C(p_1,e_1,p_0)={e_1 \over {p_1 - p_0}} C(p1,e1,p0)=p1−p0e1
表明已经执行 e1 次后,平均发现一条新路径需要的执行次数,也是上图中 L3斜率的倒数。注意,平均成本随着执行次数的增加而降低(说反了吧,平均成本随着执行次数的增加而增加)。
所欲用平均成本作为能量分配的基准。
对于种子 s ,在探索阶段为其分配的能量不超过平均成本。此外,执行高频路径的种子分配的能量更少,通过算法中的CalculateCoefficient()
函数实现,执行种子s的路径的测试用例数量比上 average_cost 得到比例 r,在三个区间内(0, 0.5],(0.5, 1], (1, +∞),对应的k分别等于1,0.5,0.25。
工作中还引入了经典MAB问题中的regret(懊悔?不知道准确的翻译)概念。regret机制用来调整能量利用系数,如果分配的能量超过发现路径所需的能量,则会减少下次分配的能量。懊悔系数取值在[0,1],越接近于1懊悔程度越低,越接近于0,懊悔程度越高,为0时表示未发现任何新路径。具体为最后一次发现新路径时所用能量比上本次分配的能量。
为了避免利用阶段过多的能量浪费,M设置为一轮中能量分配的最大值。
14个程序,每个程序喂一个初始种子。与AFL、FidgetyAFL、AFLFast、AFLFast.new、FairFuzz、MOPT-AFL对比。
评估指标:发现的路径数量、生成的测试用例数量、平均成本(平均发现每条路径需要的执行次数)
EcoFuzz在绝大部分程序中发现的路径数量多余其它模糊器,在超过半数的程序中使用最少的执行次数发现最多的路径。
在大多数程序中明显低于其它模糊器的平均成本,比EcoFuzz平均成本低的发现的路径也比它少,发现路径比EcoFuzz多的平均成本比它高。
EcoFuzz与AFLFast.new的覆盖水平差不多,但能量消耗和平均成本明显更低。
评估指标:能量利用率(每轮中查找最新路径所消耗的能量与分配的总能量之比)分为平均利用率和有效分配。分配的次数表示为 i ,范围为[1, N],利用率为 r i r_i ri,本次发现路径数量为 n i n_i ni,平均利用率为
r ˉ = ∑ i = 1 i = N r i N \bar r = {\sum _{i=1}^{i=N}r_i \over N} rˉ=N∑i=1i=Nri
有效分配为发现新路径的分配的频率
p = ∣ { i ∣ n i > 0 , 1 ≤ i ≤ N } ∣ N p = {|\{i|n_i>0,1 \leq i \leq N \}| \over N} p=N∣{i∣ni>0,1≤i≤N}∣
比较的指标都挺细挺特别的。
主要写了在GNU Binutils中发现的漏洞
EcoFuzz表现最好
当测试用例的执行速度很慢并且路径上限较低时(例如用QEMU对IoT设备会使二进程程序进行测试),EcoFuzz的优势更加突出。在SNMP(简单网络管理协议)中发现了一个漏洞。
浅谈了优势,能够和一些优化AFL的技术结合(如CollAFL),也能够集成到其它CGF中。
struct queue_entry {
u8 state, // 是否在状态2下被fuzz过
last_found, // 上一次fuzz发现的路径数量(种子数量)
abandon;
u32 chose_time, // 被fuzz次数
serial; // 序号
u64 exec_num, // 该种子覆盖路径被执行次数
last_energy, // 上一次fuzz时生成的测试用例数量
mutation_num, // 对该种子的总变异次数
exec_by_mutation, // 变异出执行与自己相同路径的测试用例数量
power; //
}
u8 state_of_fuzz; // 队列所处阶段(0=初始,1=探索,2=利用)
u32 queued_paths_initial; // 初始种子数量
u32 calculate_coe = 0 // 计算率系数
u64 total_fuzz; // 总的fuzz次数
float rate = 1; // 懊悔率
static void schedule(void) {
struct queue_entry *q, *p;
u32 i = 0;
q = queue;
p = queue;
while(q){ // 选择下一个没被fuzz的种子
if(!q->was_fuzzed && queue_cur->was_fuzzed){
queue_cur = q;
current_entry = i;
break;
}
q = q->next;
i++;
}
// 判断所处阶段
q = queue;
while(q) {
if(!q->was_fuzzed){ // 有未被fuzz的种子,阶段为1,直接返回
state_of_fuzz = 1;
return;
}
q = q->next;
}
state_of_fuzz = 2; // 都被fuzz过,阶段为2
q = queue;
p = queue;
i = 0;
while(p){ // 找到第一个没在利用阶段被fuzz过的种子
if(p->state != 2)
break;
i++;
p = p->next;
}
// 如果都在利用阶段被fuzz过了
if(!p){
p = queue;
while(p){ // 重置所有种子状态,重新选择种子
p->state = 0;
p = p->next;
}
p = queue;
i = 0;
}
// 执行到这里p指向队列中第一个state!=2的种子,即第一个未在利用阶段被fuzz过的种子
// SPEM,选择 自转移概率/序号开方 最小的 并且 未在利用阶段被fuzz过的 种子
while(q){ // exec_by_mutation/mutation_num 自转移概率
if(q->exec_by_mutation * p->mutation_num * get_sqrt(p->serial) < p->exec_by_mutation * q->mutation_num * get_sqrt(q->serial) && q->state == 0){
p = q;
current_entry = i;
}
q = q->next;
i++;
}
queue_cur = p;
}
// 删除了有一定概率跳过当前种子的机制
/*******************************************
* CALIBRATION (only if failed earlier on) *
*******************************************/
...
/************
* TRIMMING *
************/
...
queue_cur->chose_time++; // 当前种子被fuzz次数加1
/*********************
* PERFORMANCE SCORE *
*********************/
orig_perf = perf_score = calculate_score(queue_cur);
// 计算平均成本
if(queued_paths == queued_paths_initial) // 如果只有初始种子
average_cost = total_fuzz / queued_paths;
else // 否则,总的fuzz次数 / 发现的路径数量
average_cost = total_fuzz / (queued_paths - queued_paths_initial);
if(average_cost == 0)
average_cost = 1024; // 下限
if(state_of_fuzz == 2){ // 如果在利用阶段
queue_cur->state = 2; // 已在利用阶段被fuzz标志
if(queue_cur->last_found == 0){ // 上一次没发现新路径或是第一次被fuzz
energy = MIN(2 * queue_cur->last_energy, 16 * average_cost); // 分配更多能量
}
else{
energy = MIN(queue_cur->last_energy, 16 * average_cost);
}
if(energy == 0){
if(queue_cur->exec_num > average_cost) // (1, +∞]
energy = average_cost/4; // k = 0.25
else if(queue_cur->exec_num > average_cost/2) // (0.5, 1]
energy = average_cost/2; // k = 0.5
else // (0, 0.5]
energy = average_cost; // k = 1
}
energy = energy * rate;
}
else{ // 不在利用阶段
if(queue_cur->exec_num > average_cost)
energy = average_cost/4;
else if(queue_cur->exec_num > average_cost/2)
energy = average_cost/2;
else
energy = average_cost;
energy = energy * rate;
}
goto havoc_stage; // 跳过确定性变异
/* Skip right away if -d is given, if we have done deterministic fuzzing on
this entry ourselves (was_fuzzed), or if it has gone through deterministic
testing in earlier, resumed runs (passed_det). */
if (skip_deterministic || queue_cur->passed_det)
goto havoc_stage;
/* Skip deterministic fuzzing if exec path checksum puts this out of scope
for this master instance. */
if (master_max && (queue_cur->exec_cksum % master_max) != master_id - 1)
goto havoc_stage;
if(energy < calculate_power(len)) // 计算确定性变异需要的能量,这里用不到
goto havoc_stage;
...
switch cases;
common_fuzz_stuff();
if (queued_paths != havoc_queued) {
// 本次能量分配中,最后一次发现新路径所用的能量
record_num = stage_cur + 1; //
havoc_queued = queued_paths;
// 计算懊悔系数
regret = (float)record_num / stage_max;
}
// 拼接也去掉了
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(!queue_cur->was_fuzzed){ // 没用到
queue_cur->line = queued_paths;
}
if (!stop_soon && !queue_cur->cal_failed && !queue_cur->was_fuzzed) {
queue_cur->was_fuzzed = 1;
pending_not_fuzzed--;
if (queue_cur->favored) pending_favored--;
}
munmap(orig_in, queue_cur->len);
if (in_buf != orig_in) ck_free(in_buf);
ck_free(out_buf);
ck_free(eff_map);
queue_cur->TS_max = 0; // 没用到
queue_cur->last_found = queued_paths - last_queued_paths; // 发现新路径数量
queue_cur->last_energy = queue_cur->mutation_num - last_mutation_num; // 消耗的能量
if(regret == 0) // 没发现任何新路径,分配更多能量?
regret = 1.1;
rate = ((rate * calculate_coe) + regret)/(calculate_coe + 1); // 计算比率[0.1, 1.5] 越懊悔比率越小
calculate_coe++;
if ( calculate_coe > queued_paths / 100)
calculate_coe = queued_paths / 100;
if ( rate > 1.5 )
rate = 1.5;
else if(rate < 0.1)
rate = 0.1;
end_record = get_cur_time_us();
record_time = end_record - start_record;
write_to_time_file(record_time);
return ret_val;