afl-challenges

本文最后更新于:2023年10月17日 晚上

afl-challenges

这篇文章笔者将通过afl-training的几个示例,来了解如何对真实程序进行fuzz。

libxml2

libxml2库是解析XML文档的函数库。它用 C 语言写成,并且能被多种语言所调用。我们fuzz的目标是找到libxml2的历史CVE。

根据afl-training下载对应版本的libxml2

1
2
3
4
5
git clone https://github.com/GNOME/libxml2.git
cd libxml2
git submodule init # 初始化git子模块
git submodule update # 更新到父仓库指定的提交版本
git checkout v2.9.2 # 切换git分支

进行编译:(编译时python版本要 <= 3.8)

1
2
CC=afl-clang-fast ./autogen.sh
AFL_USE_ASAN=1 make -j$(nproc)

根据官方文档,编写定制的harness

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "libxml/parser.h"
#include "libxml/tree.h"

int main(int argc, char, **argv) {
if (argc != 2){
return(1);
}

xmlInitParser();

xmlDocPtr doc = xmlReadFile(argv[1], NULL, 0);
if (doc != NULL) {
xmlFreeDoc(doc);
}

xmlCleanupParser();

return(0);
}

这里选择libxml2官方提供的几个api进行fuzz。

然后编译。

1
AFL_USE_ASAN=1 afl-clang-fast ./harness.c -I ./libxml2/libxml2/include ./libxml2/libxml2/.libs/libxml2.a -lz -lm -o harness

创建种子文件。

1
2
mkdir fuzz_in
vim testcase.xml

种子文件只要是xml格式即可,不必拘泥于内容。

示例如下:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8" ?>

<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>

启动fuzzer

1
afl-fuzz -i fuzz_in -o fuzz_out -- ./harness @@

这里笔者运气也是有点小衰,跑半天没跑出来洞。

如何提高fuzz的运行效率,主要有以下方式:

  1. 使用更有效地xml样本,可以使用-x参数采用字典的形式。
  2. 使用while (__AFL_LOOP(1000)) { ... }格式,减少不断fork带来的损失。
  3. 并行进行fuzz
  4. 改进harness

这里笔者就不改进harness了,采用字典+并行的方式运行fuzzer

这里的字典是afl官方自带的,使用-m none解除对fuzzer内存的限制,采用四核并行运行的方式。

1
2
3
4
afl-fuzz -i fuzz_in -o fuzz_out -M fuzzer1 -x ~/tools/afl-2.52b/dictionaries/xml.dict -m none -- ./harness @@
afl-fuzz -i fuzz_in -o fuzz_out -S fuzzer2 -x ~/tools/afl-2.52b/dictionaries/xml.dict -m none -- ./harness @@
afl-fuzz -i fuzz_in -o fuzz_out -S fuzzer3 -x ~/tools/afl-2.52b/dictionaries/xml.dict -m none -- ./harness @@
afl-fuzz -i fuzz_in -o fuzz_out -S fuzzer4 -x ~/tools/afl-2.52b/dictionaries/xml.dict -m none -- ./harness @@

这里,笔者跑出了很多的crash,运行harness,简单验证下。

image-20231016210914936

发现是一个堆溢出漏洞,溢出了一个字节。

查看源码。

1
2
3
4
5
6
7
8
9
10
11
...
else
{
xmlFatalErr(ctxt, XML_ERR_XMLDECL_NOT_FINISHED, NULL);
MOVETO_ENDTAG(MOVETO_ENDTAG); // bug
NEXT;
}
...

#define MOVETO_ENDTAG(p) \
while ((*p) && (*(p) != '>')) (p)++

这里应该是MOVETO_ENDTAG宏对指针的检查不严格,造成指针越界了。笔者也不详细分析漏洞了。

笔者又看了几个crash,发现都是一样的漏洞。

这里也就不一个一个看了,使用afl-utils提供的工具afl-collect进行漏洞整理,这个工具可以很方便的对并行的fuzz结果进行整理,并标记漏洞是否可以利用。

1
ASAN_OPTIONS="abort_on_error=1:symbolize=0" afl-collect -d crashes.db -e script -r -rr ./fuzz_out ./collection -j 8 -- ./harness @@

image-20231016221137373

这里一共跑出了67个漏洞,但都是一样的,也就是上面描述的洞。

我们也可以使用afl-cminafl-tmin对结果进行精简。

afl-cmin 最小化输入数量。有些输入能够覆盖的路径是一样的,只需要保存一个就行,可以减少样本数量。

afl-tmin 最小化输入大小。有些样本输入是冗余的,删除部分也可以造成 crash,可以将输入的样本精简。值得一提的是,afl-tmin只能对单个文件进行分析。

使用afl-tmin减少样本数量。

image-20231016222314148

结果与afl-collect一样,仅保留了一个样本。

使用afl-tmin减小样本大小。

image-20231016223112788

可以看到,精简后的样本确实是小了不少。

image-20231016223056242

这里,我们还想找到其它的漏洞,为了避免这个漏洞对我们fuzz功能产生影响。

我们在原始漏洞的基础上打个patch,直接在源码上修改即可。

1
2
3
4
5
6
7
8
9
10
11
12
void __attribute__((no_sanitize_address)) MOVETO_ENDTAG_PATCH(xmlChar *p)
{
while ((*p) && (*(p) != '>')) (p)++;
}

else
{
xmlFatalErr(ctxt, XML_ERR_XMLDECL_NOT_FINISHED, NULL);
// MOVETO_ENDTAG(CUR_PTR);
MOVETO_ENDTAG_PATCH(CUR_PTR);
NEXT;
}

还是一样的过程,编译然后运行fuzzer

这次运行的时间就比较长了,然后也没有找的很多的漏洞。

直接使用afl-collect对crash进行分析。

image-20231016225345397

运行harness进行漏洞验证。

image-20231016230348648

还是一个堆溢出漏洞。

查看源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static unsigned long
xmlDictComputeFastQKey(const xmlChar *prefix, int plen,
const xmlChar *name, int len, int seed)
{
unsigned long value = (unsigned long) seed;

if (plen == 0)
value += 30 * (unsigned long) ':';
else
value += 30 * (*prefix);

if (len > 10) {
value += name[len - (plen + 1 + 1)]; // bug
len = 10;
if (plen > 10)
plen = 10;
}
// ...
}

可以看到,这里并没有对数组下标进行约束,存在数组溢出漏洞。

ntpq

ntpq 是 NTP 参考实现工具套件中的一个实用程序。它查询服务器(如 ntpd)并向用户提供信息。

我们此次的目标是使用fuzzer找到CVE-2009-0159

先下载ntpq源码:

1
2
wget https://www.eecis.udel.edu/~ntp/ntp_spool/ntp4/ntp-4.2/ntp-4.2.2.tar.gz
tar -xvf ntp-4.2.2.tar.gz

然后编译:

1
2
CC=afl-clang-fast ./configure 
AFL_HARDEN=1 make -C ntpq

需要注意的是,我们这次测试的程序并不是简单的从标准输入或者文件中获取输入的,而是一个网络收发包程序。测试这类程序,我们一般会进行隔离测试。通常来说,解析器等目标函数通常可以很容易地进行隔离测试。所以,我们这次采取的措施是对目标函数cookedprint进行针对性的fuzz。其中的 datatype, statusdata 都从 stdin 读入,输出文件为 stdout。

其函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
static void
cookedprint(
int datatype,
int length,
char *data,
int status,
FILE *fp
)
{
// ...
}

这里,我们直接替换ntpq/ntqp.cmain函数,并使用persistent mode加快fuzz进程。

最终形成的harness如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifdef __AFL_HAVE_MANUAL_CONTROL
__AFL_INIT();
#endif
int datatype=0;
int status=0;
char data[1024*16] = {0};
int length=0;
#ifdef __AFL_HAVE_MANUAL_CONTROL
while (__AFL_LOOP(1000)) {
#endif
datatype=0;
status=0;
memset(data,0,1024*16);
read(0, &datatype, 1);
read(0, &status, 1);
length = read(0, data, 1024 * 16);
cookedprint(datatype, length, data, status, stdout);
#ifdef __AFL_HAVE_MANUAL_CONTROL
}
#endif
return 0;

将上述代码写入到main函数中,并重新编译。

1
2
diff -u ntpq.c ntpq-harness.c > harness.patch
patch -p0 < harness.patch
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
32
33
--- ntpq.c	2023-10-17 06:14:09.149302047 -0700
+++ ntpq-harness.c 2023-10-17 06:17:38.522004679 -0700
@@ -488,7 +488,29 @@
char *argv[]
)
{
- return ntpqmain(argc, argv);
+ // return ntpqmain(argc, argv);
+
+ #ifdef __AFL_HAVE_MANUAL_CONTROL
+ __AFL_INIT();
+ #endif
+ int datatype=0;
+ int status=0;
+ char data[1024*16] = {0};
+ int length=0;
+ #ifdef __AFL_HAVE_MANUAL_CONTROL
+ while (__AFL_LOOP(1000)) {
+ #endif
+ datatype=0;
+ status=0;
+ memset(data,0,1024*16);
+ read(0, &datatype, 1);
+ read(0, &status, 1);
+ length = read(0, data, 1024 * 16);
+ cookedprint(datatype, length, data, status, stdout);
+ #ifdef __AFL_HAVE_MANUAL_CONTROL
+ }
+ #endif
+ return 0;
}
#endif

创建种子文件与输出文件。

1
2
3
mkdir fuzz && cd fuzz
mkdir fuzz_in fuzz_out
echo aaaa > fuzz_in/testcase

启动fuzzer,使用字典模式。

1
afl-fuzz -i fuzz_in -o fuzz_out -x ../ntpq.dict -m none -- ./ntpq

跑了将近5分钟,我们就跑出来了上百个crashes了。不得不说,效率真的很高。

image-20231017213347124

我们使用afl-collect对crashes做一个简单的整理。

1
afl-collect -d crashes.db -e script -r -rr ./fuzz_out ./collection -j 8 -- ./ntpq

image-20231017214330283

最终,只剩下了7个crashes。

到目前为止,仅仅出现crashes已经满足不了我们了。我们想查看代码覆盖率(针对cookedprint函数),这个具体要怎么实现呢?

可以使用llvm中对gcov的支持来查看代码覆盖率。

简单来说gcov是一个测试代码覆盖率的工具。与GCCllvm也支持)一起使用来分析程序,以帮助创建更高效、更快的运行代码,并发现程序的未测试部分。这是一个命令行方式的控制台程序,需要结合lcov,gcovr等前端图形工具才能实现统计数据图形化。

这里我们重新编译支持gcovntpq

1
CC=clang CFLAGS="--coverage -g -O0" ./configure && make -C ntpq

然后调用ntpq运行fuzz_out/queue目录下所有的文件,即可记录代码所有覆盖的路径。

这里,我们写一个简单的shell脚本。

1
2
3
4
5
6
7
#!/bin/sh
count=0
for i in fuzz/fuzz_out/queue/id*; do
count=$((count + 1))
./ntp-4.2.2/ntpq/ntpq < $i > /dev/null # 运行ntpq,并将输出结果重定位到/dev/null中
echo "$count"
done

生成gcov报告:

1
2
cd ntp-4.2.2/ntpq/
llvm-cov gcov ntpq.c

image-20231017220908746

覆盖率信息保存到了ntqp.c.gcov文件中。

查看该文件,并重点关注cookedprint函数。

其中前面是-的表示是没有对应生成代码的区域;前面是数字的表示执行了的次数;前面是#####的表示是没有执行到的代码,可以通过观察覆盖率然后调整种子提升模糊测试效率。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
-: 3025:static void
3375: 3026:cookedprint(
-: 3027: int datatype,
-: 3028: int length,
-: 3029: char *data,
-: 3030: int status,
-: 3031: FILE *fp
-: 3032: )
-: 3033:{
-: 3034: register int varid;
-: 3035: char *name;
-: 3036: char *value;
-: 3037: char output_raw;
-: 3038: int fmt;
-: 3039: struct ctl_var *varlist;
-: 3040: l_fp lfp;
-: 3041: long ival;
-: 3042: struct sockaddr_storage hval;
-: 3043: u_long uval;
-: 3044: l_fp lfparr[8];
-: 3045: int narr;
-: 3046:
3375: 3047: switch (datatype) {
-: 3048: case TYPE_PEER:
2738: 3049: varlist = peer_var;
2738: 3050: break;
-: 3051: case TYPE_SYS:
553: 3052: varlist = sys_var;
553: 3053: break;
-: 3054: case TYPE_CLOCK:
78: 3055: varlist = clock_var;
78: 3056: break;
-: 3057: default:
6: 3058: (void) fprintf(stderr, "Unknown datatype(0x%x) in cookedprint\n", datatype);
6: 3059: return;
-: 3060: }
-: 3061:
6738: 3062: (void) fprintf(fp, "status=%04x %s,\n", status,
3369: 3063: statustoa(datatype, status));
-: 3064:
3369: 3065: startoutput();
15461: 3066: while (nextvar(&length, &data, &name, &value)) {
12092: 3067: varid = findvar(name, varlist, 0);
12092: 3068: if (varid == 0) {
8253: 3069: output_raw = '*';
8253: 3070: } else {
3839: 3071: output_raw = 0;
3839: 3072: fmt = varlist[varid].fmt;
3839: 3073: switch(fmt) {
-: 3074: case TS:
1475: 3075: if (!decodets(value, &lfp))
1161: 3076: output_raw = '?';
-: 3077: else
314: 3078: output(fp, name, prettydate(&lfp));
1475: 3079: break;
-: 3080: case FL:
-: 3081: case FU:
-: 3082: case FS:
32: 3083: if (!decodetime(value, &lfp))
28: 3084: output_raw = '?';
-: 3085: else {
4: 3086: switch (fmt) {
-: 3087: case FL:
#####: 3088: output(fp, name,
#####: 3089: lfptoms(&lfp, 3));
#####: 3090: break;
-: 3091: case FU:
#####: 3092: output(fp, name,
#####: 3093: ulfptoms(&lfp, 3));
#####: 3094: break;
-: 3095: case FS:
8: 3096: output(fp, name,
4: 3097: lfptoms(&lfp, 3));
4: 3098: break;
-: 3099: }
-: 3100: }
32: 3101: break;
-: 3102:
-: 3103: case UI:
328: 3104: if (!decodeuint(value, &uval))
268: 3105: output_raw = '?';
-: 3106: else
60: 3107: output(fp, name, uinttoa(uval));
328: 3108: break;
-: 3109:
-: 3110: case SI:
#####: 3111: if (!decodeint(value, &ival))
#####: 3112: output_raw = '?';
-: 3113: else
#####: 3114: output(fp, name, inttoa(ival));
#####: 3115: break;
-: 3116:
-: 3117: case HA:
-: 3118: case NA:
137: 3119: if (!decodenetnum(value, &hval))
127: 3120: output_raw = '?';
10: 3121: else if (fmt == HA){
5: 3122: output(fp, name, nntohost(&hval));
5: 3123: } else {
5: 3124: output(fp, name, stoa(&hval));
-: 3125: }
137: 3126: break;
-: 3127:
-: 3128: case ST:
#####: 3129: output_raw = '*';
#####: 3130: break;
-: 3131:
-: 3132: case RF:
17: 3133: if (decodenetnum(value, &hval)) {
5: 3134: if ((hval.ss_family == AF_INET) &&
5: 3135: ISREFCLOCKADR(&hval))
#####: 3136: output(fp, name,
#####: 3137: refnumtoa(&hval));
-: 3138: else
5: 3139: output(fp, name, stoa(&hval));
17: 3140: } else if ((int)strlen(value) <= 4)
6: 3141: output(fp, name, value);
-: 3142: else
6: 3143: output_raw = '?';
17: 3144: break;
-: 3145:
-: 3146: case LP:
98: 3147: if (!decodeuint(value, &uval) || uval > 3)
87: 3148: output_raw = '?';
-: 3149: else {
-: 3150: char b[3];
11: 3151: b[0] = b[1] = '0';
11: 3152: if (uval & 0x2)
#####: 3153: b[0] = '1';
11: 3154: if (uval & 0x1)
5: 3155: b[1] = '1';
11: 3156: b[2] = '\0';
11: 3157: output(fp, name, b);
-: 3158: }
98: 3159: break;
-: 3160:
-: 3161: case OC:
62: 3162: if (!decodeuint(value, &uval))
45: 3163: output_raw = '?';
-: 3164: else {
-: 3165: char b[10];
-: 3166:
17: 3167: (void) sprintf(b, "%03lo", uval);
17: 3168: output(fp, name, b);
-: 3169: }
62: 3170: break;
-: 3171:
-: 3172: case MD:
#####: 3173: if (!decodeuint(value, &uval))
#####: 3174: output_raw = '?';
-: 3175: else
#####: 3176: output(fp, name, uinttoa(uval));
#####: 3177: break;
-: 3178:
-: 3179: case AR:
1463: 3180: if (!decodearr(value, &narr, lfparr))
818: 3181: output_raw = '?';
-: 3182: else
645: 3183: outputarr(fp, name, narr, lfparr);
1463: 3184: break;
-: 3185:
-: 3186: case FX:
227: 3187: if (!decodeuint(value, &uval))
188: 3188: output_raw = '?';
-: 3189: else
39: 3190: output(fp, name, tstflags(uval));
227: 3191: break;
-: 3192:
-: 3193: default:
#####: 3194: (void) fprintf(stderr,
-: 3195: "Internal error in cookedprint, %s=%s, fmt %d\n",
#####: 3196: name, value, fmt);
#####: 3197: break;
-: 3198: }
-: 3199:
-: 3200: }
12092: 3201: if (output_raw != 0) {
-: 3202: char bn[401];
-: 3203: char bv[401];
-: 3204: int len;
-: 3205:
10981: 3206: atoascii(400, name, bn);
10981: 3207: atoascii(400, value, bv);
10981: 3208: if (output_raw != '*') {
2728: 3209: len = strlen(bv);
2728: 3210: bv[len] = output_raw;
2728: 3211: bv[len+1] = '\0';
2728: 3212: }
10981: 3213: output(fp, bn, bv);
10981: 3214: }
-: 3215: }
3369: 3216: endoutput(fp);
3375: 3217:}

总结

通过对afl-training的学习,了解了afl常用的命令,以及常用工具的使用方式。同时,学到了不同输入方式下harness的编写,也就是想办法把非标准输入改为标准输入或者文件输入的形式。harness是fuzz的核心,一定要对测试程序写出针对性的harness。之后,笔者也会学习如何针对性的写出harness,继续加油吧。

参考文档:

https://tttang.com/archive/1508/

https://blog.wingszeng.top/afl-training-challenge-1-libxml2/

https://mundi-xu.github.io/2021/03/12/Start-Fuzzing-and-crashes-analysis/


afl-challenges
http://example.com/2023/10/16/afl-challenges/
作者
l1s00t
发布于
2023年10月16日
更新于
2023年10月17日
许可协议