首页
社区
课程
招聘
[翻译]野蛮fuzz - part 6:持久性fuzz
发表于: 2024-9-3 09:30 1211

[翻译]野蛮fuzz - part 6:持久性fuzz

2024-9-3 09:30
1211

引言

距离我上次写这类文章已经有一段时间了,今年我的目标之一是写更多的文章,所以我们再次回到了这里。我正在进行的一个副项目正逐步接近一个好的停顿点,因此我将有更多的闲暇时间来做自己的研究并再次写博客。期待今年分享更多内容。

在初学者模糊测试圈子中(显然我也是其中一员),最常见的问题之一是如何HOOK目标,使其能够在内存中进行模糊测试,有些人称之为“持久性”模糊测试,以获得更高的性能。持久性模糊测试有一个特定的用例,即当目标在不同的模糊测试用例之间不涉及太多全局状态时,这种方式会很有用。一个例子是对库中的单个API或二进制文件中的单个函数进行紧密的模糊测试循环。

这种模糊测试风格比一次又一次从头开始重新执行目标要快,因为我们绕过了与创建和销毁任务结构相关的所有繁重的系统调用/内核例程。

然而,对于没有源代码的二进制目标来说,在不进行深入逆向工程的情况下(真恶心,工作?呃),有时很难辨别在执行任何代码路径时我们影响了哪些全局状态。此外,我们通常希望模糊测试更广泛的循环。模糊测试一个返回一个结构体的函数并没有多大帮助,因为该结构体在我们的模糊测试工作流程中从未被读取或使用。考虑到这些情况,我们通常发现“快照”模糊测试对于二进制目标,甚至是那些我们有源代码但经过企业构建系统处理的生产二进制文件来说,是一种更稳健的工作流程。

因此,今天我们将学习如何将一个任意的仅二进制目标(该目标从用户那里接收输入文件)转换为一个从内存中接收输入的目标,并使其能够在不同的模糊测试用例之间重置状态。

目标(简单模式)

在这篇博客文章中,我们将使用 objdump 作为快照模糊测试的HOOK目标。这能够满足我们的需求,因为它相对简单(单线程、单进程),并且它是一个常见的模糊测试目标,尤其是在人们开发模糊测试工具时。本文的重点不是通过沙盒化像Chrome这样复杂的目标来打动你,而是向初学者展示如何开始思考HOOK目标。你需要将你的目标改造得面目全非,但保持相同的语义。你可以尽情发挥创造力,坦白说,有时候HOOK目标是与模糊测试相关的最令人满意的工作之一。成功地将目标沙盒化,并使其与模糊测试器良好配合,感觉非常棒。那么我们开始吧。

Hello World

第一步是确定我们想如何改变 objdump 的行为。让我们尝试在 strace 下运行它,并反汇编 ls,看看它在系统调用级别上如何表现,命令为 strace objdump -D /bin/ls。我们要寻找的是 objdump 开始与我们的输入(在本例中为 /bin/ls)交互的地方。在输出中,如果你滚动浏览过那些样板内容,你可以看到 /bin/ls 的首次出现:

1
2
3
4
5
stat("/bin/ls", {st_mode=S_IFREG|0755, st_size=133792, ...}) = 0
stat("/bin/ls", {st_mode=S_IFREG|0755, st_size=133792, ...}) = 0
openat(AT_FDCWD, "/bin/ls", O_RDONLY)   = 3
fcntl(3, F_GETFD)                       = 0
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0

请记住,在阅读本文时,如果你在家里跟随操作,你的输出可能不会与我的完全匹配。我可能运行的是与你不同的发行版以及不同版本的 objdump。但这篇博客文章的目的是展示一些概念,你可以自行发挥创意。

我还注意到,程序在执行结束前并不会关闭我们的输入文件:

1
2
3
4
5
6
7
read(3, "\0\0\0\0\0\0\0\0\10\0\"\0\0\0\0\0\1\0\0\0\377\377\377\377\1\0\0\0\0\0\0\0"..., 4096) = 2720
write(1, ":(%rax)\n  21ffa4:\t00 00         "..., 4096) = 4096
write(1, "x0,%eax\n  220105:\t00 00         "..., 4096) = 4096
close(3)                                = 0
write(1, "023e:\t00 00                \tadd "..., 2190) = 2190
exit_group(0)                           = ?
+++ exited with 0 +++

这点很重要,我们需要让我们的HOOK程序能很好地模拟一个输入文件,因为 objdump 并不会一次性将文件读入内存缓冲区或使用 mmap() 映射输入文件。它会在整个 strace 输出过程中不断地从文件中读取数据。

由于我们没有目标的源代码,我们将通过使用 LD_PRELOAD 共享对象来影响其行为。通过使用 LD_PRELOAD 共享对象,我们应该能够HOOK与输入文件交互的系统调用的包装函数,并修改它们的行为以满足我们的需求。如果你不熟悉动态链接或 LD_PRELOAD,现在是一个很好的时机去搜索更多信息,这是一个很好的起点。首先,我们先加载一个“Hello, World!”的共享对象。

我们可以利用 gcc 的函数属性(Function Attributes)来在共享对象被目标加载时执行代码,通过构造器属性(constructor attribute)来实现。

因此,迄今为止我们的代码将如下所示:

1
2
3
4
5
6
7
8
9
10
11
/*
Compiler flags:
gcc -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ldl
*/
 
#include <stdio.h> /* printf */
 
// Routine to be called when our shared object is loaded
__attribute__((constructor)) static void _hook_load(void) {
    printf("** LD_PRELOAD shared object loaded!\n");
}

我将编译所需的标志添加到了文件顶部作为注释。这些标志来自于我之前阅读的关于使用 LD_PRELOAD 共享对象的博客文章:https://tbrindus.ca/correct-ld-preload-hooking-libc/

现在我们可以使用 LD_PRELOAD 环境变量,并运行带有我们的共享对象的 objdump,它应该在加载时打印信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
h0mbre@ubuntu:~/blogpost$ LD_PRELOAD=/home/h0mbre/blogpost/blog_harness.so objdump -D /bin/ls > /tmp/output.txt && head -n 20 /tmp/output.txt
**> LD_PRELOAD shared object loaded!
 
/bin/ls:     file format elf64-x86-64
 
 
Disassembly of section .interp:
 
0000000000000238 <.interp>:
 238:   2f                      (bad) 
 239:   6c                      ins    BYTE PTR es:[rdi],dx
 23a:   69 62 36 34 2f 6c 64    imul   esp,DWORD PTR [rdx+0x36],0x646c2f34
 241:   2d 6c 69 6e 75          sub    eax,0x756e696c
 246:   78 2d                   js     275 <_init@@Base-0x34e3>
 248:   78 38                   js     282 <_init@@Base-0x34d6>
 24a:   36 2d 36 34 2e 73       ss sub eax,0x732e3436
 250:   6f                      outs   dx,DWORD PTR ds:[rsi]
 251:   2e 32 00                xor    al,BYTE PTR cs:[rax]
 
Disassembly of section .note.ABI-tag:

它有效,现在我们可以开始寻找需要HOOK的函数了。

寻找HOOK点

首先我们需要做的是创建一个虚假的文件名给 objdump,以便我们可以开始测试。我们将 ls 可执行文件复制到当前工作目录,并将其命名为 fuzzme。这将允许我们在测试时通用地操作HOOK程序。现在我们有了 strace 输出,我们知道 objdump 在调用 openat() 之前,会多次对输入文件路径(/bin/ls)调用 stat()。由于我们知道文件尚未被打开,并且系统调用的第一个参数使用路径,我们可以猜测这个系统调用是由 libc 导出的 stat()lstat() 包装函数引发的。我假设是 stat(),因为我们在我的机器上没有处理 /bin/ls 的符号链接。我们可以添加一个 stat() 的HOOK来测试是否命中,并检查它是否被调用来处理我们的目标输入文件(现在改为 fuzzme)。

为了创建一个HOOK,我们将遵循一个模式,即通过 typedef 定义指向真实函数的指针,然后将指针初始化为 NULL。一旦我们需要解析我们所HOOK的真实函数的位置,我们可以使用 dlsym(RLTD_NEXT, <symbol name>) 来获取它的位置,并将指针值更改为真实符号地址。(稍后这将变得更清晰)。

现在我们需要HOOK stat(),它在 man 3 中作为一个条目(意味着它是一个 libc 导出的函数),同时也在 man 2 中作为一个条目(意味着它是一个系统调用)。这让我困惑了很长时间,因为这个命名冲突,我经常误解系统调用的实际工作原理。你可以阅读我最早的研究博客文章之一,在那里这种困惑显而易见,我也经常做出错误的断言。(顺便说一下,我永远不会编辑那些有错误的旧博客文章,它们就像时间胶囊,对我来说这很酷)。

我们想要编写一个函数,当被调用时,只需打印一些信息并退出,以便我们知道我们的HOOK被命中了。目前,我们的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
Compiler flags:
gcc -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ldl
*/
 
#include <stdio.h> /* printf */
#include <sys/stat.h> /* stat */
#include <stdlib.h> /* exit */
 
// Filename of the input file we're trying to emulate
#define FUZZ_TARGET "fuzzme"
 
// Declare a prototype for the real stat as a function pointer
typedef int (*stat_t)(const char *restrict path, struct stat *restrict buf);
stat_t real_stat = NULL;
 
// Hook function, objdump will call this stat instead of the real one
int stat(const char *restrict path, struct stat *restrict buf) {
    printf("** stat() hook!\n");
    exit(0);
}
 
// Routine to be called when our shared object is loaded
__attribute__((constructor)) static void _hook_load(void) {
    printf("** LD_PRELOAD shared object loaded!\n");
}

然而,如果我们编译并运行该代码,我们并没有打印任何内容并退出,所以我们的HOOK没有被调用。出了点问题。有时候,libc 中与文件相关的函数有 64 位变体,比如 open()open64(),它们会根据配置和标志的不同而交替使用。我尝试HOOK stat64(),但仍然没有成功。

幸运的是,我并不是第一个遇到这个问题的人。Stackoverflow 上有一个很棒的答案,讨论了这个问题,解释了 libc 并没有像其他函数(如 open()open64())那样导出 stat(),而是导出了一个叫做 __xstat() 的符号。这个符号具有略微不同的签名,并需要一个名为 version 的新参数,用于描述调用者期望的 stat 结构体的版本。这一切本应在底层自动处理,但我们现在必须自己让这些“魔法”发挥作用。对于 lstat()fstat() 也是同样的规则,它们分别有 __lxstat()__fxstat()

我在这里找到了这些函数的定义。于是我们可以将 __xstat() HOOK添加到我们的共享对象中,替换掉 stat(),看看是否有不同的结果。现在我们的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
Compiler flags:
gcc -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ldl
*/
 
#include <stdio.h> /* printf */
#include <sys/stat.h> /* stat */
#include <stdlib.h> /* exit */
#include <unistd.h> /* __xstat, __fxstat */
 
// Filename of the input file we're trying to emulate
#define FUZZ_TARGET "fuzzme"
 
// Declare a prototype for the real stat as a function pointer
typedef int (*__xstat_t)(int __ver, const char *__filename, struct stat *__stat_buf);
__xstat_t real_xstat = NULL;
 
// Hook function, objdump will call this stat instead of the real one
int __xstat(int __ver, const char *__filename, struct stat *__stat_buf) {
    printf("** Hit our __xstat() hook!\n");
    exit(0);
}
 
// Routine to be called when our shared object is loaded
__attribute__((constructor)) static void _hook_load(void) {
    printf("** LD_PRELOAD shared object loaded!\n");
}

现在,如果我们运行我们的共享对象,我们得到了预期的结果,某处我们的HOOK被命中。现在我们可以帮自己一把,打印出HOOK请求的文件名,然后实际上代表调用者调用真实的 __xstat()。当我们的HOOK被命中时,我们需要通过名称解析真正的 __xstat() 的位置,所以我们会向共享对象中添加一个符号解析函数。现在我们的共享对象代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/*
Compiler flags:
gcc -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ldl
*/
 
#define _GNU_SOURCE     /* dlsym */
#include <stdio.h> /* printf */
#include <sys/stat.h> /* stat */
#include <stdlib.h> /* exit */
#include <unistd.h> /* __xstat, __fxstat */
#include <dlfcn.h> /* dlsym and friends */
 
// Filename of the input file we're trying to emulate
#define FUZZ_TARGET "fuzzme"
 
// Declare a prototype for the real stat as a function pointer
typedef int (*__xstat_t)(int __ver, const char *__filename, struct stat *__stat_buf);
__xstat_t real_xstat = NULL;
 
// Returns memory address of *next* location of symbol in library search order
static void *_resolve_symbol(const char *symbol) {
    // Clear previous errors
    dlerror();
 
    // Get symbol address
    void* addr = dlsym(RTLD_NEXT, symbol);
 
    // Check for error
    char* err = NULL;
    err = dlerror();
    if (err) {
        addr = NULL;
        printf("Err resolving '%s' addr: %s\n", symbol, err);
        exit(-1);
    }
     
    return addr;
}
 
// Hook function, objdump will call this stat instead of the real one
int __xstat(int __ver, const char *__filename, struct stat *__stat_buf) {
    // Print the filename requested
    printf("** __xstat() hook called for filename: '%s'\n", __filename);
 
    // Resolve the address of the real __xstat() on demand and only once
    if (!real_xstat) {
        real_xstat = _resolve_symbol("__xstat");
    }
 
    // Call the real __xstat() for the caller so everything keeps going
    return real_xstat(__ver, __filename, __stat_buf);
}
 
// Routine to be called when our shared object is loaded
__attribute__((constructor)) static void _hook_load(void) {
    printf("** LD_PRELOAD shared object loaded!\n");
}

好了,现在当我们运行这个代码,并检查我们的打印语句时,事情变得有趣起来了。

1
2
3
h0mbre@ubuntu:~/blogpost$ LD_PRELOAD=/home/h0mbre/blogpost/blog_harness.so objdump -D fuzzme > /tmp/output.txt && grep "** __xstat" /tmp/output.txt
** __xstat() hook called for filename: 'fuzzme'
** __xstat() hook called for filename: 'fuzzme'

现在我们可以开始享受过程了。 

__xstat() HOOK

这个HOOK的目的是欺骗 objdump,让它以为成功地对输入文件执行了 stat()。记住,我们正在制作一个快照模糊测试HOOK,所以我们的目标是不断地创建新输入并通过这个HOOK传递给 objdump。最重要的是,我们的HOOK需要能够将我们存储在内存中的可变长度输入表示为文件。每个模糊测试用例中,文件长度可能会发生变化,而我们的HOOK需要适应这一点。

此时我的想法是创建一个看起来“合法”的 stat 结构体,它通常会为我们的实际文件 fuzzme(只是 /bin/ls 的副本)返回。我们可以全局存储这个 stat 结构体,并在每个新的模糊测试用例中只更新大小字段。因此,我们的快照模糊测试工作流程的时间线可能如下所示:

  1. 当我们的共享对象被加载时,构造函数被调用。
  2. 构造函数设置一个全局的“合法” stat 结构体,我们可以为每个模糊测试用例更新这个结构体,并将其传递给试图对我们的模糊测试目标执行 stat()__xstat() 调用者。
  3. 假想的模糊测试器运行 objdump 到达快照位置。
  4. 我们的 __xstat() HOOK更新全局“合法” stat 结构体的大小字段,并将 stat 结构体复制到调用者的缓冲区中。
  5. 假想的模糊测试器将 objdump 的状态恢复到快照时的状态。
  6. 假想的模糊测试器将新的输入复制到HOOK,并更新输入大小。
  7. 我们的 __xstat() HOOK再次被调用,我们重复步骤 4,这个过程会无休止地进行下去。

因此,我们可以想象模糊测试器有类似这样的伪代码例程,尽管它可能是跨进程的,并且需要 process_vm_writev

1
2
3
4
insert_fuzzcase(config.input_location, config.input_size_location, input, input_size) {
  memcpy(config.input_location, &input, input_size);
  memcpy(config.input_size_location, &input_size, sizeof(size_t));
}

一个重要的事项是,如果快照模糊测试器在每次模糊测试迭代时都将 objdump 恢复到其快照状态,我们必须小心不要依赖任何全局的可变内存。全局的 stat 结构体是安全的,因为它将在构造函数期间实例化,然而,模糊测试器的快照恢复例程将在每次模糊测试迭代时将其 size 字段恢复到原始值。

我们还需要一个全局的、可识别的地址来存储可变的全局数据,比如当前输入的大小。几个快照模糊测试器具有忽略连续内存范围以用于恢复目的的灵活性。因此,如果我们能够在可识别的地址上创建一些连续的内存缓冲区,我们可以让我们的假想模糊测试器在快照恢复时忽略这些内存范围。因此,我们需要有一个地方来存储输入,以及有关其大小的信息。然后我们可以以某种方式告诉模糊测试器这些位置,当它生成新输入时,它会将其复制到输入位置,然后更新当前输入大小信息。

所以现在我们的构造函数有了一个额外的任务:设置输入位置以及输入大小信息。我们可以通过调用 mmap() 来轻松实现这一点,这使我们能够通过 MAP_FIXED 标志指定我们希望将映射映射到的地址。我们还会创建一个 MAX_INPUT_SZ 定义,以便我们知道从输入位置映射多少内存。

仅仅是与映射输入本身及其大小信息相关的函数看起来是这样的。请注意,我们使用了 MAP_FIXED 并检查了 mmap() 的返回地址,以确保调用成功但没有将我们的内存映射到不同的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Map memory to hold our inputs in memory and information about their size
static void _create_mem_mappings(void) {
    void *result = NULL;
 
    // Map the page to hold the input size
    result = mmap(
        (void *)(INPUT_SZ_ADDR),
        sizeof(size_t),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_SZ_ADDR)) {
        printf("Err mapping INPUT_SZ_ADDR, mapped @ %p\n", result);
        exit(-1);
    }
 
    // Let's actually initialize the value at the input size location as well
    *(size_t *)INPUT_SZ_ADDR = 0;
 
    // Map the pages to hold the input contents
    result = mmap(
        (void *)(INPUT_ADDR),
        (size_t)(MAX_INPUT_SZ),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_ADDR)) {
        printf("Err mapping INPUT_ADDR, mapped @ %p\n", result);
        exit(-1);
    }
 
    // Init the value
    memset((void *)INPUT_ADDR, 0, (size_t)MAX_INPUT_SZ);
}

mmap() 实际上会映射系统上页面大小的倍数(通常是 4096 字节)。所以,当我们要求映射 sizeof(size_t) 字节时,mmap() 就像:“嗯,这只是一页,兄弟”,并且从 0x13360000x1337000 给了我们一整页(不包括高端)。

随机附带提一下,在定义和宏中进行算术运算时要小心,就像我在这里用 MAX_INPUT_SIZE 所做的那样,预处理器很容易将你的文本替换为定义关键字,从而破坏一些操作顺序,甚至溢出特定的基本类型,如 int

现在我们已经为模糊测试器设置了存储输入及其大小信息的内存,我们可以创建那个全局的 stat 结构体了。但是我们实际上遇到了一个大问题。如果我们已经HOOK了 __xstat(),我们如何调用 __xstat() 以获取我们的“合法” stat 结构体呢?我们会命中自己的HOOK。为了解决这个问题,我们可以使用一个特殊的 __ver 参数调用 __xstat(),我们知道这意味着它是从构造函数中调用的,变量是一个 int 类型,所以我们可以使用 0x1337 作为特殊值。这样,在我们的HOOK中,如果我们检查 __ver 并且它是 0x1337,我们就知道它是从构造函数中调用的,我们可以实际对我们的真实文件进行 stat 调用并创建一个全局的“合法” stat 结构体。当我转储 objdump__xstat() 的正常调用时,__version 总是值 1,所以我们会在HOOK中将其修补回去。现在我们整个共享对象的源文件应该如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/*
Compiler flags:
gcc -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ldl
*/
 
#define _GNU_SOURCE     /* dlsym */
#include <stdio.h> /* printf */
#include <sys/stat.h> /* stat */
#include <stdlib.h> /* exit */
#include <unistd.h> /* __xstat, __fxstat */
#include <dlfcn.h> /* dlsym and friends */
#include <sys/mman.h> /* mmap */
#include <string.h> /* memset */
 
// Filename of the input file we're trying to emulate
#define FUZZ_TARGET "fuzzme"
 
// Definitions for our in-memory inputs
#define INPUT_SZ_ADDR   0x1336000
#define INPUT_ADDR      0x1337000
#define MAX_INPUT_SZ    (1024 * 1024)
 
// Our "legit" global stat struct
struct stat st;
 
// Declare a prototype for the real stat as a function pointer
typedef int (*__xstat_t)(int __ver, const char *__filename, struct stat *__stat_buf);
__xstat_t real_xstat = NULL;
 
// Returns memory address of *next* location of symbol in library search order
static void *_resolve_symbol(const char *symbol) {
    // Clear previous errors
    dlerror();
 
    // Get symbol address
    void* addr = dlsym(RTLD_NEXT, symbol);
 
    // Check for error
    char* err = NULL;
    err = dlerror();
    if (err) {
        addr = NULL;
        printf("Err resolving '%s' addr: %s\n", symbol, err);
        exit(-1);
    }
     
    return addr;
}
 
// Hook for __xstat
int __xstat(int __ver, const char* __filename, struct stat* __stat_buf) {
    // Resolve the real __xstat() on demand and maybe multiple times!
    if (NULL == real_xstat) {
        real_xstat = _resolve_symbol("__xstat");
    }
 
    // Assume the worst, always
    int ret = -1;
 
    // Special __ver value check to see if we're calling from constructor
    if (0x1337 == __ver) {
        // Patch back up the version value before sending to real xstat
        __ver = 1;
 
        ret = real_xstat(__ver, __filename, __stat_buf);
 
        // Set the real_xstat back to NULL
        real_xstat = NULL;
        return ret;
    }
 
    // Determine if we're stat'ing our fuzzing target
    if (!strcmp(__filename, FUZZ_TARGET)) {
        // Update our global stat struct
        st.st_size = *(size_t *)INPUT_SZ_ADDR;
 
        // Send it back to the caller, skip syscall
        memcpy(__stat_buf, &st, sizeof(struct stat));
        ret = 0;
    }
 
    // Just a normal stat, send to real xstat
    else {
        ret = real_xstat(__ver, __filename, __stat_buf);
    }
 
    return ret;
}
 
// Map memory to hold our inputs in memory and information about their size
static void _create_mem_mappings(void) {
    void *result = NULL;
 
    // Map the page to hold the input size
    result = mmap(
        (void *)(INPUT_SZ_ADDR),
        sizeof(size_t),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_SZ_ADDR)) {
        printf("Err mapping INPUT_SZ_ADDR, mapped @ %p\n", result);
        exit(-1);
    }
 
    // Let's actually initialize the value at the input size location as well
    *(size_t *)INPUT_SZ_ADDR = 0;
 
    // Map the pages to hold the input contents
    result = mmap(
        (void *)(INPUT_ADDR),
        (size_t)(MAX_INPUT_SZ),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_ADDR)) {
        printf("Err mapping INPUT_ADDR, mapped @ %p\n", result);
        exit(-1);
    }
 
    // Init the value
    memset((void *)INPUT_ADDR, 0, (size_t)MAX_INPUT_SZ);
}
 
// Routine to be called when our shared object is loaded
__attribute__((constructor)) static void _hook_load(void) {
    // Create memory mappings to hold our input and information about its size
    _create_mem_mappings();   
}

现在,如果我们运行这个代码,我们得到如下输出:

1
2
h0mbre@ubuntu:~/blogpost$ LD_PRELOAD=/home/h0mbre/blogpost/blog_harness.so objdump -D fuzzme
objdump: Warning: 'fuzzme' is not an ordinary file

这很酷,这意味着 objdump 的开发人员做了一些正确的事情,他们的 stat() 会说:“嘿,这个文件长度为零,发生了什么怪事。”然后他们会输出这个错误消息并退出。干得好,开发者们!

所以我们已经确定了一个问题,我们需要模拟模糊测试器将一个真实的输入放入内存中,为此,我将开始使用 #ifdef 来定义我们是否在测试我们的共享对象。所以基本上,如果我们编译共享对象并定义 TEST,我们的共享对象将复制一个“输入”到内存中,以模拟模糊测试器在模糊测试期间的行为,我们可以看看我们的HOOK是否工作得当。所以如果我们定义了 TEST,我们将把 /bin/ed 复制到内存中,并更新我们的全局“合法” stat 结构体的大小成员,并将 /bin/ed 的字节放入内存中。

你现在可以按照如下方式编译共享对象以进行测试:

1
gcc -D TEST -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ld

我们还需要设置我们的全局“合法” stat 结构体,代码应如下所示。记住,我们传递一个假的 __ver 变量让 __xstat() HOOK知道是在构造函数例程中调用的,从而允许HOOK正常运行并为我们提供所需的 stat 结构体:

1
2
3
4
5
6
7
8
9
10
// Create a "legit" stat struct globally to pass to callers
static void _setup_stat_struct(void) {
    // Create a global stat struct for our file in case someone asks, this way
    // when someone calls stat() or fstat() on our target, we can just return the
    // slightly altered (new size) stat struct &skip the kernel, save syscalls
    int result = __xstat(0x1337, FUZZ_TARGET, &st);
    if (-1 == result) {
        printf("Error creating stat struct for '%s' during load\n", FUZZ_TARGET);
    }
}

总的来说,我们的整个HOOK现在看起来是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/*
Compiler flags:
gcc -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ldl
*/
 
#define _GNU_SOURCE     /* dlsym */
#include <stdio.h> /* printf */
#include <sys/stat.h> /* stat */
#include <stdlib.h> /* exit */
#include <unistd.h> /* __xstat, __fxstat */
#include <dlfcn.h> /* dlsym and friends */
#include <sys/mman.h> /* mmap */
#include <string.h> /* memset */
#include <fcntl.h> /* open */
 
// Filename of the input file we're trying to emulate
#define FUZZ_TARGET     "fuzzme"
 
// Definitions for our in-memory inputs
#define INPUT_SZ_ADDR   0x1336000
#define INPUT_ADDR      0x1337000
#define MAX_INPUT_SZ    (1024 * 1024)
 
// For testing purposes, we read /bin/ed into our input buffer to simulate
// what the fuzzer would do
#define  TEST_FILE      "/bin/ed"
 
// Our "legit" global stat struct
struct stat st;
 
// Declare a prototype for the real stat as a function pointer
typedef int (*__xstat_t)(int __ver, const char *__filename, struct stat *__stat_buf);
__xstat_t real_xstat = NULL;
 
// Returns memory address of *next* location of symbol in library search order
static void *_resolve_symbol(const char *symbol) {
    // Clear previous errors
    dlerror();
 
    // Get symbol address
    void* addr = dlsym(RTLD_NEXT, symbol);
 
    // Check for error
    char* err = NULL;
    err = dlerror();
    if (err) {
        addr = NULL;
        printf("Err resolving '%s' addr: %s\n", symbol, err);
        exit(-1);
    }
     
    return addr;
}
 
// Hook for __xstat
int __xstat(int __ver, const char* __filename, struct stat* __stat_buf) {
    // Resolve the real __xstat() on demand and maybe multiple times!
    if (!real_xstat) {
        real_xstat = _resolve_symbol("__xstat");
    }
 
    // Assume the worst, always
    int ret = -1;
 
    // Special __ver value check to see if we're calling from constructor
    if (0x1337 == __ver) {
        // Patch back up the version value before sending to real xstat
        __ver = 1;
 
        ret = real_xstat(__ver, __filename, __stat_buf);
 
        // Set the real_xstat back to NULL
        real_xstat = NULL;
        return ret;
    }
 
    // Determine if we're stat'ing our fuzzing target
    if (!strcmp(__filename, FUZZ_TARGET)) {
        // Update our global stat struct
        st.st_size = *(size_t *)INPUT_SZ_ADDR;
 
        // Send it back to the caller, skip syscall
        memcpy(__stat_buf, &st, sizeof(struct stat));
        ret = 0;
    }
 
    // Just a normal stat, send to real xstat
    else {
        ret = real_xstat(__ver, __filename, __stat_buf);
    }
 
    return ret;
}
 
// Map memory to hold our inputs in memory and information about their size
static void _create_mem_mappings(void) {
    void *result = NULL;
 
    // Map the page to hold the input size
    result = mmap(
        (void *)(INPUT_SZ_ADDR),
        sizeof(size_t),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_SZ_ADDR)) {
        printf("Err mapping INPUT_SZ_ADDR, mapped @ %p\n", result);
        exit(-1);
    }
 
    // Let's actually initialize the value at the input size location as well
    *(size_t *)INPUT_SZ_ADDR = 0;
 
    // Map the pages to hold the input contents
    result = mmap(
        (void *)(INPUT_ADDR),
        (size_t)(MAX_INPUT_SZ),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_ADDR)) {
        printf("Err mapping INPUT_ADDR, mapped @ %p\n", result);
        exit(-1);
    }
 
    // Init the value
    memset((void *)INPUT_ADDR, 0, (size_t)MAX_INPUT_SZ);
}
 
// Create a "legit" stat struct globally to pass to callers
static void _setup_stat_struct(void) {
    int result = __xstat(0x1337, FUZZ_TARGET, &st);
    if (-1 == result) {
        printf("Error creating stat struct for '%s' during load\n", FUZZ_TARGET);
    }
}
 
// Used for testing, load /bin/ed into the input buffer and update its size info
#ifdef TEST
static void _test_func(void) {   
    // Open TEST_FILE for reading
    int fd = open(TEST_FILE, O_RDONLY);
    if (-1 == fd) {
        printf("Failed to open '%s' during test\n", TEST_FILE);
        exit(-1);
    }
 
    // Attempt to read max input buf size
    ssize_t bytes = read(fd, (void*)INPUT_ADDR, (size_t)MAX_INPUT_SZ);
    close(fd);
 
    // Update the input size
    *(size_t *)INPUT_SZ_ADDR = (size_t)bytes;
}
#endif
 
// Routine to be called when our shared object is loaded
__attribute__((constructor)) static void _hook_load(void) {
    // Create memory mappings to hold our input and information about its size
    _create_mem_mappings();
 
    // Setup global "legit" stat struct
    _setup_stat_struct();
 
    // If we're testing, load /bin/ed up into our input buffer and update size
#ifdef TEST
    _test_func();
#endif
}

现在,如果我们在 strace 下运行这个程序,我们注意到我们的两个 stat() 调用明显不见了。

1
2
3
4
close(3)                                = 0
openat(AT_FDCWD, "fuzzme", O_RDONLY)    = 3
fcntl(3, F_GETFD)                       = 0
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0

我们不再看到 openat() 之前的 stat() 调用,并且程序没有在任何显著的地方崩溃。因此,这个HOOK似乎工作得很好。我们现在需要处理 openat(),并确保我们实际上不与输入文件交互,而是欺骗 objdump 与内存中的输入交互。

寻找HOOK openat() 的方法

我的非专家直觉告诉我,可能有几种方式使得 libc 函数在底层调用 openat()。这些方式可能包括包装函数 open() 以及 fopen()。我们还需要注意它们的 64 位变体(open64()fopen64())。我决定首先尝试HOOK fopen()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Declare prototype for the real fopen and its friend fopen64
typedef FILE* (*fopen_t)(const char* pathname, const char* mode);
fopen_t real_fopen = NULL;
 
typedef FILE* (*fopen64_t)(const char* pathname, const char* mode);
fopen64_t real_fopen64 = NULL;
 
...
 
// Exploratory hooks to see if we're using fopen() related functions to open
// our input file
FILE* fopen(const char* pathname, const char* mode) {
    printf("** fopen() called for '%s'\n", pathname);
    exit(0);
}
 
FILE* fopen64(const char* pathname, const char* mode) {
    printf("** fopen64() called for '%s'\n", pathname);
    exit(0);
}

如果我们编译并运行我们的探索性HOOK,我们得到以下输出:

1
2
h0mbre@ubuntu:~/blogpost$ LD_PRELOAD=/home/h0mbre/blogpost/blog_harness.so objdump -D fuzzme
** fopen64() called for 'fuzzme'

Bingo,成功找到目标。

所以现在我们可以稍微完善一下这个HOOK函数,使其按我们的预期运行。

改进 fopen64() HOOK

fopen64() 的定义是:FILE *fopen(const char *restrict pathname, const char *restrict mode);。返回的 FILE * 对我们来说有点麻烦,因为这是一个不透明的数据结构,调用者不应理解它的内容。也就是说,调用者不应访问这个数据结构的任何成员,也不应担心它的布局。你只需要将返回的 FILE * 作为一个对象传递给其他函数,例如 fclose()。系统在这些相关函数中处理数据结构,这样程序员就不必担心特定的实现。

我们实际上并不知道返回的 FILE * 将如何被使用,它可能根本不会被使用,或者可能会被传递给诸如 fread() 之类的函数,因此我们需要一种方式返回一个令人信服的 FILE * 数据结构给调用者,这个数据结构实际上是从我们内存中的输入构建的,而不是从输入文件构建的。幸运的是,有一个叫做 fmemopen()libc 函数,其行为与 fopen() 非常相似,也返回一个 FILE *。所以我们可以继续创建一个 FILE * 返回给 fopen64() 的调用者,目标输入文件是 fuzzme。感谢 @domenuk 向我展示了 fmemopen(),我以前从未遇到过它。

不过有一个关键区别。fopen() 实际上会为底层文件获取文件描述符,而 fmemopen() 因为它实际上并未打开文件,所以不会。因此,在 FILE * 数据结构中的某处,如果它是从 fopen() 返回的,则存在一个底层文件的文件描述符,而如果它是从 fmemopen() 返回的,则不存在。这一点非常重要,因为诸如 int fileno(FILE *stream) 之类的函数可以解析一个 FILE * 并将其底层文件描述符返回给调用者。objdump 可能出于某种原因想要这样做,我们需要能够稳健地处理这个问题。因此,我们需要一种方法来知道有人是否试图使用我们伪造的 FILE * 的底层文件描述符。

我的想法是简单地找到 fmemopen() 返回的 FILE * 中包含文件描述符的结构成员,并将其更改为类似 1337 这样的荒谬值,这样如果 objdump 试图使用该文件描述符,我们就会知道它的来源,并可以尝试HOOK与该文件描述符的任何交互。现在我们的 fopen64() HOOK应该看起来如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Our fopen hook, return a FILE* to the caller, also, if we are opening our
// target make sure we're not able to write to the file
FILE* fopen64(const char* pathname, const char* mode) {
    // Resolve symbol on demand and only once
    if (NULL == real_fopen64) {
        real_fopen64 = _resolve_symbol("fopen64");
    }
 
    // Check to see what file we're opening
    FILE* ret = NULL;
    if (!strcmp(FUZZ_TARGET, pathname)) {
        // We're trying to open our file, make sure it's a read-only mode
        if (strcmp(mode, "r")) {
            printf("Attempt to open fuzz-target in illegal mode: '%s'\n", mode);
            exit(-1);
        }
 
        // Open shared memory FILE* and return to caller
        ret = fmemopen((void*)INPUT_ADDR, *(size_t*)INPUT_SZ_ADDR, mode);
         
        // Make sure we've never fopen()'d our fuzzing target before
        if (faked_fp) {
            printf("Attempting to fopen64() fuzzing target more than once\n");
            exit(-1);
        }
 
        // Update faked_fp
        faked_fp = ret;
 
        // Change the filedes to something we know
        ret->_fileno = 1337;
    }
 
    // We're not opening our file, send to regular fopen
    else {
        ret = real_fopen64(pathname, mode);
    }
 
    // Return FILE stream ptr to caller
    return ret;
}

你可以看到我们:

  • 如果符号位置尚未解析,则解析它
  • 检查我们是否正在对模糊测试目标输入文件进行调用
  • 调用 fmemopen() 并打开内存缓冲区,其中包含我们当前的输入及其大小

你可能还注意到了一些安全检查,以确保事情不会被忽视。我们有一个全局变量 FILE *faked_fp,我们将其初始化为 NULL,这让我们知道是否多次打开了我们的输入(在后续尝试打开时它将不再是 NULL)。

我们还检查了 mode 参数,以确保我们得到的是一个只读的 FILE *。我们不希望 objdump 修改我们的输入或以任何方式写入它,如果它试图这样做,我们需要知道。

此时运行我们的共享对象会产生以下输出:

1
2
h0mbre@ubuntu:~/blogpost$ LD_PRELOAD=/home/h0mbre/blogpost/blog_harness.so objdump -D fuzzme
objdump: fuzzme: Bad file descriptor

我的直觉告诉我,有什么东西试图与文件描述符 1337 进行交互。让我们再次在 strace 下运行,看看会发生什么。

1
h0mbre@ubuntu:~/blogpost$ strace -E LD_PRELOAD=/home/h0mbre/blogpost/blog_harness.so objdump -D fuzzme > /tmp/output.txt

在输出中,我们可以看到一些系统调用 fcntl()fstat() 都是用文件描述符 1337 调用的,该描述符显然不存在于我们的 objdump 进程中,因此我们已经能够找到问题所在。

1
2
3
4
fcntl(1337, F_GETFD)                    = -1 EBADF (Bad file descriptor)
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=4*1024, rlim_max=4*1024}) = 0
fstat(1337, 0x7fff4bf54c90)             = -1 EBADF (Bad file descriptor)
fstat(1337, 0x7fff4bf54bf0)             = -1 EBADF (Bad file descriptor)

正如我们已经了解到的,libc 中没有直接导出的 fstat(),它像 stat() 一样是那种奇怪的函数,我们实际上必须HOOK __fxstat()。所以让我们尝试HOOK它,看看它是否会被调用用于我们的 1337 文件描述符。HOOK函数的初始代码如下:

1
2
3
4
5
6
7
8
9
10
11
// Declare prototype for the real __fxstat
typedef int (*__fxstat_t)(int __ver, int __filedesc, struct stat *__stat_buf);
__fxstat_t real_fxstat = NULL;
 
...
 
// Hook for __fxstat
int __fxstat (int __ver, int __filedesc, struct stat *__stat_buf) {
    printf("** __fxstat() called for __filedesc: %d\n", __filedesc);
    exit(0);
}

现在我们还需要处理 fcntl(),幸运的是,这个HOOK比较简单。如果有人请求 F_GETFD(即与那个特殊的 1337 文件描述符关联的标志),我们只需返回 O_RDONLY,因为它是以这些标志“打开”的,如果有人为不同的文件描述符调用它,我们暂时只会触发一个 panic。这个HOOK如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Declare prototype for the real __fcntl
typedef int (*fcntl_t)(int fildes, int cmd, ...);
fcntl_t real_fcntl = NULL;
 
...
 
// Hook for fcntl
int fcntl(int fildes, int cmd, ...) {
    // Resolve fcntl symbol if needed
    if (NULL == real_fcntl) {
        real_fcntl = _resolve_symbol("fcntl");
    }
 
    if (fildes == 1337) {
        return O_RDONLY;
    }
 
    else {
        printf("** fcntl() called for real file descriptor\n");
        exit(0);
    }
}

现在在 strace 下运行时,fcntl() 调用如预期那样消失了:

1
2
3
4
5
6
7
8
9
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=26376, ...}) = 0
mmap(NULL, 26376, PROT_READ, MAP_SHARED, 3, 0) = 0x7ff61d331000
close(3)                                = 0
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=4*1024, rlim_max=4*1024}) = 0
fstat(1, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
write(1, "** __fxstat() called for __filed"..., 42) = 42
exit_group(0)                           = ?
+++ exited with 0 +++

现在我们可以完善我们的 __fxstat() HOOK逻辑。调用者希望通过传递特殊的文件描述符 1337 从函数中获取一个针对模糊测试目标 fuzzmestat 结构体。幸运的是,我们有一个全局的 stat 结构体,在我们更新其大小以匹配内存中当前输入的大小(由我们和模糊测试器通过 INPUT_SIZE_ADDR 处的值进行跟踪)之后,我们可以返回这个结构体。因此,如果被调用,我们只需更新 stat 结构体的大小,并将我们的结构体通过 memcpy 复制到调用者的 *__stat_buf 中。我们完整的HOOK代码现在如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Hook for __fxstat
int __fxstat (int __ver, int __filedesc, struct stat *__stat_buf) {
    // Resolve the real fxstat
    if (NULL == real_fxstat) {
        real_fxstat = _resolve_symbol("__fxstat");
    }
 
    int ret = -1;
 
    // Check to see if we're stat'ing our fuzz target
    if (1337 == __filedesc) {
        // Patch the global struct with current input size
        st.st_size = *(size_t*)INPUT_SZ_ADDR;
 
        // Copy global stat struct back to caller
        memcpy(__stat_buf, &st, sizeof(struct stat));
        ret = 0;
    }
 
    // Normal stat, send to real fxstat
    else {
        ret = real_fxstat(__ver, __filedesc, __stat_buf);
    }
 
    return ret;
}

现在,如果我们运行这个代码,程序实际上不会崩溃,并且 objdump 可以在 strace 下干净地退出。

总结

为了测试我们是否做得不错,我们将输出 objdump -D fuzzme 到一个文件中,然后我们将加载我们的HOOK共享对象并输出相同的命令。最后,我们将运行 objdump -D /bin/ed 并输出到一个文件中,以查看我们的HOOK是否生成了相同的输出。

1
2
3
h0mbre@ubuntu:~/blogpost$ objdump -D fuzzme > /tmp/fuzzme_original.txt     
h0mbre@ubuntu:~/blogpost$ LD_PRELOAD=/home/h0mbre/blogpost/blog_harness.so objdump -D fuzzme > /tmp/harness.txt
h0mbre@ubuntu:~/blogpost$ objdump -D /bin/ed > /tmp/ed.txt

然后我们对这些文件进行 sha1sum

1
2
3
4
h0mbre@ubuntu:~/blogpost$ sha1sum /tmp/fuzzme_original.txt /tmp/harness.txt /tmp/ed.txt
938518c86301ab00ddf6a3ef528d7610fa3fd05a  /tmp/fuzzme_original.txt
add4e6c3c298733f48fbfe143caee79445c2f196  /tmp/harness.txt
10454308b672022b40f6ce5e32a6217612b462c8  /tmp/ed.txt

我们实际上得到了三个不同的哈希值,我们希望HOOK和 /bin/ed 输出相同的结果,因为 /bin/ed 是我们加载到内存中的输入。

1
2
3
4
5
6
h0mbre@ubuntu:~/blogpost$ ls -laht /tmp
total 14M
drwxrwxrwt 28 root   root   128K Apr  3 08:44 .
-rw-rw-r--  1 h0mbre h0mbre 736K Apr  3 08:43 ed.txt
-rw-rw-r--  1 h0mbre h0mbre 736K Apr  3 08:43 harness.txt
-rw-rw-r--  1 h0mbre h0mbre 2.2M Apr  3 08:42 fuzzme_original.txt

啊,它们的长度至少是一样的,这意味着一定存在一些细微的差异,diff 命令显示了哈希值不同的原因:

1
2
3
4
5
h0mbre@ubuntu:~/blogpost$ diff /tmp/ed.txt /tmp/harness.txt
2c2
< /bin/ed:     file format elf64-x86-64
---
> fuzzme:     file format elf64-x86-64

argv[] 数组中的文件名不同,这是唯一的区别。最终,我们能够向 objdump 提供一个输入文件,但实际上它从我们HOOK中的内存缓冲区获取输入。

还有一件事情,我们实际上忘记了 objdump 关闭了我们的文件,不是吗!于是我添加了一个快速的 fclose() HOOK。如果 fclose() 只是想释放与 fmemopen() 返回的 FILE * 关联的堆内存,我们不会有任何问题;然而,它可能还会尝试调用 close() 关闭那个奇怪的文件描述符,而我们不希望这样做。最终这可能都无关紧要,但为了安全起见还是加上了。读者可以自行实验,看看会产生什么变化。假想的模糊测试器应该会在其快照恢复例程中恢复 FILE * 的堆内存。

结论

有无数种方法可以实现这个目标,我只是想带你们走一遍我的思考过程。实际上,有很多很酷的事情你可以用这个HOOK做,其中一件事是HOOK malloc(),让它在大规模分配时失败,这样我就不会浪费模糊测试周期在最终会超时的事情上。你还可以创建一个 at_exit() 卡点,这样无论如何,程序每次退出时都会执行你的 at_exit() 函数,这对于快照重置很有用,尤其是在程序可能有多个退出路径的情况下,因为你只需要覆盖一个退出点。

希望这对某些人有帮助!HOOK的完整代码在下面,祝模糊测试顺利!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
/*
Compiler flags:
gcc -shared -Wall -Werror -fPIC blog_harness.c -o blog_harness.so -ldl
*/
 
#define _GNU_SOURCE     /* dlsym */
#include <stdio.h> /* printf */
#include <sys/stat.h> /* stat */
#include <stdlib.h> /* exit */
#include <unistd.h> /* __xstat, __fxstat */
#include <dlfcn.h> /* dlsym and friends */
#include <sys/mman.h> /* mmap */
#include <string.h> /* memset */
#include <fcntl.h> /* open */
 
// Filename of the input file we're trying to emulate
#define FUZZ_TARGET     "fuzzme"
 
// Definitions for our in-memory inputs
#define INPUT_SZ_ADDR   0x1336000
#define INPUT_ADDR      0x1337000
#define MAX_INPUT_SZ    (1024 * 1024)
 
// For testing purposes, we read /bin/ed into our input buffer to simulate
// what the fuzzer would do
#define  TEST_FILE      "/bin/ed"
 
// Our "legit" global stat struct
struct stat st;
 
// FILE * returned to callers of fopen64()
FILE *faked_fp = NULL;
 
// Declare a prototype for the real stat as a function pointer
typedef int (*__xstat_t)(int __ver, const char *__filename, struct stat *__stat_buf);
__xstat_t real_xstat = NULL;
 
// Declare prototype for the real fopen and its friend fopen64
typedef FILE* (*fopen_t)(const char* pathname, const char* mode);
fopen_t real_fopen = NULL;
 
typedef FILE* (*fopen64_t)(const char* pathname, const char* mode);
fopen64_t real_fopen64 = NULL;
 
// Declare prototype for the real __fxstat
typedef int (*__fxstat_t)(int __ver, int __filedesc, struct stat *__stat_buf);
__fxstat_t real_fxstat = NULL;
 
// Declare prototype for the real __fcntl
typedef int (*fcntl_t)(int fildes, int cmd, ...);
fcntl_t real_fcntl = NULL;
 
// Returns memory address of *next* location of symbol in library search order
static void *_resolve_symbol(const char *symbol) {
    // Clear previous errors
    dlerror();
 
    // Get symbol address
    void* addr = dlsym(RTLD_NEXT, symbol);
 
    // Check for error
    char* err = NULL;
    err = dlerror();
    if (err) {
        addr = NULL;
        printf("** Err resolving '%s' addr: %s\n", symbol, err);
        exit(-1);
    }
     
    return addr;
}
 
// Hook for __xstat
int __xstat(int __ver, const char* __filename, struct stat* __stat_buf) {
    // Resolve the real __xstat() on demand and maybe multiple times!
    if (!real_xstat) {
        real_xstat = _resolve_symbol("__xstat");
    }
 
    // Assume the worst, always
    int ret = -1;
 
    // Special __ver value check to see if we're calling from constructor
    if (0x1337 == __ver) {
        // Patch back up the version value before sending to real xstat
        __ver = 1;
 
        ret = real_xstat(__ver, __filename, __stat_buf);
 
        // Set the real_xstat back to NULL
        real_xstat = NULL;
        return ret;
    }
 
    // Determine if we're stat'ing our fuzzing target
    if (!strcmp(__filename, FUZZ_TARGET)) {
        // Update our global stat struct
        st.st_size = *(size_t *)INPUT_SZ_ADDR;
 
        // Send it back to the caller, skip syscall
        memcpy(__stat_buf, &st, sizeof(struct stat));
        ret = 0;
    }
 
    // Just a normal stat, send to real xstat
    else {
        ret = real_xstat(__ver, __filename, __stat_buf);
    }
 
    return ret;
}
 
// Exploratory hooks to see if we're using fopen() related functions to open
// our input file
FILE* fopen(const char* pathname, const char* mode) {
    printf("** fopen() called for '%s'\n", pathname);
    exit(0);
}
 
// Our fopen hook, return a FILE* to the caller, also, if we are opening our
// target make sure we're not able to write to the file
FILE* fopen64(const char* pathname, const char* mode) {
    // Resolve symbol on demand and only once
    if (NULL == real_fopen64) {
        real_fopen64 = _resolve_symbol("fopen64");
    }
 
    // Check to see what file we're opening
    FILE* ret = NULL;
    if (!strcmp(FUZZ_TARGET, pathname)) {
        // We're trying to open our file, make sure it's a read-only mode
        if (strcmp(mode, "r")) {
            printf("** Attempt to open fuzz-target in illegal mode: '%s'\n", mode);
            exit(-1);
        }
 
        // Open shared memory FILE* and return to caller
        ret = fmemopen((void*)INPUT_ADDR, *(size_t*)INPUT_SZ_ADDR, mode);
         
        // Make sure we've never fopen()'d our fuzzing target before
        if (faked_fp) {
            printf("** Attempting to fopen64() fuzzing target more than once\n");
            exit(-1);
        }
 
        // Update faked_fp
        faked_fp = ret;
 
        // Change the filedes to something we know
        ret->_fileno = 1337;
    }
 
    // We're not opening our file, send to regular fopen
    else {
        ret = real_fopen64(pathname, mode);
    }
 
    // Return FILE stream ptr to caller
    return ret;
}
 
// Hook for __fxstat
int __fxstat (int __ver, int __filedesc, struct stat *__stat_buf) {
    // Resolve the real fxstat
    if (NULL == real_fxstat) {
        real_fxstat = _resolve_symbol("__fxstat");
    }
 
    int ret = -1;
 
    // Check to see if we're stat'ing our fuzz target
    if (1337 == __filedesc) {
        // Patch the global struct with current input size
        st.st_size = *(size_t*)INPUT_SZ_ADDR;
 
        // Copy global stat struct back to caller
        memcpy(__stat_buf, &st, sizeof(struct stat));
        ret = 0;
    }
 
    // Normal stat, send to real fxstat
    else {
        ret = real_fxstat(__ver, __filedesc, __stat_buf);
    }
 
    return ret;
}
 
// Hook for fcntl
int fcntl(int fildes, int cmd, ...) {
    // Resolve fcntl symbol if needed
    if (NULL == real_fcntl) {
        real_fcntl = _resolve_symbol("fcntl");
    }
 
    if (fildes == 1337) {
        return O_RDONLY;
    }
 
    else {
        printf("** fcntl() called for real file descriptor\n");
        exit(0);
    }
}
 
// Map memory to hold our inputs in memory and information about their size
static void _create_mem_mappings(void) {
    void *result = NULL;
 
    // Map the page to hold the input size
    result = mmap(
        (void *)(INPUT_SZ_ADDR),
        sizeof(size_t),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_SZ_ADDR)) {
        printf("** Err mapping INPUT_SZ_ADDR, mapped @ %p\n", result);
        exit(-1);
    }
 
    // Let's actually initialize the value at the input size location as well
    *(size_t *)INPUT_SZ_ADDR = 0;
 
    // Map the pages to hold the input contents
    result = mmap(
        (void *)(INPUT_ADDR),
        (size_t)(MAX_INPUT_SZ),
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
        0,
        0
    );
    if ((MAP_FAILED == result) || (result != (void *)INPUT_ADDR)) {
        printf("** Err mapping INPUT_ADDR, mapped @ %p\n", result);
        exit(-1);
    }
 
    // Init the value
    memset((void *)INPUT_ADDR, 0, (size_t)MAX_INPUT_SZ);
}
 
// Create a "legit" stat struct globally to pass to callers
static void _setup_stat_struct(void) {
    int result = __xstat(0x1337, FUZZ_TARGET, &st);
    if (-1 == result) {
        printf("** Err creating stat struct for '%s' during load\n", FUZZ_TARGET);
    }
}
 
// Used for testing, load /bin/ed into the input buffer and update its size info
#ifdef TEST
static void _test_func(void) {   
    // Open TEST_FILE for reading
    int fd = open(TEST_FILE, O_RDONLY);
    if (-1 == fd) {
        printf("** Failed to open '%s' during test\n", TEST_FILE);
        exit(-1);
    }
 
    // Attempt to read max input buf size
    ssize_t bytes = read(fd, (void*)INPUT_ADDR, (size_t)MAX_INPUT_SZ);
    close(fd);
 
    // Update the input size
    *(size_t *)INPUT_SZ_ADDR = (size_t)bytes;
}
#endif
 
// Routine to be called when our shared object is loaded
__attribute__((constructor)) static void _hook_load(void) {
    // Create memory mappings to hold our input and information about its size
    _create_mem_mappings();
 
    // Setup global "legit" stat struct
    _setup_stat_struct();
 
    // If we're testing, load /bin/ed up into our input buffer and update size
#ifdef TEST
    _test_func();
#endif
}

译者言

本文使用chatGPT-4o翻译而成,如有错误之处,请斧正
原文链接:https://h0mbre.github.io/Fuzzing-Like-A-Caveman-6/
本篇预计为本系列最后一篇


[课程]Android-CTF解题方法汇总!

最后于 2024-9-3 09:47 被pureGavin编辑 ,原因: 修改内容
收藏
免费 0
支持
分享
最新回复 (1)
雪    币: 1267
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
谢谢分享
2024-9-3 14:26
0
游客
登录 | 注册 方可回帖
返回
//