学习一下如何使用 LibAFL 来实现对 binary-only 的 socket 进行 fuzz。
LibAFL 本身提供了 binary-only 模式,即 LibAFL-Qemu。然后针对 binary-only 的 socket 目标 AFL++ 也提供了常见的方法。这里我们采用 LibAFL-Qemu + socketfuzz hook library 的方法。

baby_fuzzer of LibAFL-QEMU

首先阅读 LibAFL 提供的 LibAFL-QEMU 的几个实例,由于 frida_mode 目前相较于 qemu_mode 功能仍有所欠缺,我们仅考虑 qemu 模式。官方给的几个例子中 qemu_launcher 是比较完整可以直接拿来用的。
要跑起来直接 just run 即可,其脚本会自动下载 libpng 的库并编译 harness 来进行 fuzz。
这里我是自己先写了一个很简单的 harness.cc 来做测试,基本逻辑和 LibAFL 的 babyfuzzer 差不多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

extern "C" int LLVMFuzzOneTestInput(const char *buf, size_t len) {
char bof_buf[0x4];
if (len > 0x100) { return 1; }
if (buf[0] == 'a') {
if (buf[1] == 'b') {
if (buf[2] == 'c') { strcpy(bof_buf, buf); }
}
}
return 0;
}

int main() {
char buf[0x100];
size_t len = 0;
LLVMFuzzOneTestInput(buf, len);
return 0;
}

这里对应要修改 harness.rs 文件中的 hook 符号以及写入的参数,似乎要 extern “C” 防止符号名被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    /// Helper function to find the function we want to fuzz.
fn start_pc(qemu: Qemu) -> Result<GuestAddr, Error> {
let mut elf_buffer = Vec::new();
let elf = EasyElf::from_file(qemu.binary_path(), &mut elf_buffer)?;

let start_pc = elf
.resolve_symbol("LLVMFuzzTestOneInput", qemu.load_addr())
.ok_or_else(|| Error::empty_optional("Symbol LLVMFuzzTestOneInput not found"))?;
Ok(start_pc)
}
...
self.qemu
.write_function_argument(0, self.input_addr)
.map_err(|e| Error::unknown(format!("Failed to write argument 0: {e:?}")))?;

self.qemu
.write_function_argument(1, len)
.map_err(|e| Error::unknown(format!("Failed to write argument 1: {e:?}")))?;

编译命令如下:

1
2
3
x86_64-linux-gnu-g++ \
./harness_test.cc \
-o test-harness-release

最终 fuzz 运行的命令,把 cores 缩减为了两个(这里笔者发现如果只用一个核 fuzz 会卡死不动,可能是进程调度的问题,有师傅懂的话可以回答解惑一下 orz)。

1
2
3
4
5
6
7
8
LibAFL/fuzzers/binary_only/qemu_launcher/target/release/qemu_launcher \
--input ./corpus_test \
--output LibAFL/fuzzers/binary_only/qemu_launcher/target/output/ \
--log LibAFL/fuzzers/binary_only/qemu_launcher/target/output/log.txt \
--cores 0-1 --asan-host-cores 0 --cmplog-cores 1 \
--iterations 1000000 \
-- \
LibAFL/fuzzers/binary_only/qemu_launcher/test-harness-release

然后应该很快就会出现 crash。

调试

github repo 里有个 LibAFL_QEMU 调试的 issue,刚好借此学习下如何对其做调试。
qemu_launcher 本身支持 qemu 自带的调试命令 -g,设置 qemu fuzzer 为 simplemgr,并设置为单核,然后给 qemu_launcher 固定输入进行调试即可。

See this example:
if let Some(rerun_input) = &self.options.rerun_input {

simplemgr 在 fuzzer.rs 中已经提供了相应的实现逻辑,只需要添加 simplemgr 的 feature 即可,同时其会自动设置单核。

1
2
3
4
5
6
#[cfg(feature = "simplemgr")]
return client.run(
None,
SimpleEventManager::new(monitor.clone()),
ClientDescription::new(0, 0, CoreId(0)),
);

看 option.rs 是需要提供 -r <path> 参数
cargo 重新编译 qemu_launcher:

1
cargo build --release --features simplemgr

调试时还可以设置环境变量 LIBAFL_FUZZBENCH_DEBUG 来重新启用 stderror。

1
2
3
4
5
6
7
8
9
// If we are debugging, re-enable target stderror.
if std::env::var("LIBAFL_FUZZBENCH_DEBUG").is_ok() {
// # Safety
// Nobody else uses the new stderror here.
unsafe {
dup2(new_stderr, io::stderr().as_raw_fd())?;
}
}

添加 socketfuzz hook

对于建立 socket 连接的 binary 的输入,如果不作修改直接去建立 socket 连接进行 fuzz 会由于 socket 本身导致 fuzz 速率下降,并且需要额外修改 harness。这里我们采用添加 自定义库 来 hook socket 相关函数的方法。
基本原理就是重定义 socket 中的 bind,accept,listen 等函数,让其到最后 recv 或者 read 时从标准输入中读取输入。
AFL++ 中已提供了重定义的 socketfuzz.c 逻辑,复制下来。
这里我们添加 read 函数 hook 到 LLVMFuzzTestOneInput 函数,方便 harness 介入。

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
/*
* This is desock_dup.c from the amazing preeny project
* https://github.com/zardus/preeny
*
* It is packaged in afl++ to have it at hand if needed
*
*/

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h> //
#include <sys/socket.h> //
#include <sys/stat.h> //
#include <fcntl.h> //
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <dlfcn.h>
#include <errno.h>
#include <stdio.h>
#include <poll.h>
// #include "logging.h" // switched from preeny_info() to fprintf(stderr, "Info:
// "

//
// originals
//
int (*original_close)(int);
int (*original_dup2)(int, int);
int (*original_read)(int, void *, size_t);
__attribute__((constructor)) void preeny_desock_dup_orig() {
original_close = dlsym(RTLD_NEXT, "close");
original_dup2 = dlsym(RTLD_NEXT, "dup2");
original_read = dlsym(RTLD_NEXT, "read");
}

int close(int sockfd) {
if (sockfd <= 2) {
fprintf(stderr, "Info: Disabling close on %d\n", sockfd);
return 0;

} else {
return original_close(sockfd);
}
}

int dup2(int old, int new) {
if (new <= 2) {
fprintf(stderr, "Info: Disabling dup from %d to %d\n", old, new);
return 0;

} else {
return original_dup2(old, new);
}
}

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {
(void)sockfd;
(void)addr;
(void)addrlen;
fprintf(stderr, "Info: Emulating accept on %d\n", sockfd);
return 0;
}

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
(void)sockfd;
(void)addr;
(void)addrlen;
fprintf(stderr, "Info: Emulating bind on port %d\n",
ntohs(((struct sockaddr_in *)addr)->sin_port));
return 0;
}

int listen(int sockfd, int backlog) {
(void)sockfd;
(void)backlog;
return 0;
}

int setsockopt(int sockfd, int level, int optid, const void *optdata,
socklen_t optdatalen) {
(void)sockfd;
(void)level;
(void)optid;
(void)optdata;
(void)optdatalen;
return 0;
}

ssize_t LLVMFuzzTestOneInput(char *buf, size_t data) {
return data;
}

ssize_t read(int fd, void *buf, size_t count) {
if (fd != 0) { return original_read(fd, buf, count); }
return LLVMFuzzTestOneInput(buf, count);
}

然后直接 make 即可获取到32位以及64位的 so 库。
然后我们修改前面的 babyfuzzer 逻辑,先添加 read 的用户输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int vuln(char *buf, size_t len) {
char bof_buf[0x4];
// we will hook read function with socket_fuzzing/socketfuzz64.so
read(0, buf, len);
if (len > 0x100) { return 1; }
if (buf[0] == 'a') {
if (buf[1] == 'b') {
if (buf[2] == 'c') { strcpy(bof_buf, buf); }
}
}
return 0;
}

int main() {
char buf[0x100];
size_t len = 0;
vuln(buf, len);
return 0;
}

然后由于 LLVMFuzzTestOneInput 函数被我们搞到了 so 库里,这里我们修改 qemu_launcher 的 harness.rs 逻辑,令其先运行到 main 函数加载好所有共享库后,找到 socketfuzz64.so 获取其地址然后解析 LLVMFuzzTestOneInput 符号即可(这样可以方便后续的功能)。
harness.rs 逻辑如下:

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
base ❯ diff ./harness_ori.rs ./src/harness.rs
29a30
> // we run target binary to main first
31a33,72
> let main_pc = elf
> .resolve_symbol("main", qemu.load_addr())
> .ok_or_else(|| Error::empty_optional("Symbol Main not found"))?;
>
> qemu.entry_break(main_pc);
>
> // enumrate all shared library
> let maps = qemu.mappings();
>
> let mut so_base: Option<GuestAddr> = None;
> let mut so_path: Option<String> = None;
>
> for m in maps {
> if let Some(path) = &m.path() {
> if path.contains("socketfuzz64.so") {
> log::info!(
> "found socketfuzz64.so mapping: {:#x}-{:#x} {}",
> m.start(),
> m.end(),
> path
> );
> so_base = Some(m.start());
> so_path = Some(path.to_string());
> break;
> }
> }
> }
>
> let so_base = so_base
> .ok_or_else(|| Error::empty_optional("socketfuzz64.so not found in memory mappings"))?;
>
> let so_path = so_path.unwrap();
>
> let mut so_buf = Vec::new();
> let so_elf = EasyElf::from_file(&so_path, &mut so_buf)?;
> let fuzz_pc = so_elf
> .resolve_symbol("LLVMFuzzTestOneInput", so_base)
> .ok_or_else(|| {
> Error::empty_optional("Symbol LLVMFuzzTestOneInput not found in socketfuzz64.so")
> })?;
33,36c74
< let start_pc = elf
< .resolve_symbol("LLVMFuzzerTestOneInput", qemu.load_addr())
< .ok_or_else(|| Error::empty_optional("Symbol LLVMFuzzerTestOneInput not found"))?;
< Ok(start_pc)
---
> Ok(fuzz_pc)
46,50c84,88
< let ret_addr: GuestAddr = qemu
< .read_return_address()
< .map_err(|e| Error::unknown(format!("Failed to read return address: {e:?}")))?;
< log::info!("ret_addr = {ret_addr:#x}");
< qemu.set_breakpoint(ret_addr);
---
> let end_pc: GuestAddr = qemu.load_addr() + 0x00000000000011F3;
> log::info!("end_pc = {end_pc:#x}");
> // we not consider for now, when qemu execute here it will panic, we need to add check
> // logic. This config will exit early to narrow down fuzz scope.
> // qemu.set_breakpoint(end_pc);
115c153
<
---
> /*
118a157,169
> */
> // we need to copy content to &buf instead overwrite &buf to &input_addr
> let target_addr: GuestAddr = self
> .qemu
> .read_function_argument(0)
> .map_err(|e| Error::unknown(format!("Failed to read argument 0: {e:?}")))?;
>
> self.qemu.write_mem(target_addr, buf).map_err(|e| {
> Error::unknown(format!(
> "Failed to write to memory@{:#x}: {e:?}",
> target_addr
> ))
> })?;

由于我们已知 qemu_launcher 适配 qemu 的原始命令,这里我们可以尝试传入 LD_PRELOAD 环境变量。
然后就可以成功 hook read 函数了,socket 同理,运行命令:

1
2
3
4
5
6
7
8
9
LibAFL/fuzzers/binary_only/qemu_launcher/target/release/qemu_launcher \
--input ./corpus_test \
--output LibAFL/fuzzers/binary_only/qemu_launcher/target/output/ \
--log LibAFL/fuzzers/binary_only/qemu_launcher/target/output/log.txt \
--timeout 1000000 \
-- \
-E LD_PRELOAD=./socket_fuzzing/socketfuzz64.so \
-g 1234 \
LibAFL/fuzzers/binary_only/qemu_launcher/test-harness-release

参考链接