学习一下如何使用 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 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 std::env::var ("LIBAFL_FUZZBENCH_DEBUG" ).is_ok () { 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 #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> 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 ]; 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
参考链接