-
-
[原创]AFL++实战入门与afl-fuzz源码流解析
-
发表于: 2024-10-4 15:13 6790
-
本项目为模糊测试的零基础教学,适合了解 pwn 且会使用 Linux 的 gcc、gdb 的读者。模糊测试旨在通过向程序投喂数据使其崩溃,从而获取崩溃样本以寻找程序漏洞。本文前半部分介绍 AFL++ 的 docker 环境配置,帮助读者解决入门时的环境和网络问题;
后半部分全面解析 afl 的模糊测试流程与源码架构,包括漏洞挖掘实战和原理机制解析。通过本文,读者可以快速入门模糊测试,并深入了解 AFL++ 的工作原理和应用。
文章主要大纲:
以 AFL++ 作为入门模糊测试漏洞挖掘的最佳选择。然而,入门时存在的环境和网络问题往往会使大部分人望而却步,无法顺利转战实战。为解决这一问题,我们直接采用 docker + windows 的环境,并结合 Docker + Windows + VsCode 进行运行,至于代理问题则需要自行解决。
在windows上可通过以下步骤使用docker:
使用Dev Containers插件,可在商店中获取。
所以自己手动在命令行运行创建docker的命令:
存在一个巨大的bug如果直接使用vscode创建一个docker那么就会发生stop容器时容器自动删除!!!
解决方案可参考:
解决方案,创建afl++的docker容器可在命令行运行以下命令:
这是一个普通的栈溢出案例,添加了一点点的if判断语句用来模拟正常程序:
我们在正常使用这个程序的时候会通过命令行对程序进行输入,而afl++采用的就是这个方式对程序进行输入从而发现奔溃从而定位漏洞!而AFL++又是如何使投喂的数据精确制导发现漏洞的呢?
准备工作:
开始实验:
模糊测试一段时间后发现一个崩溃后,会出现相应的提示信息,如下所示:
接下来开始验收崩溃:
然后开始复现崩溃,将样本投喂进入程序:
成功复现出崩溃,这是通过afl++挖掘出来的第一个漏洞。如果使用gdb分析这个漏洞的成因,会发现这是一个栈溢出漏洞。即使目标源码体积扩大,大致流程也是相同的,可以大大缩小人力挖掘漏洞的难度。
AFL-GCC是AFL++中用于在目标程序中插入特定代码(即插桩)的组件,其主要作用是跟踪程序执行路径,并提供反馈信息以优化模糊测试过程。
通过对比 gcc
和 afl-gcc
编译的二进制文件,我们可以深入理解AFL (American Fuzzy Lop) 如何在程序中插入代码来实现代码覆盖率的追踪。这一插桩(Instrumentation)技术使 AFL 能够有效地检测输入引起的代码执行路径差异,从而高效进行模糊测试。
gcc 编译的版本:
使用 GCC 编译时,程序的控制流结构保持较为简洁,与源码几乎一致。
afl-gcc 编译的版本:
使用 AFL 编译后,生成的二进制文件中插入了 _afl_maybe_log()
函数,这个函数用于记录代码的执行路径信息。AFL 会通过这些插桩信息推断不同输入导致的执行路径差异。每当关键的程序状态发生变化(如输入判断等),AFL 插桩会记录这些变化,以便 AFL Fuzzer 能够捕捉到特定输入引发的行为变化。
通过bindiff进行二进制对比:
安装:Win10 + Bindiff 6.0 + IDA 7.5 安装教程_ida bindiff-CSDN博客
可以很明显的发现afl-gcc编译出的的部分被插桩了!
被afl-gcc插桩的程序会在每一条分支路径下插入一个_afl_maybe_log()
函数用来进行路径反馈!
在 AFL++ 中,插桩功能的核心由 afl_maybe_log
函数实现,它负责记录程序执行路径的覆盖信息,并将该信息通过共享内存反馈给 AFL++ 模糊测试引擎 (afl-fuzz
)。这一过程至关重要,帮助 AFL++ 确定输入如何触发新路径,从而优化输入生成。
目标进程的执行与路径记录:
反馈信息的传递:
共享内存与路径生成:
AFL++ 的路径生成机制基于覆盖率反馈,是模糊测试的核心功能之一。afl_maybe_log
用于生成并记录每次执行的唯一路径。
在AFL++中,模糊测试可以抽象为一个简单的模型。这个模型描述了程序在接收输入后,生成输出和反馈的过程。这种反馈用于指导下一轮的输入生成,从而不断优化测试覆盖率和发现潜在的漏洞。
基本流程如下:
为了使模糊测试更加有效,AFL++会在编译时对目标程序进行插桩。插桩的作用是记录程序在执行过程中走过的路径,从而提供精确的覆盖信息反馈。
插桩后的模型流程如下:
在上述代码中,afl_maybe_log
是插桩函数,它会基于条件的执行情况来记录路径信息,从而为模糊测试提供反馈。
在插桩反馈的基础上,模糊测试的完整模型如下:
该模型示意图如下:
AFL++通过覆盖制导的模糊测试模型,能够高效地发现程序中的潜在漏洞。通过插桩技术,AFL++精确记录了程序的执行路径,从而使得模糊测试在每一次变异后,都能基于反馈信息生成更加有效的测试用例,最大化地提升了测试覆盖率和漏洞发现的概率。
AFL(American Fuzzy Lop)是一种高效的模糊测试工具,通过forkserver
机制显著提高了模糊测试的效率,减少了系统开销。afl_maybe_log
函数在这个流程中起到了关键作用,负责记录程序的执行路径,并通过共享内存反馈覆盖信息,帮助afl-fuzz
发现新的路径和潜在的漏洞。
源码位置:AFL项目地址:google/AFL: american fuzzy lop - a security-oriented
通过源码阅读首先找到afl-fuzz.c的main函数开始阅读:
afl-fuzz.c->mian()-> init_forkserver()
->父进程->等待子进程目标程序运行起来发来信号
->子进程fork() -> execv(target_path, argv);
在开启forkserver的情况下,目标程序会启动子进程,而父进程负责控制子进程.
父进程和子进程通过两个管道进行通信:
execv(target_path, argv);->__afl_maybe_log()
->父进程->运行目标程序->__afl_fork_wait_loop
->等待afl-fuzz命令准备通过fork启动目标进程减少开销
->子进程fork()->在父进程接收到命令后决定是否运行
整理出来的流程图就是下面的:
基本流程:afl-fuzz -> 运行 -> 目标程序
在AFL的早期实现中,afl-fuzz
直接运行目标程序。虽然这种方式简单,但在需要频繁创建新进程的场景中,效率不高。
改进流程:afl-fuzz -> forkserver -> 子进程
为了解决效率问题,AFL引入了forkserver
机制。在afl-fuzz
启动后,它首先通过forkserver
启动目标程序。forkserver
进程保持在内存中,等待afl-fuzz
传递命令。接收到命令后,forkserver
会创建新的子进程运行目标程序,并将子进程的状态信息反馈给afl-fuzz
。这种方式避免了频繁的进程创建和销毁操作,大大提高了模糊测试的效率。
这里面最重要的关键函数就是在插桩时候被插入程序的afl_maybe_log(),这个函数包括了路径计算以及forkserver的启动.
afl_maybe_log
函数分析
根据前面提到的afl_maybe_log
函数,用ida查看其真实的内部实现,这个函数用于记录目标程序的执行路径信息,这些信息将被afl-fuzz
用来识别新的路径和潜在的漏洞。
afl_maybe_log具体分析:
初次进入函数:afl_area_ptr为0
非初次进入函数:afl_area_ptr非0
afl_maybe_log
确保了每条独特的执行路径都会在共享内存中记录,这种机制帮助AFL在模糊测试过程中有效识别新的路径和潜在的漏洞。通过forkserver
机制,AFL能够更高效地进行测试,显著减少了系统开销。
//test2.c
#include <stdio.h>
#include <string.h>
void
vulnerable_function(
char
*input) {
char
buffer[4];
// 定义一个长度为4的字符数组
// 复制用户输入到buffer中
strcpy
(buffer, input);
printf
(
"输入内容: %s\n"
, buffer);
}
int
main() {
char
user_input[100];
printf
(
"请输入一串字符(以回车结束):"
);
fgets
(user_input,
sizeof
(user_input), stdin);
// 移除换行符
user_input[
strcspn
(user_input,
"\n"
)] = 0;
// 检查前四个字符是否是'a', 'b', 'c', 'd'
if
(user_input[0] ==
'a'
){
if
( user_input[1] ==
'b'
) {
if
(user_input[2] ==
'c'
){
if
(user_input[3] ==
'd'
){
vulnerable_function(user_input);
}
}
}
}
return
0;
}
//test2.c
#include <stdio.h>
#include <string.h>
void
vulnerable_function(
char
*input) {
char
buffer[4];
// 定义一个长度为4的字符数组
// 复制用户输入到buffer中
strcpy
(buffer, input);
printf
(
"输入内容: %s\n"
, buffer);
}
int
main() {
char
user_input[100];
printf
(
"请输入一串字符(以回车结束):"
);
fgets
(user_input,
sizeof
(user_input), stdin);
// 移除换行符
user_input[
strcspn
(user_input,
"\n"
)] = 0;
// 检查前四个字符是否是'a', 'b', 'c', 'd'
if
(user_input[0] ==
'a'
){
if
( user_input[1] ==
'b'
) {
if
(user_input[2] ==
'c'
){
if
(user_input[3] ==
'd'
){
vulnerable_function(user_input);
}
}
}
}
return
0;
}
//gcc编译版本的程序
int
__cdecl main(
int
argc,
const
char
**argv,
const
char
**envp)
{
__int16
s;
// [rsp+0h] [rbp-70h] BYREF
char
v5;
// [rsp+2h] [rbp-6Eh]
char
v6;
// [rsp+3h] [rbp-6Dh]
unsigned
__int64
v7;
// [rsp+68h] [rbp-8h]
v7 = __readfsqword(0x28u);
printf
(&byte_2020, argv, envp);
fgets
(&s, 100, _bss_start);
*(&s +
strcspn
(&s,
"\n"
)) = 0;
if
( s ==
'ba'
&& v5 ==
'c'
&& v6 ==
'd'
)
vulnerable_function(&s);
return
0;
}
//gcc编译版本的程序
int
__cdecl main(
int
argc,
const
char
**argv,
const
char
**envp)
{
__int16
s;
// [rsp+0h] [rbp-70h] BYREF
char
v5;
// [rsp+2h] [rbp-6Eh]
char
v6;
// [rsp+3h] [rbp-6Dh]
unsigned
__int64
v7;
// [rsp+68h] [rbp-8h]
v7 = __readfsqword(0x28u);
printf
(&byte_2020, argv, envp);
fgets
(&s, 100, _bss_start);
*(&s +
strcspn
(&s,
"\n"
)) = 0;
if
( s ==
'ba'
&& v5 ==
'c'
&& v6 ==
'd'
)
vulnerable_function(&s);
return
0;
}
//afl-gcc编译版本的程序
int
__cdecl main(
int
argc,
const
char
**argv,
const
char
**envp)
{
__int64
v3;
// rcx
__int64
v4;
// rdx
__int64
v5;
// rdx
__int64
v7;
// [rsp+0h] [rbp-110h]
__int64
v8;
// [rsp+0h] [rbp-110h]
__int64
v10;
// [rsp+80h] [rbp-90h]
char
user_input[100];
// [rsp+98h] [rbp-78h] BYREF
unsigned
__int64
v12;
// [rsp+100h] [rbp-10h]
v10 = v3;
_afl_maybe_log(argc, argv, envp, 24157LL);
v12 = __readfsqword(0x28u);
__printf_chk(1LL, &unk_2018, envp, v10);
fgets
(user_input, 100, stdin);
user_input[
strcspn
(user_input,
"\n"
)] = 0;
if
( user_input[0] ==
'a'
)
{
v8 = v4;
_afl_maybe_log(user_input,
"\n"
, v4, 32188LL);
v5 = v8;
if
( user_input[1] ==
'b'
)
{
_afl_maybe_log(user_input,
"\n"
, v8, 61008LL);
v5 = v8;
if
( user_input[2] ==
'c'
)
{
_afl_maybe_log(user_input,
"\n"
, v8, 12651LL);
v5 = v8;
if
( user_input[3] ==
'd'
)
{
_afl_maybe_log(user_input,
"\n"
, v8, 16091LL);
vulnerable_function(user_input);
}
}
}
}
else
{
v7 = v4;
_afl_maybe_log(user_input,
"\n"
, v4, 5770LL);
v5 = v7;
}
if
( v12 != __readfsqword(0x28u) )
_afl_maybe_log(user_input,
"\n"
, v5, 49767LL);
_afl_maybe_log(user_input,
"\n"
, v5, 41314LL);
return
0;
}
//afl-gcc编译版本的程序
int
__cdecl main(
int
argc,
const
char
**argv,
const
char
**envp)
{
__int64
v3;
// rcx
__int64
v4;
// rdx
__int64
v5;
// rdx
__int64
v7;
// [rsp+0h] [rbp-110h]
__int64
v8;
// [rsp+0h] [rbp-110h]
__int64
v10;
// [rsp+80h] [rbp-90h]
char
user_input[100];
// [rsp+98h] [rbp-78h] BYREF
unsigned
__int64
v12;
// [rsp+100h] [rbp-10h]
v10 = v3;
_afl_maybe_log(argc, argv, envp, 24157LL);
v12 = __readfsqword(0x28u);
__printf_chk(1LL, &unk_2018, envp, v10);
fgets
(user_input, 100, stdin);
user_input[
strcspn
(user_input,
"\n"
)] = 0;
if
( user_input[0] ==
'a'
)
{
v8 = v4;
_afl_maybe_log(user_input,
"\n"
, v4, 32188LL);
v5 = v8;
if
( user_input[1] ==
'b'
)
{
_afl_maybe_log(user_input,
"\n"
, v8, 61008LL);
v5 = v8;
if
( user_input[2] ==
'c'
)
{
_afl_maybe_log(user_input,
"\n"
, v8, 12651LL);