AFL++实战入门与afl-fuzz流程解析(源码流程图)
Brinmon 发表于 湖南 二进制安全 2447浏览 · 2024-09-05 03:00

简介

本文章为模糊测试的零基础教学,适合了解 pwn 且会使用 Linux 的 gcc、gdb 的读者。模糊测试旨在通过向程序投喂数据使其崩溃,从而获取崩溃样本以寻找程序漏洞。本文前半部分介绍 AFL++ 的 docker 环境配置,帮助读者解决入门时的环境和网络问题;
后半部分全面解析 afl 的模糊测试流程与源码架构,包括漏洞挖掘实战和原理机制解析。通过本文,读者可以快速入门模糊测试,并深入了解 AFL++ 的工作原理和应用。

文章主要大纲:

一、AFL++的docker环境配置

以 AFL++ 作为入门模糊测试漏洞挖掘的最佳选择。然而,入门时存在的环境和网络问题往往会使大部分人望而却步,无法顺利转战实战。为解决这一问题,我们直接采用 docker + windows 的环境,并结合 Docker + Windows + VsCode 进行运行,至于代理问题则需要自行解决。

(一)Docker+Windows

在windows上可通过以下步骤使用docker:

  1. Docker Desktop:面向开发人员的 #1 容器化工具 |码头工人 --- Docker Desktop: The #1 Containerization Tool for Developers | Docker
  2. vscode远程连接docker容器_attach to vscode-CSDN博客
  3. VsCode轻松使用docker容器-Remote Containers_哔哩哔哩_bilibili

(二)Vscode插件

使用Dev Containers插件,可在商店中获取。

(三)创建afl++的docker容器

所以自己手动在命令行运行创建docker的命令:

docker run --name afl -it -d aflplusplus/aflplusplus /bin/bash

(四)存在问题及解决方案

存在一个巨大的bug如果直接使用vscode创建一个docker那么就会发生stop容器时容器自动删除!!!
解决方案可参考:

解决方案,创建afl++的docker容器可在命令行运行以下命令:

docker run name afl -it -d aflplusplus/aflplusplus /bin/bash

二、AFL++进行漏洞挖掘实战

(一)目标程序的源码

这是一个普通的栈溢出案例,添加了一点点的if判断语句用来模拟正常程序:

//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;
}

我们在正常使用这个程序的时候会通过命令行对程序进行输入,而afl++采用的就是这个方式对程序进行输入从而发现奔溃从而定位漏洞!而AFL++又是如何使投喂的数据精确制导发现漏洞的呢?

(二)模糊测试的准备工作

准备工作:

  • 源码编译插桩:将目标程序的每一条路径都进行代码插桩从而反馈每条投喂数据的情况。
  • 创建语料库:为程序创建一个初始的语料库就是最开始的输入,用来作为初始变异的数据。
  • 开启模糊测试:最后就是进行模糊测试,等待afl++发掘出漏洞了。

开始实验:

  1. 源码编译插桩:使用 afl++ 自带的插桩编译器:afl-gcc(还有很多其他的插桩编译器现以这个举例)
    [AFL++ 405757f4e3c5] ~/work/test2 # afl-gcc ./test2.c -o ./stackoverflow
    afl-cc++4.22a by Michal Zalewski, Laszlo Szekeres, Marc Heuse - mode: GCC-GCC
    [!] WARNING: You are using outdated instrumentation, install LLVM and/or gcc-plugin and use afl-clang-fast/afl-clang-lto/afl-gcc-fast instead!
    ./test2.c: In function 'main':
    ./test2.c:17:5: warning: ignoring return value of 'fgets' declared with attribute 'warn_unused_result' [-Wunused-result]
    17 |     fgets(user_input, sizeof(user_input), stdin);
       |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    afl-as++4.22a by Michal Zalewski
    [+] Instrumented 10 locations (64-bit, non-hardened mode, ratio 100%).
    
  2. 准备语料库:作为程序输入的原始材料,必须有才可以运行,原始材料越好漏洞发现的越快
    [AFL++ 405757f4e3c5] ~/work/test2 # echo "abcd" >> ./input/seed1
    
  3. 开启模糊测试:指定必要的目标文件夹就可以开启模糊测试了,也可能发生其他情况根据提示解决
    [AFL++ 405757f4e3c5] ~/work/test2 # afl-fuzz -i input/ -o output/ ./stackoverflow
    
  4. afl-fuzz: 这是 AFL++ 的核心工具,用于执行模糊测试。AFL++ 是一个强大的模糊测试工具,它通过自动化生成输入数据来测试目标程序,从而发现潜在的安全漏洞或程序崩溃。
  5. -i input/: 这个选项指定了 AFL++ 用来开始模糊测试的初始输入种子文件的目录。在这里,input/ 是包含初始输入数据的目录。AFL++ 会基于这些初始输入生成变异后的测试数据。
  6. -o output/: 这个选项指定了 AFL++ 将输出模糊测试结果的目录。output/ 目录将包含测试过程中生成的各种数据,比如发现的崩溃或挂起的输入、统计信息等。
  7. ./stackoverflow: 这是要测试的目标程序。在这个例子中,./stackoverflow 是你要使用 AFL++ 进行模糊测试的可执行文件。AFL++ 会向这个程序提供变异后的输入,观察它的行为(例如,是否会崩溃或触发其他异常行为)。

(三)验证挖掘出来的漏洞

模糊测试一段时间后发现一个崩溃后,会出现相应的提示信息,如下所示:

接下来开始验收崩溃:

  • 崩溃的输入样本会被保存在output目录下的crashes目录。
    [AFL++ 405757f4e3c5] ~/work/test2 # ls./output/default/crashes/
    README.txt  id:000000,sig:06,src:000003,time:2086,execs:1700,op:havoc,rep:2
    
    然后开始复现崩溃,将样本投喂进入程序:
    [AFL++ 405757f4e3c5] ~/work/test2 #./stackoverflow <./output/default/crashes/id\,time\:2086\,execs\:1700\,op\:havoc\,rep\:2 
    *** buffer overflow detected ***: terminated
    Aborted
    
    成功复现出崩溃,这是通过afl++挖掘出来的第一个漏洞。如果使用gdb分析这个漏洞的成因,会发现这是一个栈溢出漏洞。即使目标源码体积扩大,大致流程也是相同的,可以大大缩小人力挖掘漏洞的难度。

三、AFL++漏洞挖掘原理与机制解析

(一)afl-gcc插桩机制详解

AFL-GCC是AFL++中用于在目标程序中插入特定代码(即插桩)的组件,其主要作用是跟踪程序执行路径,并提供反馈信息以优化模糊测试过程。

插桩流程概述

  1. 从高级语言到二进制文件的转换:高级语言(如C/C++)代码首先被编译为汇编代码,然后进一步被编译为二进制可执行文件(通过GCC完成)。在编译过程中,AFL-GCC会在汇编层面查找条件跳转指令(如jnz addr),并在这些位置插入一个叫做afl_maybe_log的函数调用。
  2. 条件跳转点的插桩:插桩的目的是在每个条件跳转点之后插入 afl_maybe_log,以便在程序运行时记录该跳转的执行情况。

二进制文件对比

通过对比 gccafl-gcc 编译的二进制文件,我们可以深入理解AFL (American Fuzzy Lop) 如何在程序中插入代码来实现代码覆盖率的追踪。这一插桩(Instrumentation)技术使 AFL 能够有效地检测输入引起的代码执行路径差异,从而高效进行模糊测试。

gcc 编译的版本

//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 编译时,程序的控制流结构保持较为简洁,与源码几乎一致。

afl-gcc 编译的版本

//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 编译后,生成的二进制文件中插入了 _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++ 确定输入如何触发新路径,从而优化输入生成。

覆盖信息反馈流程

  1. 目标进程的执行与路径记录:

    • afl-fuzz 启动目标程序,并监控其执行。
    • 在目标程序运行的过程中,AFL++ 通过插桩机制将 afl_maybe_log 函数插入到每个可能触发的分支点上。每当目标程序执行到插桩位置时,afl_maybe_log 记录执行路径。
  2. 反馈信息的传递:

    • 覆盖信息通过共享内存 (shared_mem) 传递给 afl-fuzz。每个被执行的路径会生成一个哈希值,该哈希值映射到共享内存中的某个索引。
    • 具体步骤如下:
      1. 共享内存获取afl_maybe_log 从全局共享内存(由 AFL++ 设置)中获取路径数据。
      2. 路径索引生成:每条路径执行时,生成一个唯一的哈希值。该哈希值通过简单的逻辑运算和路径信息生成。
      3. 记录路径执行次数:程序每执行一个路径,shared_mem[index](共享内存对应路径哈希值的位置)就会递增,记录该路径的执行频次。
  3. 共享内存与路径生成:

    • 共享内存模型:共享内存可以被看作一个固定大小的字节数组。目标程序在运行期间,每条路径都会映射到数组的某个索引,该索引的值表示路径被执行的次数。
    • 路径执行频率:不同的输入会影响不同路径的触发情况。afl-fuzz 分析共享内存中的信息,发现哪些输入会触发新的路径或未覆盖的代码。

核心:唯一路径生成原理

AFL++ 的路径生成机制基于覆盖率反馈,是模糊测试的核心功能之一。afl_maybe_log 用于生成并记录每次执行的唯一路径。

  1. 路径哈希值生成
    • 每当目标程序执行到某个分支点时(如 A),afl_maybe_log 会生成一个与该分支点相关的随机数或标识符。例如,点 A 的标识符为 1
    • 当程序执行到另一个分支点(如 B),则生成另一个标识符,例如 2
    • 程序从 AB 经过的路径哈希值可以表示为 hash(1 -> 2),此哈希值被用于生成唯一的路径索引。
  2. 路径唯一性
    • 程序的每个执行点都会生成一个唯一的标识符,AFL++ 通过一系列的位操作(如异或和移位)生成路径哈希。
    • 哈希值的变化随着程序经过的点越来越多而不断累积,最终形成一个标志着程序完整执行路径的哈希值。
    • 该哈希值在共享内存中映射到某个具体的索引,这一索引则标识了特定路径的执行。

(三)基于覆盖制导的模糊测试抽象模型

a. 模糊测试的基本模型

在AFL++中,模糊测试可以抽象为一个简单的模型。这个模型描述了程序在接收输入后,生成输出和反馈的过程。这种反馈用于指导下一轮的输入生成,从而不断优化测试覆盖率和发现潜在的漏洞。

基本流程如下:

  1. 数据生成器:生成输入数据。
  2. 数据输入:将生成的数据输入目标程序。
  3. 进程执行:程序处理输入数据并生成输出,同时提供路径覆盖的反馈信息。
    模型示意图如下(假设有图像支持):

b. 插桩技术的引入与模型扩展

为了使模糊测试更加有效,AFL++会在编译时对目标程序进行插桩。插桩的作用是记录程序在执行过程中走过的路径,从而提供精确的覆盖信息反馈。

插桩后的模型流程如下:

  1. 数据生成器:生成输入数据。
  2. 数据输入:将数据输入插桩后的目标程序。
  3. 进程执行(已插桩):程序处理数据并输出路径反馈信息。
    在这个模型中,插桩代码的作用体现在以下示例中:
    void fun() {
     data = input();
     if (data[0] == 'a') {
         {A} ---> afl_maybe_log;
     } else {
         {B} ---> afl_maybe_log;
     }
    }
    
    在上述代码中,afl_maybe_log 是插桩函数,它会基于条件的执行情况来记录路径信息,从而为模糊测试提供反馈。

c. 完整的覆盖制导模糊测试模型

在插桩反馈的基础上,模糊测试的完整模型如下:

  1. 数据生成器:生成输入数据。
  2. 数据输入:输入数据到插桩后的程序中。
  3. 进程执行:程序处理数据,产生路径反馈信息。
  4. 路径信息统计/规整:收集和整理路径覆盖信息。
  5. 反馈给数据生成器:基于反馈信息,对输入数据进行变异,生成新的输入数据。

该模型示意图如下:

AFL++通过覆盖制导的模糊测试模型,能够高效地发现程序中的潜在漏洞。通过插桩技术,AFL++精确记录了程序的执行路径,从而使得模糊测试在每一次变异后,都能基于反馈信息生成更加有效的测试用例,最大化地提升了测试覆盖率和漏洞发现的概率。

(四)AFL的执行流程与forkserver机制

AFL(American Fuzzy Lop)是一种高效的模糊测试工具,通过forkserver机制显著提高了模糊测试的效率,减少了系统开销。afl_maybe_log函数在这个流程中起到了关键作用,负责记录程序的执行路径,并通过共享内存反馈覆盖信息,帮助afl-fuzz发现新的路径和潜在的漏洞。

1.整个AFL的运行流程图与源码参考位置

源码位置:AFL项目地址:google/AFL: american fuzzy lop - a security-oriented

通过源码阅读首先找到afl-fuzz.c的main函数开始阅读:

afl-fuzz开启fork部分

afl-fuzz.c->mian()-> init_forkserver()
->父进程->等待子进程目标程序运行起来发来信号
->子进程fork() -> execv(target_path, argv);

在开启forkserver的情况下,目标程序会启动子进程,而父进程负责控制子进程.
父进程和子进程通过两个管道进行通信:

  • dup2(ctl_pipe[0], FORKSRV_FD) : 状态管道
  • dup2(st_pipe[1], FORKSRV_FD + 1) : 命令管道
被插桩程序开启fork部分

execv(target_path, argv);->__afl_maybe_log()
->父进程->运行目标程序->__afl_fork_wait_loop->等待afl-fuzz命令准备通过fork启动目标进程减少开销
->子进程fork()->在父进程接收到命令后决定是否运行

整理出来的流程图就是下面的:

2.AFL的运行流程部分拆解

基本流程: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具体分析:

void afl_maybe_log()
{
    if(!afl_area_ptr)//判断是否为空,是否是第一次进入
    {
        //forkserver初始化的逻辑里面
        //然后通过管道和afl-fuzz进行交互,管道id 199

        //通过199管道通知afl-fuzz,forkserver已经准备完毕
        if(write(199,&afl_tmp,4) == 4)
        {
            while 1             //forkserver
            {
                //收到afl-fuzz指令fork新进程
                if(read(198,,&afl_tmp,4) != 4)
                    break;//如果198管道关闭,forkserver退出
                v14 = fork();
                if(v14 < 0 )
                    break;
                //
                if(!v14)
                    goto afl_fork_resume;
                afl_fork_pid = v14
                write(199,&afl_fork_pid,4)//想afl-fuzz传递进程pid
                v13 = afl_fork_pid
                v15 = waitpid(afl_fork_pid,afl-tmp,0)//父进程forkserver获得子进程pid,并通过waitpid等待子进程退出
                if(v15<=0)
                    break;
                write(199,afl_temp,4);//通过保存退出信息,并且传递给afl-fuzz
            }
        }

    }
    else
    {
        //这里就是进入到覆盖信息反馈的位置
        //主要就是计算路径hash,并且反馈
         index = _afl_prev_loc ^ random_num;
         _afl_prev_loc ^= index;
         _afl_prev_loc = _afl_prev_loc >> 1;
         v8 = __CFADD__(((v6 + index))++, 1);
         (v6 + index) += v8;
    }
}
详细分支解析
  1. 初次进入函数:afl_area_ptr为0

    • afl_area_ptr为0时,表示这是函数第一次被调用。在这种情况下,forkserver的初始化逻辑会被执行。forkserver通过管道与afl-fuzz进行交互,并通知afl-fuzz准备就绪。随后,它进入一个循环,等待afl-fuzz的指令来创建新子进程。
    • 共享内存的初始化:afl_maybe_log通过获取环境变量__AFL_SHM_ID来访问共享内存的句柄。这部分共享内存用于记录路径覆盖信息。
  2. 非初次进入函数:afl_area_ptr非0

    • 如果afl_area_ptr不为0,则说明forkserver已经初始化,并且子进程已经开始执行。此时,afl_maybe_log进入路径信息的记录阶段。
    • 路径信息的计算与记录
      index = _afl_prev_loc ^ random_num;
      _afl_prev_loc ^= index;
      _afl_prev_loc = _afl_prev_loc >> 1;
      v8 = __CFADD__(((v6 + index))++, 1);
      (v6 + index) += v8;
      
    • 当前路径的索引通过对前一个路径标识符(_afl_prev_loc)和当前随机数进行异或操作生成。之后,路径标识符通过右移操作进行更新,以避免循环路径生成相同的索引。
    • 为了防止路径计数器溢出,使用__CFADD__宏进行溢出检测。如果检测到溢出,v8的值会影响路径覆盖计数器,从而确保记录的准确性。

afl_maybe_log确保了每条独特的执行路径都会在共享内存中记录,这种机制帮助AFL在模糊测试过程中有效识别新的路径和潜在的漏洞。通过forkserver机制,AFL能够更高效地进行测试,显著减少了系统开销。

0 条评论
某人
表情
可输入 255