PathAFL: Path-Coverage Assisted Fuzzing
文章来自 ASIA CCS2020
作者来自南开大学
Abstract
提出了PathAFL,有效地识别和利用 h-path,h-path 是一种边已经被覆盖过的新路径(覆盖了相同的边,但是路径不同)。PathAFL只插入一条汇编指令计算路径的哈希值,并使用选择性插桩策略降低执行路径的跟踪粒度。其次,设计了一种快速过滤算法,以从大量的h-path 中选择更高权重的路径,并将其添加到种子队列中。第三,种子选择算法和功率调度都是基于路径权重实现的。发现了6个CVE。
1. Introduction
PathAFL 在编译时使用静态插桩来跟踪程序执行路径并计算路径哈希以区分不同的h-path。PathAFL仅通过跟踪较大的函数和内存操作函数来进行选择性插桩。该方法减少了实际被跟踪的路径数量。该解决方案在跟踪路径粒度和模糊性能之间取得了平衡。
文章共享:
-
阐述了AFL使用边覆盖的缺陷,讨论了路径覆盖的问题,在跟踪路径覆盖粒度和模糊性能之间进行了权衡。
-
设计了一种路径过滤算法,只有那些满足特定条件并拥有高权重的路径才会被添加到种子队列中。
-
基于路径权重来改进AFL,添加了基于路径权重的种子调度。
-
开源
2. Overview
2.1 Overview of AFL
2.1.1 seed selection
2.1.2 coverage calculate
2.2 Overview of CollAFL
2.3 Motivation of PathAFL
2.3.1 BBL Coverage
2.3.2Edge Coverage
一个例子
void vul (short ∗s) {
if(s[0] == 0x6261) // ab
s[2] = 0x6665; // ef
if(s[1] == 0x6463 ) // cd
if(((int ∗)s)[1] == 0x21216665 ) // ef!!
abort() ;
}
上面的程序有6个基本块,7条边,6条执行路径
先执行 A、C、G,再执行 A、B、C、G,再执行 A、C、E、G,则执行 A、B、C、E、G时将不会认为是新的覆盖,所以很难通过变异执行到F。
2.3.3 Path Coverage
将路径分为两类:
- e-path:路径覆盖到了新的边
- h-path:路径没有覆盖新的边,但是哈希值不同,即边的执行顺序或是数量有变化。
e-path比较好处理,因为比较少。但是h-path很多,所以作者仅将权重高的h-path添加到队列中。
3. Technology
3.1 How to Identify h-paths?
最直观的方法是记录整个程序执行流程,即基本块的执行序列,但是,当执行路径太长时,它将花费大量内存。所以作者采用路径哈希,类似于AFL的运行时计算哈希值并存入共享内存。
3.2 How to Reduce the Number of h-paths?
3.2.1 Selective Instrumentation
Path仅插桩部分基本块,并跟踪具有较高概率到达新基本块或触发新bug的路径
- 要大致跟踪整个路径,应在执行路径中均匀分布插桩位置。插桩函数输入是一个不错的选择并且容易实现。
- 插桩较大的函数,即代码量更多的函数,因为有更多的机会触发bug
- 插桩具有更多内存操作的函数
具体的插桩选择策略:
- 插桩20%最大的函数
- 插桩具有内存操作的函数
- 忽略掉太小的函数
- 插桩其它函数的10%
3.2.2 Hash Algorithm
h a s h _ p a t h ( p ) = ∑ b b ∈ B S B I D ( b b ) hash\_path(p)=\sum_{bb\in BS}BID(bb) hash_path(p)=bb∈BS∑BID(bb)
p表示执行路径,BS表示插桩基本块的执行序列。就是简单的将所有基本块的ID相加。
3.3. Which h-paths Will Be Added to the Seed Queue?
策略需求:
- 效率
- e-path 比 h-path 更重要
- 变异后覆盖到新边的h-path应该被选择
路经过滤算法:
- e-path 直接加入到队列中
- 如果当前队列中的种子数量少于
MIN_PATHS
,路径不加入队列。因为在初始阶段,模糊器应该先将大量的e-path 加入队列,h-path 不必加入队列。如果没有足够多的路径,则很难判断一条新路径的好坏。 - 为了保持 h-path 的多样性, 三个连续的 h-path 是不允许出现的, 因为它们通常是通过同一种子变异产生的,执行路径会很相似。
CalcWight
最后调用,因为有计算开销。
权重计算:
w
e
i
g
h
t
_
e
d
g
e
(
<
b
b
1
,
b
b
2
>
)
=
I
s
U
n
t
o
u
c
h
e
d
(
<
b
b
1
,
b
b
2
>
)
?
(
1
+
C
o
u
n
t
C
a
l
l
(
b
b
2
)
∗
2
)
:
0
weight\_edge(<bb1,bb2>)=IsUntouched(<bb1,bb2>)? (1+CountCall(bb2)*2):0
weight_edge(<bb1,bb2>)=IsUntouched(<bb1,bb2>)?(1+CountCall(bb2)∗2):0
w e i g h t _ p a t h = ∑ b b ∈ p , < b b , b b i > ∈ E w e i g h t _ e d g e ( < b b . b b i > ) weight\_path=\sum_{bb \in p, <bb,bb_i> \in E}weight\_edge(<bb.bb_i>) weight_path=bb∈p,<bb,bbi>∈E∑weight_edge(<bb.bbi>)
CountCall
返回 bb1 中 call 指令的调用次数
IsUntouched
检查是否是新的覆盖
E 表示边的集合
当路径权重大于 wight_high 时加入队列
w
e
i
g
h
t
_
h
i
g
h
=
w
e
i
g
h
t
_
a
v
g
+
(
w
i
g
h
t
_
m
a
x
−
w
e
i
g
h
t
_
a
v
g
)
/
w
weight\_high=weight\_avg+(wight\_max-weight\_avg)/w
weight_high=weight_avg+(wight_max−weight_avg)/w
weight_avg
表示队列中所有种子的平均权重
weight_max
表示最大权重
w
是一个平衡参数,默认为3
很显然,wight_high是一个大于平均权重的阈值,这确保了添加到种子队列的 h-path 具有足够的邻居分支(没理解后半句)
3.4 Seed Selection Algorithm
Q = q 0 , q 1 , . . . , q i , . . . , q , . . . , q n Q={q_0,q_1,...,q_i,...,q,...,q_n} Q=q0,q1,...,qi,...,q,...,qn
AFL中存在的问题:上面为一个种子队列,q 表示当前正在fuzz的种子,如果在循环开始时F (偏爱种子集合)中不包含 q i q_i qi,但是现在又包含了 q i q_i qi,则 q i q_i qi覆盖道德的唯一边会被忽略,导致v欸测试的偏爱种子不能包含所有的发现边,这种情况还很常见。(没看懂!!!)
设计了新的种子选择算法
(1)将种子对列根据当前位置分为 Q 1 Q_1 Q1 和 Q 2 Q_2 Q2
(2)更新全局位图 C
(3)将 Q 2 Q_2 Q2 中的种子按照权重降序排序
(4)遍历 Q 2 Q_2 Q2 ,有新覆盖加入到 F 中,否则从 F 中移除
3.5 Power Schedule
能量调度决定一个种子产生测试用例的数量。PathAFL给权重高的种子分配更多能量。具体地,根据权重分成六个部分,每一部分分配的能量都是不同的。
4. Implementation
Instrumentation
为了更准确的计算路径权重,实现了 CollAFL中的 Fsingle 和 Fmul,没有实现 Fhash 是因为使用频率太低。实现了CollAFL中的未触及邻居分支权重计算方法,作为baseline。
Static Analysis
使用IDA进行静态分析。编写了IDAPython脚本,用于自动分析程序结构,获取边缘信息,并为目标程序生成数据文件。该文件记录所有边缘信息,作为PathAFL的输入来计算路径权重。
Path Filtering
算法2 在 afl-fuzz.c 中的 svae_if_interesting
函数中实现,每发现一个种子会计算参数 wight_high
Power Schedule
添加了参数选项 -p 决定是否开启能量分配策略
5. Evaluation
总体来讲就是覆盖了有明显提升,也发现了bug。没有比较吞吐量的实验,感觉应该会比原生AFL差一些。
6. Discussion
**Tracing Path:**仅插桩一部分函数,具有随机性,可能会错过一些重要路径。
**Path Hash:**使用简单的加法计算路径哈希值,无法区分边的执行顺序。
Filtering Path: h-path 有待优化 ,当前是仅根据权重进行选择。
二、源码分析
论文中给的GitHub上的代码不是很全!
1. 新增参数选项
- -p:开启能量分配
- -r:指定队列中h-path 最高占比
- -b:开始将h-path 加入队列中的阈值
- -w:指定计算权重阈值的平衡参数
g_weight_high_param
- -c:连续的h-path 加入队列的最大限制次数
- -h:指定记录基本块父子关系信息的输入文件位置
- -g:权重分配指导策略(0、1、2)
2. 一些变量
path_hash
:queue_entry结构体新增属性,当前种子的路径哈希
next_hash
:queue_entry结构体新增属性,一个链表,存储下一个路径哈希相同的种子
g_new_paths_ratio
:队列中新路径的占比上限
g_new_path
:当前路径的哈希值
g_consecutive_pat
:连续放入队列的 h_path 数量
g_param_consecutive_pat
:连续放入队列的 h_path 数量上限
g_path_hash
:结构体数组,其实是一个存储路径哈希值的哈希表。用结构体数组应该是由足够多的位,保证不会溢出。
struct edge_neighbor
{ 存储边信息的结构体
hash
:边哈希
hash_neighbor
:邻居分支哈希
num_call
:邻居分支数量
num_mem
:内存操作数量
};
g_edge_info
:所有的边信息
g_edge_info_num
:g_edge_info
中的元素数量
g_edge_info_index
:记录每条边对应文件中的第几个条目
g_guide_type
:权重类型,0表示只计算后代,1表示计算邻居分支,2表示计算邻居分支+内存操作
g_begin_num
:将 h_path 添加到队列中的阈值,大于才开始添加。如果为0,不过滤,全部添加
g_weight_high_param
:计算权重阈值的平衡参数
g_nb_count
:记录每条边邻居分支数量的数组
g_weight_high
:权重阈值,大于阈值才会加入队列
g_weight_avg
:队列中种子的权重平均值
3. save_if_interesting 修改
static u8 save_if_interesting(char** argv, void* mem, u32 len, u8 fault) {
u8 *fn = "";
u8 hnb;
s32 fd;
u8 keeping = 0, res;
if (fault == crash_mode) {
/* Keep only if there are new bits in the map, add to queue for
future fuzzing, etc. */
if (!(hnb = has_new_bits(virgin_bits))) {
if (crash_mode) total_crashes++;
// 没有覆盖新的边,判断是否是新的路径
#ifdef _1_PATH_HASH
if ( g_begin_num )
if ( FAULT_TMOUT == fault // 超时造成路径不准确,提前判断
|| queue_cur->var_behavior // 可变种子,直接跳过
|| g_consecutive_pat >= g_param_consecutive_pat // 3 consecutive pat files.
|| queued_paths < g_begin_num // 队列中的数量少于阈值
// 超过队列的1/3
|| g_new_paths >= queued_paths * g_new_paths_ratio / (100+g_new_paths_ratio)
// || get_cur_ms() - last_path_time < 3000 // need last org afl path time
// || R(100) < 67
)
return 0;
if ( !has_new_path() ) return 0; // 如果不是新路径
// 如果 g_begin_num 不为0,即过滤,并且有邻居分支的信息
if ( g_begin_num && g_edge_info_num)
if ( calc_cur_path_neighbor() < g_weight_high ) // 路径权重小于阈值
return 0;
++g_new_paths; // 计数加1
hnb = 3;
#else
return 0;
#endif
}
......
#ifdef _1_PATH_HASH // 保存路径哈希
queue_top->path_hash = *(u32*)(trace_bits + MAP_SIZE) & (MAP_SIZE - 1);
#endif
queue_top->exec_cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);
......
4. has_new_path 判断是否是新路径
static inline u8 has_new_path(/*u32 cksum*/) {
// 最后四字节按位与上全1计算出路径哈希值,没看出来按位与有什么意义
u32 path_hash = *(u32*)(trace_bits + MAP_SIZE) & (MAP_SIZE - 1);
if (!g_path_hash[path_hash]) // 如果是新路径
return 1;
return 0; // 否则,不是新路径
}
5. calc_cur_path_neighbor 计算当前路径的权重
static u64 calc_cur_path_neighbor() {
u64 num = 0;
// 其实trace_bits代表每一条边,如果表示基本块,则会有重复的,因为有些块有多个传入边。
for(u32 i = 0; i < MAP_SIZE; ++i) // 遍历每一个基本块
if( trace_bits[i] ) // 如果覆盖到了
num += g_nb_count[i]; // 累加邻居分支数量,重复的基本块也累加进来
return num;
}
6. load_edge_neighbor_file 加载邻居分支信息文件
static int load_edge_neighbor_file(char *fn) {
struct stat statbuf;
if (stat(fn, &statbuf)) PFATAL("Unable to stat '%s'", fn); // 文件状态复制到statbuf中
s32 fd = open(fn, O_RDONLY);
if (fd < 0) PFATAL("Unable to open '%s'", fn);
g_edge_info = (struct edge_neighbor*)ck_alloc(statbuf.st_size); // 分配空间
ck_read(fd, g_edge_info, statbuf.st_size, fn); // 读取文件内容至内存
close(fd);
memset(g_edge_info_index, 0xFF, sizeof(g_edge_info_index)); // 第一个元素初始化全1
g_edge_info_num = statbuf.st_size / sizeof(struct edge_neighbor); // 包含了多少条边信息
// 记录每条边对应文件中的第几个条目
u32 last_hash = g_edge_info[0].hash;
g_edge_info_index[last_hash] = 0;
for(int i=1; i<g_edge_info_num; ++i){
if(g_edge_info[i].hash == last_hash) // 因为是按照边的哈希值排好序的,所以与前一个比较进行去重
continue;
last_hash = g_edge_info[i].hash;
g_edge_info_index[last_hash] = i;
}
return 0;
}
7. cull_queue 精简队列
static void cull_queue(void) {
struct queue_entry* q;
static u8 temp_v[MAP_SIZE >> 3];
u32 i, j;
if (dumb_mode || !score_changed || !queue_cur) return;
score_changed = 0;
memset(temp_v, 255, MAP_SIZE >> 3);
queued_favored = 0;
pending_favored = 0;
// PATHAFL
// 当前种子不为null,有邻居分支信息,队列中种子数量大于阈值
if (queue_cur && g_edge_info_num && queued_paths > g_begin_num) {
u32 flag = 0;
u32 max = 0;
u32 sum = 0;
u32 count = 0;
static u64 time_cumulative = 0;
u64 time_start = get_cur_time();
for (q = queue; q != queue_cur; q = q->next) { // Q1
if (!q->favored)
continue;
// set temp_v to zero for all the edges that the q covers
j = MAP_SIZE >> 3;
while (j--)
if (q->trace_mini[j])
temp_v[j] &= ~q->trace_mini[j];
}
struct queue_entry **array_entry = (struct queue_entry**)ck_alloc(sizeof(struct queue_entry*)
* (queued_paths + g_new_paths) ); // 数组
u32 count_array = 0;
update_neighbor_count();
for (q = queue_cur; q; q = q->next) { // Q2
q->favored = 0;
q->neigbhor = calc_neighbor(q); // 根据邻居分支计算权重
sum += q->neigbhor; // 累加
if (q->neigbhor > max) // 最大值
max = q->neigbhor;
array_entry[count++] = q;
}
if (count) {
if ( count > (queued_paths>>3) ) { // Q2中的种子数量大于队列中种子总数的1/8
flag = sum / count; // 平均权重
//flag += (max - flag) / 5;
g_weight_avg = flag;
g_weight_high = flag + (max - flag) / g_weight_high_param; // 权重阈值
}
AFL_LOG("cur=%d count=%d avg=%d high=%d ", queue_cur->id, count, g_weight_avg, g_weight_high);
count_array = count;
// 按照权重降序排序
qsort(array_entry, count_array, sizeof(struct queue_entry*), compare_neigbhor_score);
count = 0;
u32 untounch_edge = MAP_SIZE - count_non_255_bytes(virgin_bits); // 未覆盖到的边数
for (i = 0; i < count_array; i++) {
q = array_entry[i];
if (!q->trace_mini)
continue;
j = MAP_SIZE >> 3;
while (j--) // 有新的覆盖
if ( (temp_v[j] & ~q->trace_mini[j]) != temp_v[j] ) { // has new covarage, set favored
q->favored = 1; ++j; // 置为偏爱的种子
break;
}
if ( !q->favored ) continue;
while (j--)
temp_v[j] &= ~q->trace_mini[j]; // 同步当前偏爱种子的所有覆盖
queued_favored++;
if (!q->was_fuzzed) ++pending_favored;
if (++count <= 3 || i >= count_array - 3)
AFL_LOG("%d-%d ", q->id, q->neigbhor);
if ( (i & 0xF) == 0xF && count_bits(temp_v, sizeof(temp_v)) == untounch_edge){
break;
}
}
ck_free(array_entry);
if (count_bits(temp_v, sizeof(temp_v)) != untounch_edge) // 如果覆盖了新的边
AFL_LOG("virgin=%d temp_v=%d ", untounch_edge, count_bits(temp_v, sizeof(temp_v)));
time_cumulative += get_cur_time() - time_start;
AFL_LOG("i=%d fav=%d time=%llds\n", i, count, time_cumulative / 1000);
}
} else { // 否则,采用AFL默认的策略
q = queue;
while (q) {
q->favored = 0;
q = q->next;
}
/* Let's see if anything in the bitmap isn't captured in temp_v.
If yes, and if it has a top_rated[] contender, let's use it. */
for (i = 0; i < MAP_SIZE; i++)
if (top_rated[i] && (temp_v[i >> 3] & (1 << (i & 7)))) {
u32 j = MAP_SIZE >> 3;
/* Remove all bits belonging to the current entry from temp_v. */
while (j--)
if (top_rated[i]->trace_mini[j])
temp_v[j] &= ~top_rated[i]->trace_mini[j];
top_rated[i]->favored = 1;
queued_favored++;
if (!top_rated[i]->was_fuzzed) pending_favored++;
}
} // end of if (queue_cur && g_edge_info_num)
q = queue;
while (q) {
mark_as_redundant(q, !q->favored);
q = q->next;
}
}
8. calc_edge_neighbor 根据边哈希计算邻居分支数量
static inline u32 calc_edge_neighbor(u32 hash) {
u32 score = 0;
// 根据边哈希值找到当前条目
for (u32 j = g_edge_info_index[hash]; j < g_edge_info_num; ++j) {
//index is over
if (g_edge_info[j].hash != hash) // 一个边哈希值可能存在多个条目,
break;
// 邻居已经被覆盖过了,不加分,直接跳过
if ( 0xFF != virgin_bits[g_edge_info[j].hash_neighbor] )
continue;
if (0 == g_guide_type){ //
score += 1;
} else if (1 == g_guide_type) { // 邻居分支指导权重
score += 1 + (g_edge_info[j].num_call << 1);
} else if (2 == g_guide_type) { // 邻居分支+内存操作指导权重
score += 1 + (g_edge_info[j].num_call << 1) + (g_edge_info[j].num_mem << 2);
}
}
return score;
}
9. calc_neighbor 统计当前种子的权重
static u64 calc_neighbor(struct queue_entry *q) {
u64 num = 0;
if(!q->trace_mini) return num; // 不是优秀种子,直接返回,感觉有点多余
for(u32 i = 0; i < MAP_SIZE; ++i)
if( q->trace_mini[i>>3] & (1<<(i&7)) ) // 如果覆盖到了当前边
num += g_nb_count[i]; // 累加上当前边的邻居数量
return num;
}
10. compare_neigbhor_score 传给qsort的比较函数
static int compare_neigbhor_score(const void* p1, const void* p2) {
// 注意参数是 void* 类型,所以要强制类型转换后解引用
return (*(struct queue_entry**)p2)->neigbhor - (*(struct queue_entry**)p1)->neigbhor;
}
11. update_bitmap_score 修改
static void update_bitmap_score(struct queue_entry* q) {
u32 i;
u64 fav_factor = q->exec_us * q->len;
/* For every byte set in trace_bits[], see if there is a previous winner,
and how it compares to us. */
for (i = 0; i < MAP_SIZE; i++)
if (trace_bits[i]) {
if (top_rated[i]) {
/* Faster-executing or smaller test cases are favored. */
if (fav_factor > top_rated[i]->exec_us * top_rated[i]->len) continue;
/* Looks like we're going to win. Decrease ref count for the
previous winner, discard its trace_bits[] if necessary. */
if (!--top_rated[i]->tc_ref) {
// 这里没有清空释放trace_mini
}
}
/* Insert ourselves as the new winner. */
top_rated[i] = q;
q->tc_ref++;
score_changed = 1;
}
#if (defined _2_GUIDED_NEIGHBOR)
// 改成了在判断条件外分配并计算trace_mini,也就是不管是不是top_rated都会计算,队列中的所有种子都会计算出trace_mini
if (!q->trace_mini) {
q->trace_mini = ck_alloc(MAP_SIZE >> 3);
minimize_bits(q->trace_mini, trace_bits);
}
#endif
}