banner
KendyZ

KendyZ

feedId:65592353402595328+userId:54400481666870272
twitter
github
bilibili

AFL-training 笔记

mykter/afl-training 包含能够帮助初学者快速掌握如何使用 afl++ 对开源软件进行模糊测试的一系列材料。

Setup#

考虑到将构建好的 ghcr.io/mykter/fuzz-training 直接 pull 下来所花的时间可能会更长,我选择在本地通过 Dockerfile 从头开始 build image,然后运行容器。

  • build:可以在 Dockerfile 中添加 ARG http_proxy,然后在 docker build 命令中添加 --build-arg "http_proxy=http://127.0.0.1:7890",同时配置参数 --network=host(和 docker run --net=host 不一样!),使得 build 过程能够连接网络代理。在 Dockerfile 中合适的位置添加 git 代理(git config --global http-proxy $http_proxy),加快 git pull 的速度。
  • run:由于访问外网和科学上网的代理程序都运行在 host 上,而我的容器在运行时始终无法连通 host 的 ip(应该是 172.27.0.1)和代理端口,于是只能选择让容器运行在 host 模式下(--net=host),免去考虑端口映射、host ip 等麻烦。

libxml2#

libxml2 是一个流行的 XML 处理库,使用 C 语言实现,其具有的以下特点使其非常适合用于练习 fuzz:

  • 无状态
  • 无网络通信和文件读写
  • 文档详细丰富,API 外显,无需额外分析内部的 fuzz 接口
  • 功能集中、简单(处理 XML),运行较快

这里考虑的 CVE(Common Vulnerabilities & Exposures)是 CVE-2015-8317:在 parser.c 中的 xmlParseXMLDecl 函数在处理 不完整的 encoding 以及 不完整的 XML 声明 时会触发 out-of-bounds heap read

/**
 * xmlParseXMLDecl:
 * @ctxt:  an XML parser context
 *
 * parse an XML declaration header
 *
 * [23] XMLDecl ::= '<?xml' VersionInfo EncodingDecl? SDDecl? S? '?>'
 */
void xmlParseXMLDecl(xmlParserCtxtPtr ctxt) { ... }

在 parser.c 中 xmlParseXMLDecl 函数就是负责解析 XML 声明部分的,XML 声明指的就是一般放在 XML 文档第一行的内容:

<?xml version="1.0" encoding="UTF-8"?>

思路#

虽说问题出在这个函数功能内部,但是倒也没必要仅测试该函数:

  • 我自我感觉,由不完整的字符串取值导致的 XML 声明解析操作的薄弱性,在 fuzz 过程中应该是不难发现的,生成仅包含声明的 XML 文档和完整的 XML 文档,分别进行 fuzz 的效率的差异应该不会过大。
  • xmlParseXMLDecl 函数的参数是 XML 解析过程的 context(xmlParserCtxtPtr),直接生成和变异此 context 是一件非常麻烦且低效的事情,此 context 应由更高级的 API 生成。

基于这两点考虑,我找到了和 xmlParserCtxtPtr 相关的示例程序,作为 fuzz 的 harness:

#include <stdio.h>
#include <libxml/parser.h>
#include <libxml/tree.h>

/**
 * exampleFunc:
 * @filename: a filename or an URL
 *
 * Parse and validate the resource and free the resulting tree
 */
static void exampleFunc(const char *filename) {
    xmlParserCtxtPtr ctxt; /* the parser context */
    xmlDocPtr doc; /* the resulting document tree */
    /* create a parser context */
    ctxt = xmlNewParserCtxt();
    if (ctxt == NULL) {
        fprintf(stderr, "Failed to allocate parser context\n");
    return;
    }
    /* parse the file, activating the DTD validation option */
    doc = xmlCtxtReadFile(ctxt, filename, NULL, XML_PARSE_DTDVALID);
    /* check if parsing succeeded */
    if (doc == NULL) {
        fprintf(stderr, "Failed to parse %s\n", filename);
    } else {
    /* check if validation succeeded */
        if (ctxt->valid == 0)
        fprintf(stderr, "Failed to validate %s\n", filename);
    /* free up the resulting document */
    xmlFreeDoc(doc);
    }
    /* free up the parser context */
    xmlFreeParserCtxt(ctxt);
}

int main(int argc, char **argv) {
    if (argc != 2)
        return(1);
    /*
     * this initialize the library and check potential ABI mismatches
     * between the version it was compiled for and the actual shared
     * library used.
     */
    LIBXML_TEST_VERSION
    exampleFunc(argv[1]);
    /*
     * Cleanup function for the XML library.
     */
    xmlCleanupParser();
    /*
     * this is to debug memory for regression tests
     */
    xmlMemoryDump();
    return(0);
}

程序逻辑极为简单,先从命令行参数中读取一个 filename,构造一个全新的 xmlParserCtxtPtr 实例,然后从 filename 中去文本并解析,解析的过程由 xmlParserCtxtPtr 实例追踪。

接着编译 libxml2 的库和 harness 可执行程序:

cd libxml2
CC=afl-clang-fast ./autogen.sh
AFL_USE_ASAN=1 make -j 4
cd ..
AFL_USE_ASAN=1 afl-clang-fast ./harness.c -I libxml2/include libxml2/.libs/libxml2.a -lz -lm -o fuzzer

然后开始 fuzz:

AFL_SKIP_CPUFREQ=1 afl-fuzz -i inputs/ -o outputs/ ./fuzzer @@

Fuzz result#

image.png

image.png

Persistent Mode#

参考答案中给出的 harness 使用了 AFL-LLVM 的 Persistent Mode

在最基础的 AFL 中,fuzzer 主程序会 fork 一个子进程作为 fork_server。fuzz 过程不可避免多次执行目标(target)程序,为了避免多次 exec 装载目标程序带来的系统开销,AFL 让 fork_server 执行 fork 命令来产生新的 target

为了进一步降低 fork 带来的开销,persistent mode 允许 harness 代码中显式地循环调用被测 API,即在 harness 的 main 函数中以这样的形式进行:

while (__AFL_LOOP(1000)) {
	/* Setup function call, e.g. struct target *tmp = libtarget_init() */
    /* Call function to be fuzzed, e.g.: */
    target_function(buf, len);
    /* Reset state. e.g. libtarget_free(tmp) */
}

编译的 XML 文档由内存中产生,完全可以省去写入文件的操作,直接输入到 fuzzer 中。在 AFL 中,测试用例经过文件重定向到 STDIN,然后由 target 程序读取。AFL++ 提供了共享内存通道,实现 fuzzer 和 target 之间的测试用例传递。要利用这一特性,只需要声明 __AFL_FUZZ_INIT(); 这个宏即可。

image

此外,虽然通过 persistent mode 能够避免每次都执行 target 的初始化代码,但是这些初始化代码依然会在 fork_server fork target 进程时被执行。为了复用程序运行了这些初始化代码之后的状态,AFL++ 的方法是将 fork server 的初始化(从原本的程序开头)延迟到用户指定位置。

/* This one can be called from user code when deferred forkserver mode
    is enabled. */
void __afl_manual_init(void) {
  static u8 init_done;
  if (...) {...}
  if (!init_done) {
    __afl_start_forkserver();
    init_done = 1;
  }
}

结合上述三个特性:persistent mode,shared memory fuzzing 和 deferred initialization,最终组成了 libxml2 的 harness。其中 __AFL_FUZZ_TESTCASE_BUF__AFL_FUZZ_TESTCASE_LEN 这两个宏分别表示共享内存地址和用例所占内存大小。

#include "libxml/parser.h"
#include "libxml/tree.h"
#include <unistd.h>
__AFL_FUZZ_INIT();
int main(int argc, char **argv) {
    #ifdef __AFL_HAVE_MANUAL_CONTROL
        __AFL_INIT();
    #endif
    unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;  // must be after __AFL_INIT
    xmlInitParser();
    while (__AFL_LOOP(1000)) {
        int len = __AFL_FUZZ_TESTCASE_LEN;
        xmlDocPtr doc = xmlReadMemory((char *)buf, len, "https://mykter.com", NULL, 0);
        if (doc != NULL) {
            xmlFreeDoc(doc);
        }
    }
    xmlCleanupParser();
    return(0);
}

借助 AFL 自带的 XML dictionary ,在获知一定语法的情况下进行输入文档的变异,能够进一步提升效率:

AFL_SKIP_CPUFREQ=1 afl-fuzz -i inputs/ -o outputs/ -x ~/AFLplusplus/dictionaries/xml.dict ./fuzzer

image

image.png

最后找到的 crash 用例其实数量挺多的,其中也确实有 CVE 所描述的 unterminated encoding value

image.png

Heartbleed#

CVE-2014-0160是曾经非常著名的 “心脏滴血”(heartbleed)漏洞,这个漏洞存在于 OpenSSL 1.0.1g 之前的 1.0.1 版本中。

为了加快 fuzz 进度,我在改造 handshake.cc 的过程中融入了 persistent mode、shared memory 和 deferred initialization。

#include <openssl/ssl.h>
#include <openssl/err.h>
#include <assert.h>
#include <stdint.h>
#include <stddef.h>
#include <unistd.h>
#ifndef CERT_PATH
	# define CERT_PATH
#endif

__AFL_FUZZ_INIT();

SSL_CTX *Init() {
  SSL_library_init();
  SSL_load_error_strings();
  ERR_load_BIO_strings();
  OpenSSL_add_all_algorithms();
  SSL_CTX *sctx;
  assert (sctx = SSL_CTX_new(TLSv1_method()));
  /* These two file were created with this command:
      openssl req -x509 -newkey rsa:512 -keyout server.key \
     -out server.pem -days 9999 -nodes -subj /CN=a/
  */
  assert(SSL_CTX_use_certificate_file(sctx, "server.pem",
                                      SSL_FILETYPE_PEM));
  assert(SSL_CTX_use_PrivateKey_file(sctx, "server.key",
                                     SSL_FILETYPE_PEM));
  return sctx;
}

int main() {
  SSL_CTX *sctx = Init();
  #ifdef __AFL_HAVE_MANUAL_CONTROL
    __AFL_INIT();
  #endif
  unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
  while (__AFL_LOOP(1000))
  {
    SSL *server = SSL_new(sctx);
    BIO *sinbio = BIO_new(BIO_s_mem());
    BIO *soutbio = BIO_new(BIO_s_mem());
    SSL_set_bio(server, sinbio, soutbio);
    SSL_set_accept_state(server);
    /* TODO: To spoof one end of the handshake, we need to write data to sinbio
    * here */
    int len = __AFL_FUZZ_TESTCASE_LEN;
    BIO_write(sinbio, buf, len);
    SSL_do_handshake(server);
    SSL_free(server);
  }
  SSL_CTX_free(sctx);
  return 0;
}

fuzz 的结果如图所示,怀疑 stability 较低可能于复用 SSL Context 有关。

image.png

image.png

Vulnerability#

通过 xxd 命令可以 16 进制格式输出文件数据(据说 xxd 更强大的功能是支持将修改后的数据 dump 到文件里)。

image.png

image.png

  • byte 0:SSL 数据包类型,这里 18 对应 TLS1_RT_HEARTBEAT。
  • byte 1-2:SSL 版本,这里 0301 代表 TLS1.0,0302 代表 1.1,0303 代表 1.2,这里的 03f5 因该是 fuzzer 自动生成的版本号,可能没有实际的版本与之对应,但是 TLS server 由于一些 fallback 的逻辑使得其依然能够接受这样的版本号。
  • byte 3-4:TLS 完整数据包(包括头部)的长度,这里的 0005 应该也是 fuzzer 生成的,不合理,但是该漏洞的核心问题不是出在这里。
  • byte 5:TLS heartbeat 类型,1 对应 request,2 对应 response。这里是从客户端发送给服务端的,所以是 1。
  • byte 6:heartbeat payload 长度。
  • 其他:heart payload 数据,数据长度要符合 byte 6。

按照正常的处理逻辑,server 会回显(echo)request 的 payload length 和 data,简化后的代码逻辑为:

int
tls1_process_heartbeat(SSL *s) {
	unsigned char *p = &s->s3->rrec.data[0], *pl;
	/* Read type and payload length first */
	hbtype = *p++;
	n2s(p, payload);
	pl = p;
	if (hbtype == TLS1_HB_REQUEST) {
		/* Allocate memory for the response, size is 1 bytes
		 * message type, plus 2 bytes payload length, plus
		 * payload, plus padding
		 */
		buffer = OPENSSL_malloc(1 + 2 + payload + padding);
		bp = buffer;
		/* Enter response type, length and copy payload */
		*bp++ = TLS1_HB_RESPONSE;
		s2n(payload, bp);
		memcpy(bp, pl, payload);
	}
}

如果攻击者给定一个较大的 payload length,而没有给出足够的 payload data,那么 server 会根据 payload length 来开辟 response 的缓冲区,并且从 payload data 缓冲区拷贝 payload length 长度的数据到 response 缓冲区,结果就是 payload length 紧接着之后的内存中的数据会泄露到 response 中,发送给攻击者。

sendmail#1301#

需要测试的漏洞是 CVE-1999-0206。代码仓库中已经给出了 main.c 作为 harness 代码,测试的目标函数为 mime7to8。这个函数的作用是将 base64 编码或者 quoted-printable 编码的文本解析成实际数据。

  • base64:用 64 个字符(6bit)来表达二进制数据,也就是 3 byte 的数据需要用 4 个字符来编码。
  • quoted-printable:对于不可打印的 ASCII 字符,将其 16 进制的字符表示前加上等号,用三个字符来编码。

image

AFL-training 推荐在 fuzz 时使用 persistent mode 和 multicore 并行。由于 ENVELOPE 结构中貌似没有能够直接存放字符串的数据成员,而是必须要指定一个 temporary filename,因此这里应该是不能用 shared memory 直接传递测试用例的。

除了 shared memory,deferred initialization 似乎也无法起到太大作用,因为初始化部分代码看起来挺简单的,用不了多少时间,所以我只使用了 __AFL_LOOP

#include "my-sendmail.h"
#include <assert.h>

int main(int argc, char **argv)
{
     HDR *header;
     register ENVELOPE *e;
     FILE *temp;
     while (__AFL_LOOP(1000))
     {
          temp = fopen(argv[1], "r");
          assert(temp != NULL);
          header = (HDR *)malloc(sizeof(struct header));
          header->h_field = "Content-Transfer-Encoding";
          header->h_value = "quoted-printable";
          header->h_link = NULL;
          header->h_flags = 0;

          e = (ENVELOPE *)malloc(sizeof(struct envelope));
          e->e_id = "First Entry";
          e->e_dfp = temp;

          mime7to8(header, e);
          
          fclose(temp);
          free(e);
          free(header);
     }
     return 0;
}

image.png

在寻找到产生 crash 的用例之后,用 tmin 进行精简:

0000000000000000000000=
0000000000000000000000000000000000000000=
00000000=
000000

Vulnerability#

image.png

实际导致 crash 的问题很简单,在 mime7to8 函数中,buf 和 obuf 分别是读和写的缓冲区,该函数在写 obuf 时没有对指针位置进行校验,导致指向下一个写位置的 obp “出界”。

mime7to8 中,canary 就是因为 obp 出界,导致其内容被覆盖了。若是没有 canary,那么 obp 最终会移动到 buf 的范围内,导致本应读的数据受到 “污染”。

image.png

sendmail#1305#

CVE-2003-0161

仓库中提供了 harness,我借用 persistent mode 将其改写。

// ...
__AFL_FUZZ_INIT();

int main(){
    const int MAX_MESSAGE_SIZE = 1000;
    char special_char = '\377';  /* same char as 0xff.  this char will get interpreted as NOCHAR */
    int delim = '\0';
    static char **delimptr = NULL;
    char *addr;
    OperatorChars = NULL;
    ConfigLevel = 5;
    addr = (char *) malloc(sizeof(char) * MAX_MESSAGE_SIZE);
    CurEnv = (ENVELOPE *) malloc(sizeof(struct envelope));
    CurEnv->e_to = (char *) malloc(MAX_MESSAGE_SIZE * sizeof(char) + 1);
    #ifdef __AFL_HAVE_MANUAL_CONTROL
        __AFL_INIT();
    #endif
    unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;  // must be after __AFL_INIT
    while (__AFL_LOOP(1000)) {
        int len = __AFL_FUZZ_TESTCASE_LEN;
        strncpy(addr, buf, len);
        addr[len] = '\0';
        memcpy(CurEnv->e_to, addr, len);
        CurEnv->e_to[len] = '\0';
        parseaddr(addr, delim, delimptr);
    }
	return 0;
}

在 ANSWERS.md 中,作者提示 harness 的代码中用到了一些全局变量,这些变量可能会成为程序的状态,对 persistent mode 产生影响,但是根据我的观察,只有 CurEnv 是起实际用途且与程序上下文密切相关的变量,而且 parseaddr 函数也仅包含对该变量唯一一个字段 e_to 的读操作,所以我自己改写的 persistent mode 应该是可行的。

然而这次貌似找不到 crash testcase 了。。。

image.png

date#

CVE-2017-7476

由于不知道 date.c 的 main 函数是否存在全局变量,因此我不敢随意用 __AFL_LOOP 改写,考虑到后续调试时用 STDIN 也比较方便,且用 STDIN 传入用例和 shared memory 传入用例的效率差距不大(影响效率的大头在 fork 上面),所以按照 ANSWERS.md 将 date.c 改写为了 harness,在 getenv 之前添加了设置 env 的代码。

static char val[1024 * 16];
read(0, val, sizeof(val) - 1);
setenv("TZ", val, 1);

char const *tzstring = getenv ("TZ");
timezone_t tz = tzalloc (tzstring);

image.png

Vulnerability#

Savannah Git Hosting - gnulib.git/commit 给出的解释是环境变量 TZ 的长度超过了 ABBR_SIZE_MIN (119) on x86_64,导致 extend_abbr 函数中会出现堆内存溢出的情况。

image.png

我用 GDB 跟踪了一下,确实是 extend_abbr 函数中,试图在 tzalloc 分配的 tz->abbrs 内存之后添加新的字符串,导致指针越界。

ntpq#

CVE-2009-0159

漏洞主要存在于 cookedprint(datatype, length, data, status, stdout) 函数中。对于类似的 C/S 模式的程序,我原以为需要模拟正常程序交互,先启动 ntpd 之后再在 ntpq 端进行 fuzz,考虑 cookedprint 的程序逻辑。结果 ANSWERS.md 中直接对参数进行了语义无关的 fuzz,即变异一个大的字符串作为 STDIN,然后拆分成各个参数。

datatype=0;
status=0;
memset(data,0,1024*16);
read(0, &datatype, 1);
read(0, &status, 1);
length = read(0, data, 1024 * 16);
cookedprint(datatype, length, data, status, stdout);

image.png

从代码覆盖率的角度看, 4.2.2 版本下能够触发新路径的用例,覆盖了 cookedprint 函数,但是不能覆盖 4.2.8p10 版本下的代码(##### 代表没有覆盖)。

image.png

Summary#

AFL 及其配套工具确实将模糊测试实现成了几乎 “开箱即用” 的程度,要 fuzz 一个开源程序,只需要在编译时进行插桩,然后编写恰当的 harness 程序,剩下就可以完全 “撒手” 交给 AFL。

但是我在 fuzz 时也遇到了一些难题问题:

  • 如何千方百计提高 fuzz 的效率。在用例的变异、选择、调度、执行过程中,还存在很大的效率优化空间。我在尝试 AFL-training 中的各个 challenge 时,虽然只有一个 challenge 在经过了十几个小时之后依然没有发现 crash(sendmail#1305),其他程序都能在较短的时间内发现或多或少的 crash,但是这是建立在目标函数和 crash 都较为简单的前提下的,因为漏洞的位置已知,所以可以把存在漏洞的函数单独拿出来编写 harness。在漏洞未知的实际情况下,需要进一步提高效率。
  • 如何分析 fuzz 过程中发现的 crash。AFL-training 中大部分 CVE 都是和内存溢出(overflow)相关的。对溢出最直白的理解就是指针指向了超出其应该指向的区域,但是为何会产生 “越界” 的情况,需要结合程序自身的内存分配、访问模式进行分析。对于不熟悉的程序,需要借助 GDB 等工具单步调试。
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。