这里阅读源码主要是调试阅读,由于没有熟练的使用过AFL,所以其中的很多模式和原理都不太懂,所以其中一些认知可能出现偏差,目的是摸清楚AFL的架构方便后期的魔改和使用。
这里主要是发生在main函数中的一些参数处理,调试的整个配置如下:
首先是利用开源的png处理库
https://github.com/brackeen/ok-file-formats.git
clone下来,然后写了一个调用库函数的文件:
#include <stdio.h>
#include "ok_png.h"
int main(int _argc, char **_argv) {
FILE *file = fopen(_argv[1], "rb");
ok_png image = ok_png_read(file, OK_PNG_COLOR_FORMAT_BGRA | OK_PNG_PREMULTIPLIED_ALPHA);
fclose(file);
if (image.data)
{
/* code */
printf("Got image ! Size: %li x %li \n", (long) image.width, (long) image.height);
free(image.data);
}
return 0;
}
使用afl-gcc编译:/home/tamako/Desktop/FUZZ/AFL_debug/AFLcpp/afl-gcc -g -o fuzz_png test.c ok_png.c ok_png.h
然后设置afl的参数如下:
-i
/home/tamako/Desktop/FUZZ/AFL_debug/work_dir/fuzz_in
-o
/home/tamako/Desktop/FUZZ/AFL_debug/work_dir/fuzz_out
-m
none
-t
500+
--
/home/tamako/Desktop/FUZZ/AFL_debug/work_dir/fuzzbuild/other/ok-file-formats/fuzz_png
@@
首先从main函数理清楚整个执行的流程,然后再去细看。
进入main函数之后,进行一些初始化,设置随机数种子之类的,然后进行参数的设置,参数的设置使用的是while循环加上一个switch结构,其中没啥好讲的,直接继续看。
后续的流程为了能够更加清晰的理解,我们首先看一下AFL的整体执行流程,此外8000+的代码,我们更应该注意的是变异和一些对修改AFL为自己的fuzz机器的重要位置。
解析完参数之后,通过函数setup_signal_handlers
设置一些信号处理,主要涉及程序瑞出时候和屏幕大小变化的一些操作;后面继续调用了check_asan_opts
函数来检查ASAN_OPTIONS和MASN_OPTIONS
,对其中内容的合法性做了一些检查。
首先分析其中的函数,然后总结流程
函数进行了-M -S
主从模式的一些修改。
如果通过-M或者-S指定了sync_id,则更新out_dir和sync_dir的值
out_dir/sync_id
x = alloc_printf("%s/%s", out_dir, sync_id);
sync_dir = out_dir;
out_dir = x;
sysnc_id来源于-S参数
处理完主从模式之后,判断是否是dumb模式
save_line函数主要是把参数全部转移到堆上存储,并且存储指针在全局变量orig_cmdline
中
fix_up_banner函数是设置use_banner变量的值,主要用于后面UI图形化展示中的标题,流程如下,以调试为例:
check_if_tty
函数检查程序是否是tty终端运行AFL_NO_UI
,如果有则return,然后设置not_no_tty
=1get_core_count函数,获得当前cpu的核心个数(可以是虚拟核)
该函数定义在宏内,如果定义了HAVE_AFFINITY
,那么执行该函数,该函数的作用是绑定当前进程到free的cpu上。
这和崩溃的处理有关系,/proc/sys/kernel/core_pattern指定了发生崩溃的时候如何处理崩溃,check_cpu_governor是检查cpu的调节器,来使得cpu可以处于高效的运行状态。
static void setup_post(void)
{
void *dh;
u8 *fn = getenv("AFL_POST_LIBRARY");
u32 tlen = 6;
if (!fn)
return;
ACTF("Loading postprocessor from '%s'...", fn);
dh = dlopen(fn, RTLD_NOW);
if (!dh)
FATAL("%s", dlerror());
post_handler = dlsym(dh, "afl_postprocess");
if (!post_handler)
FATAL("Symbol 'afl_postprocess' not found.");
/* Do a quick test. It's better to segfault now than later =) */
post_handler("hello", &tlen);
OKF("Postprocessor installed successfully.");
}
如果定义了AFL_POST_LIBRARY
环境变量,那么使用dlopen函数打开该动态链接库,然后使用alsym定位该库中afl_postprocess
函数的位置,最后对该函数进行一次尝试调用(此时出错比后面出错开销小)
该函数在后续的每一次fuzz中,都会先调用,相当于实在这里给用户留下了一个hook,可以自定义fuzz前的操作
该函数在讲编译的时候提到过,是用来配置共享内存和virgin_bits之类的变量的,函数流程如下:
EXP_ST void setup_shm(void)
{
u8 *shm_str;
if (!in_bitmap)
memset(virgin_bits, 255, MAP_SIZE);
memset(virgin_tmout, 255, MAP_SIZE);
memset(virgin_crash, 255, MAP_SIZE);
shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);
if (shm_id < 0)
PFATAL("shmget() failed");
atexit(remove_shm);
shm_str = alloc_printf("%d", shm_id);
/* If somebody is asking us to fuzz instrumented binaries in dumb mode,
we don't want them to detect instrumentation, since we won't be sending
fork server commands. This should be replaced with better auto-detection
later on, perhaps? */
if (!dumb_mode)
setenv(SHM_ENV_VAR, shm_str, 1);
ck_free(shm_str);
trace_bits = shmat(shm_id, NULL, 0);
if (trace_bits == (void *)-1)
PFATAL("shmat() failed");
}
int shmget(key_t key, size_t size, int shmflg);
IPC_CREAT | IPC_EXCL | 0600
shmctl(shm_id, IPC_RMID, NULL);
的wrapper函数。 这里对返回的trace_bits的理解如下:
SHM with instrumentation bitmap
EXP_ST void init_count_class16(void)
{
u32 b1, b2;
for (b1 = 0; b1 < 256; b1++)
for (b2 = 0; b2 < 256; b2++)
count_class_lookup16[(b1 << 8) + b2] =
(count_class_lookup8[b1] << 8) |
count_class_lookup8[b2];
}
非常简单的函数,该函数中涉及到了两个全局数组,count_class_lookup16
和count_class_lookup8
,后面这个变量被提前定义了
static const u8 count_class_lookup8[256] = {
[0] = 0,
[1] = 1,
[2] = 2,
[3] = 4,
[4 ... 7] = 8,
[8 ... 15] = 16,
[16 ... 31] = 32,
[32 ... 127] = 64,
[128 ... 255] = 128
};
为什么要这样定义呢?实际上是因为这两个变量后面会被用来记录是否到大该路径,trace_bits是用一个字节来记录0-255之间,但比如一个循环,循环多次实际上效果一样,所以为了不被当作不同的路径,即减少因为命中次数不一样导致的区别,按照上面的划分了区间,每次去计算发现新的路径之前,先把这个路径的命中次数进行一次转变,比如说4-7次之间,都统一认为命中了8次
那为什么又用这个函数初始化了一个lookup16的变量呢,使用这个变量主要是因为在AFL中,使用一个二元组来表示一条分支的路径,举个例子
A->B->C->D->A->B 可以用如下方式表示
[A, B] [B, C] [C, D] [D, A]四个二元组表示,只需要记录跳转的其实和目的地址,因为一个块中的执行次数是一样的,这里的[A, B]执行了两次,其他的都执行了一次,这里使用hash映射在一张map中,而之前的单字节,表示一个二元组不太方便,所以定义了一个新的变量,用count_class_lookup16 使用 两个字节来表示二元组,并且在这里初始化
同样的初始化也用了规整
还有一段文字对这个解释了:
这样处理之后,对分支执行次数就会有一个简单的归类。例如,如果对某个测试用例处理时,分支A执行了32次;对另外一个测试用例,分支A执行了3次,那么AFL就会认为这两次的代码覆盖是相同的。当然,这样的简单分类肯定不能区分所有的情况,不过在某种程度上,处理了一些因为循环次数的微小区别,而误判为不同执行结果的情况
具体的还不太能够自己理解,需要和后面的综合起来才能够掌握。
该函数主要是对输入和输出的目录做了处理,函数流程如下:
mkdir(out_dir, 0700)
,创建输出文件夹 in_dir/queue
是否可以存在,如果存在则设置in_dir为in_dir/queue
。in_dir
中的文件个数,nl_cnt = scandir(in_dir, &nl, NULL, alphasort);
,扫描的结果存储在nl中,nl使用数组存储结果,返回的nl_cnt为个数(这个个数都把…
包含了进去shuffle_ptrs((void **)nl, nl_cnt);
,作用是对nl数组中的内容进行重新排序一般是readme.txt …
也可以加入自定义in_dir/.state/deterministic_done/testcase
这个文件是否可以访问,如果可以则设置passed_det为1 add_to_queue(fn, st.st_size, passed_det);
q->fname = fname; // fn文件名
q->len = len; // size
q->depth = cur_depth + 1; // cur_depth 定义为Current path depth
q->passed_det = passed_det;
初始化一些参数
if (q->depth > max_depth)
max_depth = q->depth;
if (queue_top)
{
queue_top->next = q;
queue_top = q;
}
else
q_prev100 = queue = queue_top = q;
queued_paths++;
pending_not_fuzzed++;// 待fuzz的样例计数器
cycles_wo_finds = 0;//解释为Cycles without any new paths
/* Set next_100 pointer for every 100th element (index 0, 100, etc) to allow faster iteration. */
if ((queued_paths - 1) % 100 == 0 && queued_paths > 1)
{
q_prev100->next_100 = q;
q_prev100 = q;
}
last_path_time = get_cur_time();
队列的维护方式是头插法,且有几个q_prev100这样类似的变量,从这里可以看出queued_paths使用来统计个数的,队列按照每一百个testcase一起管理。
函数使用循环自动load生成的字典。
alloc_printf("%s/.state/auto_extras/auto_%06u", in_dir, i)
MAX_AUTO_EXTRA+1
个字节到tmp数组里面,默认MAX_AUTO_EXTRA为32,读出的长度保存在len中if (len >= MIN_AUTO_EXTRA && len <= MAX_AUTO_EXTRA)
则调用maybe_add_auto(tmp,len)函数,否则结束memcmp_nocase(extras[i].data, mem, len)
,如果有一个相同,就直接return。memcmp_nocase(a_extras[i].data, mem, len)
,如果相同,就将其hit_cnt值加一,这是代表在语料中被use的次数,然后跳转到sort_a_extras
struct extra_data {
u8 *data; /* Dictionary token data */
u32 len; /* Dictionary token length */
u32 hit_cnt; /* Use count in the corpus */
该函数在第一次循环的时候没有进入,直接就在上一个函数返回了,所以有些的理解可能不太到位,大部分是借鉴了sakura师傅的注释,这俩函数我还没太看懂意义是什么。
该函数为testcases在out_dir中设置hard link
完成命名之后,调用link_or_copy函数修改q->frame为修改后的命名细节不考虑
link_or_copy(q->fname, nfn);
ck_free(q->fname);
q->fname = nfn;
这三句意思就是如此,接着
out_dir/queue/.state/deterministic_done/use_name
这个文件,如果不存在就创建这个文件,然后设置q的passed_det为1。use_name就是orig:后面的字符串
如果定义了extras_dir,那么调用该函数,从extras_dir读取extras到extras数组里,并按size排序。extra_dir来源于-x 参数。
如果指定了resuming_fuzz即从输出目录当中恢复模糊测试状态,会从之前的模糊测试状态fuzz_stats文件中计算time out的值,保存在exec_tmout中,如此一来在没有指定-t 的情况下resuming session的时候,则能够迅速的确定超过时间,而不是让系统一次又一次的自动调整。
resuming_fuzz
为0,则直接returnin_place_resume
,设置fn为 out_dir/fuzzer_stats
in_dir/../fuzzer_stats
exec_timeout :
功能是检查参数中是否有@@,如果有则替换为out_dir/.cur_input
。
实现的一些细节就不讲了,这个意思就是参数的处理,用了一些基本的函数进行替换,不是关注的技术重点。
对文件路径进行检查,检查文件是否存在且不是一个shell脚本,检查方式是检查文件内容,文件内容的开头是不是#!
这部分代码比较少,如下:
perform_dry_run(use_argv); //这是主要的fuzz函数,将每个种子文件作为输入,运行目标程序一次 重点分析
cull_queue(); // 将运行过的种子根据运行的效果排序,后续模糊测试根据排序结果来挑选样例进行模糊测试
show_init_stats(); // 进行初步的UI显示
seek_to = find_start_position();
write_stats_file(0, 0, 0); // 状态文件的写入和保存
save_auto(); // 保存自动提取的token,用于后续字典模式的fuzz
if (stop_soon)
goto stop_fuzzing;
/* Woop woop woop */
if (!not_on_tty)
{
sleep(4);
start_time += 4000;
if (stop_soon)
goto stop_fuzzing;
}
主要的函数功能写了一些注释,下面细看每一个函数
看函数名字可以知道和运行测试用例有一点关系,该函数的参数是user_argv。
res == crash_mode || res == FAULT_NOBITS
错误类型如下:(这些错误类型都来自别的师傅的博客)
WARNF("Test case results in a timeout (skipping)");
,并设置q的cal_failed为CAL_CHANCES,cal_failures计数器加一。FATAL("Test case '%s' results in a crash", fn);
Unable to execute target application
No instrumentation detected
WARNF("No new instrumentation output, test case may be useless.")
,认为这是无用路径。useless_at_start计数器加一检查有没有新路径或者某个路径的执行次数有所不同。该函数注释较多,一些细节可能不太懂,但是大致明白了
*virgin &= ~*current;
if (ret && virgin_map == virgin_bits)
bitmap_changed = 1;
最后如果传入has_new_bits的参数virgin_map是virgin_bit,且ret不为0,设置bitmap_changed为1,vrigin_bit保存还没有被fuzz覆盖到的byte,其初始值每次都会被全部置为1,然后每次按字节置位,最后返回ret
比较复杂,总结起来功能就是,通过共享内存指针和传入的参数指针,进行测试,具体的测试前面提到了,如果ret为2则执行*virgin &= ~*current;
然后判断是否还有路径,如果ret小于2,则代表当前cur和vir只是代表hit-count增加,此时继续进行判断,判断完毕之后执行和上面一样的操作,总的来说就是对传入的参数进行检查,把其中新发现的路径都找出来
这是主要的运行函数,注释的意思是该函数用来校准新的测试用例,主要是早期警告可疑或有问题的测试用例,以及当发现新的路径以检测可变行为。
char **argv, struct queue_entry *q, u8 *use_mem, u32 handicap, u8 from_queue
!dumb_mode && !stage_cur && !count_bytes(trace_bits)
count_bytes意思是检查共享内存已经保存的路径,在以上的条件下,fault被设置为FAULT_NOINST
,然后跳转到abort_calibrationhash32(trace_bits, MAP_SIZE, HASH_CONST)
的结果,其值为一个32位uint值,保存到cksum中,这个步骤可以理解为计算每一次预先你滚的checksum即特征update_bitmap_score(struct queue_entry *q)
函数FAULT_NONE
,且该queue是第一次执行,且不属于dumb_mode,而且new_bits为0,代表在这个样例所有轮次的执行里,都没有发现任何新路径和出现异常,设置fault为FAULT_NOBITS
new_bits == 2 && !q->has_new_cov
则设置has_new_cov为1,然后queued_with_cov++,代表有一个新的覆盖路径产生mark_as_variable(struct queue_entry *q)
out_dir/queue/.state/variable_behavior/fname
queued_variable
的值加一show_stats
该函数实际上就是利用一个大的for循环对queue中的每一个fname都运行一次,可以理解为这就是第一个fuzz,其中主要的函数就是run_target,此外,这里面还涉及到了forkserver的初始化,运行完毕之后有一系列复杂的操作来记录,覆盖率和路径有没有新的变化,这里还不是很理解,后续调试的时候可以重点关注,变量实在是太多了,只能稍微理清楚一点。
prev_timed_out
,该管道的对象是fork server,此时应该是和fork server联动,forkserver会用子进程execv该程序,发送了信息之后,通过read读取返回的信息,存储在res中,此时应该是把子进程的pid读入到了child_pid中classify_counts((u32 *)trace_bits);
给出的注释是:每当我们发现一个新的path,都会调用这个函数来判断是不是更加的favorable,即是不是包含最小的路径集合来遍历到所有bitmap中的位,我们专注于这些集合而忽略其他的。
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)
{
ck_free(top_rated[i]->trace_mini);
top_rated[i]->trace_mini = 0;
}
如果当前的rated中的faver值比较小,那么continue,看下一个trace_bits
如果当前faver比当前case的faver大,那么把rated中的tc_ref减去1,然后把trace_mini字段置为0
这个函数的理解主要在于top_rated这个数组,可以看下数组的定义top_rated[MAP_SIZE]; /* Top entries for bitmap bytes
通过if top_rated[i]的判断之后,执行如下内容
/* Insert ourselves as the new winner. */
top_rated[i] = q;
q->tc_ref++;
if (!q->trace_mini)
{
q->trace_mini = ck_alloc(MAP_SIZE >> 3);
minimize_bits(q->trace_mini, trace_bits);
}
score_changed = 1;
设置当前top_rated为q,看起来是把更加优秀的case放入top_rated中,如果trace_mini为空,那么通过minimize_bits压缩trace_bit然后存入trace_mini中
最后设置score changed为 1
static void minimize_bits(u8 *dst, u8 *src)
{
u32 i = 0;
while (i < MAP_SIZE)
{
if (*(src++))
dst[i >> 3] |= 1 << (i & 7);
i++;
}
}
算是一个比较经典的算法,和cull_queue中的类似,可以看这个代码
https://blog.csdn.net/lxlmycsdnfree/article/details/78926359
这里几乎覆盖了afl_fuzz的2/3的代码,其中有很多对于模式的选择和处理,同时也有很多的共享内存的处理以及相关路径覆盖的更新和记录,都是一些难以理解的地方,后续还是需要多多调试和使用AFL才能明白其中的一些处理,同时,以后如果AFL使用过程中出现了报错,也好及时的定位解决。
11 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!