首页
社区
课程
招聘
[原创]AFL++实战入门与afl-fuzz源码流解析
发表于: 2024-10-4 15:13 3875

[原创]AFL++实战入门与afl-fuzz源码流解析

2024-10-4 15:13
3875

本项目为模糊测试的零基础教学,适合了解 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++中用于在目标程序中插入特定代码(即插桩)的组件,其主要作用是跟踪程序执行路径,并提供反馈信息以优化模糊测试过程。

通过对比 gccafl-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);
收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//