每次做完 mutation 都會執行 common_fuzz_stuff()
,而此 function 會呼叫 run_target()
以及 save_if_interesting()
來執行 target 並分析,程式碼如下:
EXP_ST u8 common_fuzz_stuff(char** argv, u8* out_buf, u32 len)
{
u8 fault;
// 將 mutated 的 input data 寫入 <output_dir>/.cur_input
// 所對應到的 fd
write_to_testcase(out_buf, len);
// 執行 target
fault = run_target(argv, exec_tmout);
// 執行 save_if_interesting() 分析此次 input data
save_if_interesting(argv, out_buf, len, fault);
return 0;
}
先前已經介紹過 run_target()
,在此我們關注的是 save_if_interesting()
,其功能是用來保存 interesting input,並做更深入的分析,程式碼如下:
static u8 save_if_interesting(char** argv, void* mem, u32 len, u8 fault)
{
// 如果此 input 有走到新的 coverage,或是走到
// 更多次 edge,就表示為 interesting,否則直接
// return
if (!(hnb = has_new_bits(virgin_bits)))
return 0;
// 為其產生檔案名稱並加到 queue 當中
fn = alloc_printf("%s/queue/id:%06u,%s", out_dir, queued_paths,
describe_op(hnb));
add_to_queue(fn, len, 0);
// 新進來的 queue entry 會是 queue_top,更新
// 其 checksum
queue_top->exec_cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);
// 校正此 input,檢測是否正常
res = calibrate_case(argv, queue_top, mem, queue_cycle - 1, 0);
// 保存至檔案當中
fd = open(fn, O_WRONLY | O_CREAT | O_EXCL, 0600);
ck_write(fd, mem, len, fn);
close(fd);
keeping = 1;
// 檢查是否發生 fault
switch (fault) {
// 執行期間出現 timeout
case FAULT_TMOUT:
// timeout 異常發生太多次,直接不理會
if (unique_hangs >= 500) return keeping;
// 簡化 coverage
// 如果過去有相同 coverage 造成 timeout,
// 則不額外保存 timeout input
simplify_trace((u64*)trace_bits);
if (!has_new_bits(virgin_tmout))
return keeping;
// 重新執行一次,確保 timeout 確實會發生
write_to_testcase(mem, len);
new_fault = run_target(argv, hang_tmout);
// 重新執行的結果是 crash
if (new_fault == FAULT_CRASH) goto keep_as_crash;
// 重新執行後什麼也不是
if (new_fault != FAULT_TMOUT) return keeping;
// 產生檔案名字
fn = alloc_printf("%s/hangs/id:%06llu,%s", out_dir,
unique_hangs, describe_op(0));
unique_hangs++;
break;
case FAULT_CRASH:
keep_as_crash:
total_crashes++;
// crash 異常發生太多次,直接不理會
if (unique_crashes >= 5000) return keeping;
// 簡化 coverage
simplify_trace((u64*)trace_bits);
// 如果過去有相同 coverage 造成 crash,
// 則不額外保存 crash input
if (!has_new_bits(virgin_crash))
return keeping;
// 產生檔案名字
fn = alloc_printf("%s/crashes/id:%06llu,sig:%02u,%s", out_dir,
unique_crashes, kill_signal, describe_op(0));
unique_crashes++;
break;
default: return keeping;
}
// 保存 crash input 到 output directory 當中
fd = open(fn, O_WRONLY | O_CREAT | O_EXCL, 0600);
ck_write(fd, mem, len, fn);
close(fd);
ck_free(fn);
return keeping;
}
到此已經介紹完整個 AFL fuzzer 的架構,這裡可以做個總結:
雖然做法直觀,但其中存在許多可以優化的地方,像是對於不同的程式來說,各個變異所造成的效果可能不相同,因此可以變異本身也可以有優先順序。Fuzzing 相關的論文皆在提出諸如此類的優化,畢竟 fuzzing 都會跑很久,並且每秒執行的次數也很多,即使只能增加一些效率,但累積下來的差異還是很可觀的。
在後續的文章中,會帶讀者了解除了 userspace 之外的 fuzzer,像是 kernel fuzzer 或是 hypervisor,同時也包含一些經典的 fuzzer 優化方式。