通过编译器转换(llvm pass)规避模糊测试的一些障碍

这个是看一个文章的记录,算是简译吧

总的来说是通过llvm pass去优化代码的生成去提高AFL的代码覆盖率

AFL通过插桩获取的代码覆盖率,假如变异的样本触发的新的路径,就会加入到接下来的变异队列中

但是有一些情况,会给afl造成极大的障碍

1
2
3
4
5
if (input == 0xabad1dea) {
/* terribly buggy code */
} else {
/* secure code */
}

afl是随机变异的,去变异生成一个完全一样的4字节的数,真是难于上青天

那怎么解决这个问题呢?

假如把上面的代码换成下面那样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (input >> 24 == 0xab){
if ((input & 0xff0000) >> 16 == 0xad) {
if ((input & 0xff00) >> 8 == 0x1d) {
if ((input & 0xff) == 0xea) {
/* terrible code */
goto end;
}
}
}
}

/* good code */

end:

现在一次比较一个字节,大大提升了中奖概率,从1/2^32降低到了1/2^9,即1/512

原文作者根据这个原理,把可能出现这个问题两种情况也写了pass:switch的选项,还有比较函数(memcmp,strcmp …)

LLVM Passes

作者共实现了3个pass

比较指令的:split-compares-pass
strcmp和memcmp的: compare-transform-pass
switch的:split-switches-pass

The split-compares-pass

其实除了有==,还有!=,>,<,>=,<=

首先把>=,<=这种拆分为两个,比如>=,一个是==,另一个是>

再将有符号的比较拆分为,符号位比较和无符号的比较

到这就只剩这四种比较了:<, >, ==, != ,而且都是无符号的,所以再将字符拆分成单字节就好了

The compare-transform-pass

原始

1
2
3
if(!strcmp(directive, "crash")) {
programbug()
}

改成一个一个比较

1
2
3
4
5
6
7
8
if(directive[0] == 'c') {
if(directive[1] == 'r') {
if(directive[2] == 'a') {
if(directive[3] == 's') {
if(directive[4] == 'h') {
if(directive[5] == 0) {
programbug()
}

局限性:就是这个比较是文字字符串并且因此字符串本身及其长度在编译时已知

The split-switches-pass

1
2
3
4
5
6
7
8
9
10
11
int x = userinput();
switch(x) {
case 0x11ff:
/* handle case 0x11ff */
break;
case 0x22ff:
/* handle case 0x22ff */
break;
default:
/* handle default */
}

思想是想转化为if else,之后在通过split-compares-pass来处理,但这可能不是生成最优代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
if (x >> 24 == 0x00){
if ((x & 0xff0000) >> 16 == 0x00) {
if ((x & 0xff00) >> 8 == 0x11) {
if ((x & 0xff) == 0xff) {
/* case 0x11ff */
goto after_switch;
}
goto default_case;
}
goto default_case;
}
goto default_case;
}
else if (x >> 24 == 0x00){
if ((x & 0xff0000) >> 16 == 0x00) {
if ((x & 0xff00) >> 8 == 0x22) {
if ((x & 0xff) == 0xff) {
/* case 0x22ff */
goto after_switch;
}
goto default_case;
}
goto default_case;
}
goto default_case;
}

default_case:
/* default case */

after_switch:

评价

作者还用libpng和harfbuzz对着几个pass进行了测试

Driller的test case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 1 int main(void) {
2 config_t* config = readconfig();
3 if(config == NULL){
4 puts("Configuration syntax error");
5 return 1;
6 }
7 if (config->magic != MAGICNUMBER) {
8 puts("Bad magic number");
9 return 2;
10 }
11 initialize(config);
12
13 char* directive = config->directives[0];
14 if(!strcmp(directive, "crashstring")) {
15 programbug();
16 }
17 else if(!strcmp(directive, "setoption")) {
18 setoption(config->directives[1]);
19 }
20 else{
21 _default();
22 }
23
24 return 0;
25 }

有了pass,1分钟就通过了第7行的检查,60分钟生成了crashstring,但是最后没出来setoption

没有pass,可能在合理的时间都过不了第7行的检查

libpng

作者将afl分为两组

组A:1个master,3个slave,都是正常插桩
组B:1个master,1个slave,都是正常插桩,还有另外的,1个master,1个slave,都是加了pass的

A组发现了1459条路径B找到了2318条路径。

在代码覆盖率方面(使用lcov进行度量的),A组命中了libpng的2186行,而B组命中了2707行

通过看图,可以看到加了pass,比较随便过

harfbuzz

harfbuzz的测试设置与libpng的设置相同,但是测试仅运行了24小时。在测试结束时,A组发现2070条路径,而B组发现2150条路径。在代码覆盖率方面,A组达到3358行,而B组达到3474行,增长了3.5%。

效果还是比较明显的

参考原文

https://lafintel.wordpress.com/2016/08/15/circumventing-fuzzing-roadblocks-with-compiler-transformations/

打赏专区