本文旨在探讨一种针对 IoT 设备的 AFL++ Fuzz 新方案。
Harness 编写 目前大部分 Fuzz 工具仅支持标准输入或命令行参数作为输入,而 IoT 设备 Fuzz 的主要对象为网络程序,需通过 Socket 进行输入输出,这构成了技术难点。
在掌握 Harness 编写技术后,可利用该方案对 IoT 设备的 Socket 通信程序进行 Fuzz 测试。
本文以 ASUS RT-N56U 设备为例进行阐述。目标 Fuzz 程序为 httpd,通过逆向分析可知,处理 HTTP 流程的代码位于 handle_request 函数中,该函数的第一个参数是 Socket 文件描述符,第二个参数是一个包含连接信息的结构体。
在 handle_request 函数中,通过 fgets 函数获取 HTTP 请求,如下所示:
1 2 3 4 5 6 7 8 9 10 if ( !fgets(v95, 4096 , a1) ) { v4 = "Bad Request" ; v5 = "No request found." ; LABEL_14: v6 = 400 ; LABEL_15: v7 = 0 ; return send_error(v6, v4, v7, v5, a1); }
基于此,可以构建如下思路:
Hook httpd 的 main 函数。
将 HTTP 请求置于文件中,文件名作为 httpd 参数输入。
伪造一个可读写的文件描述符,将输入的 HTTP 请求写入该描述符,供 httpd 读取。
伪造 handle_request 所需的参数。
基于上述逻辑,编写 Fuzz 函数如下:
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 void fuzz (const char *filename) { int memfd; printf ("do fuzz(%s)\n" , filename); chdir(currentPWD); memfd = create_memfd_from_file(filename); if (memfd == -1 ) { perror("create_memfd_from_file error." ); return ; } FILE* single_handle = fdopen(memfd, "r+" ); if (!single_handle) { perror("fdopen error." ); close(memfd); return ; } chdir("/www" ); conn_item_t fake_item; fake_item.fd = memfd; fake_item.usa.sa_in.sin_family = AF_INET; fake_item.usa.sa_in.sin_port = htons(12345 ); inet_pton(AF_INET, "192.168.1.10" , &fake_item.usa.sa_in.sin_addr); handle_request(single_handle, &fake_item); if (getenv("DEBUG" )) { if (fseek(single_handle, 0 , SEEK_SET) != 0 ) { perror("fseek single_handle before dump" ); } else { char buf[4096 ]; int write_failed = 0 ; for (;;) { size_t r = fread(buf, 1 , sizeof (buf), single_handle); if (r == 0 ) { if (ferror(single_handle)) { perror("fread single_handle dump" ); clearerr(single_handle); } break ; } size_t off = 0 ; while (off < r) { ssize_t w = write(STDOUT_FILENO, buf + off, r - off); if (w < 0 ) { perror("write stdout dump" ); write_failed = 1 ; break ; } off += (size_t )w; } if (write_failed) { break ; } } if (write_failed) { clearerr(single_handle); } } } }
首先,Fuzz 函数的参数源于命令行参数。接着,编写 create_memfd_from_file 函数,将文件内容转换为可读写的文件描述符,实现逻辑如下:
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 static int memfd_create_compat (const char *name, unsigned int flags) { #ifdef SYS_memfd_create return (int )syscall(SYS_memfd_create, name, flags); #elif defined(__NR_memfd_create) return (int )syscall(__NR_memfd_create, name, flags); #else FILE *tf = tmpfile(); if (!tf) return -1 ; int fd = dup(fileno(tf)); fclose(tf); return fd; #endif } static int create_memfd_from_file (const char *filename) { int fd = memfd_create_compat("harness_mem" , MFD_CLOEXEC); if (fd < 0 ) { perror("memfd_create" ); return -1 ; } int file_fd = open(filename, O_RDONLY); if (file_fd < 0 ) { perror("open file" ); close(fd); return -1 ; } char buf[8192 ]; for (;;) { ssize_t r = read(file_fd, buf, sizeof (buf)); if (r == 0 ) break ; if (r < 0 ) { if (errno == EINTR) continue ; perror("read file" ); close(file_fd); close(fd); return -1 ; } ssize_t off = 0 ; while (off < r) { ssize_t w = write(fd, buf + off, r - off); if (w < 0 ) { if (errno == EINTR) continue ; perror("write memfd" ); close(file_fd); close(fd); return -1 ; } off += w; } } close(file_fd); if (lseek(fd, 0 , SEEK_SET) < 0 ) { perror("lseek memfd" ); close(fd); return -1 ; } return fd; }
随后,使用 fdopen 函数将 int 类型的文件描述符转换为 FILE* 类型。最后构造 fake_item 结构体,作为 handle_request 的第二个参数,fake_item 结构体定义如下:
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 struct qm_trace { char * lastfile; int lastline; char * prevfile; int prevline; }; #define TRACEBUF struct qm_trace trace; #define TAILQ_ENTRY(type) \ struct { \ struct type *tqe_next; \ struct type **tqe_prev; \ TRACEBUF \ } typedef union { struct sockaddr sa ; struct sockaddr_in sa_in ; #if defined (USE_IPV6) struct sockaddr_in6 sa_in6 ; #endif } usockaddr; typedef struct conn_item { TAILQ_ENTRY(conn_item) entry; int fd; #if defined (SUPPORT_HTTPS) int ssl; #endif usockaddr usa; } conn_item_t ;
此外,在调试模式下,还需支持输出 HTTP 请求结果。
Hook main 函数的方法如下:
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 int __uClibc_main( int (*main)(int , char **, char **), int argc, char **argv, void (*app_init)(void ), void (*app_fini)(void ), void (*rtld_fini)(void ), void *stack_end) { printf ("do __uClibc_main(argc=%d, argv=%p)\n" , argc, argv); if (!uClibc_main_orig) { LOG("dlsym(RTLD_NEXT, __uClibc_main_orig) failed: %s\n" , dlerror()); _exit(1 ); } LOG("uClibc_main_orig = %p\n" , uClibc_main_orig); return uClibc_main_orig(main_hook, argc, argv, app_init, app_fini, rtld_fini, stack_end); } __attribute__((constructor)) static void harness_init (void ) { LOG("constructor executed: harness.so loaded\n" ); uClibc_main_orig = dlsym(RTLD_NEXT, "__uClibc_main" ); if (!uClibc_main_orig) { LOG("dlsym(RTLD_NEXT, __uClibc_main_orig) failed: %s\n" , dlerror()); } else { LOG("dlsym(RTLD_NEXT, __uClibc_main_orig) success: %p\n" , uClibc_main_orig); } }
由于大多数 IoT 设备使用 uClibc 库,因此需要 Hook __uClibc_main 函数。
针对不同设备,需根据具体情况和架构进行差异化处理。在本例中,目标 httpd 程序在监听 Web 端口之前,也会执行初始化操作,如设置管理员账号密码等,因此需执行同样的初始化动作。如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 typedef void * (*HANDLE_RESET_LOGIN_DATA) (void );HANDLE_RESET_LOGIN_DATA handle_reset_login_data = (HANDLE_RESET_LOGIN_DATA) 0x402ED8 ; typedef void * (*HANDLE_LOAD_NVRAM_AUTH) (void );HANDLE_LOAD_NVRAM_AUTH handle_load_nvram_auth = (HANDLE_LOAD_NVRAM_AUTH) 0x402DE0 ; void init () { handle_reset_login_data(); handle_load_nvram_auth(); currentPWD = malloc (MAX_PATH); if (getcwd(currentPWD, MAX_PATH) != NULL ) { printf ("Current working directory: %s\n" , currentPWD); } else { perror("Error getting current working directory" ); } }
Harness 编写思路至此结束,后续需针对具体 IoT 程序进行差异化操作,以确保程序正常运行。
处理 Fuzz 程序依赖问题 本例中,httpd 仅需解决 NVRAM 依赖问题。IoT 设备通常使用 NVRAM 存储配置信息,但运行 Fuzz 的主机通常不包含 NVRAM 驱动,不过部分操作系统可能存在 NVRAM 驱动包,可自行安装。为使 httpd 正常使用 NVRAM,存在以下三种方案:
若操作系统存在 NVRAM 包,可直接安装并尝试适配使用,该方案最为简便。
当默认 NVRAM 驱动与 IoT 设备不匹配时,可通过逆向 IoT 固件中的 NVRAM 驱动,参考其代码,借助 AI 编写适配当前机器的 NVRAM 驱动。
可通过逆向 IoT 固件中的 libnvram 共享库,参考其代码,Hook 核心函数,如:nvram_get, nvram_set 等。
接下来,需获取设备的配置文件,或导出设备上的 NVRAM 数据并导入当前机器,以实现更真实的仿真。
鉴于代码篇幅较长,此处仅展示用法,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 $ hexdump -C /var/lib/soft_nvram.bin |tail 00001fd0 74 73 70 3d 30 00 6e 66 5f 61 6c 67 5f 73 69 70 |tsp=0.nf_alg_sip| 00001fe0 3d 30 00 70 72 65 66 65 72 72 65 64 5f 6c 61 6e |=0.preferred_lan| 00001ff0 67 3d 45 4e 00 6c 6f 67 69 6e 5f 74 69 6d 65 73 |g=EN.login_times| 00002000 74 61 6d 70 3d 33 33 30 38 38 35 39 00 00 00 00 |tamp=3308859....| 00002010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00010000 $ make make -C /lib/modules/6.1.0-37-amd64/build M=/home/debian/nvram_driver modules make[1]: Entering directory '/usr/src/linux-headers-6.1.0-37-amd64' CC [M] /home/debian/nvram_driver/soft_nvram.o MODPOST /home/debian/nvram_driver/Module.symvers CC [M] /home/debian/nvram_driver/soft_nvram.mod.o LD [M] /home/debian/nvram_driver/soft_nvram.ko BTF [M] /home/debian/nvram_driver/soft_nvram.ko Skipping BTF generation for /home/debian/nvram_driver/soft_nvram.ko due to unavailability of vmlinux make[1]: Leaving directory '/usr/src/linux-headers-6.1.0-37-amd64' $ sudo insmod soft_nvram.ko backing_path=/var/lib/soft_nvram.bin$ cd romfs$ export QEMU_LD_PREFIX="." $ afl-qemu-trace ./usr/sbin/nvram get http_username ./usr/sbin/nvram: cache '/etc/ld.so.cache' is corrupt admin
Patch QEMU 通常情况下,上述 NVRAM 程序尚无法正常运行,因仍缺关键一步。该架构下的 NVRAM 驱动调用需使用 ioctl,而 QEMU 对 ioctl 调用有独立处理逻辑,并非默认使用系统调用。QEMU 默认无法识别 NVRAM 的 ioctl 调用方法。因此,需对 QEMU 进行相应修改,经研究,较为简便的修改方案如下:
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 @@ -758,3 +758,17 @@ #ifdef TUNGETDEVNETNS IOCTL(TUNGETDEVNETNS, IOC_R, TYPE_NULL) #endif + +// nvram +#define NVRAM_IOCTL_CLEAR 0x14 +#define TARGET_NVRAM_IOCTL_CLEAR NVRAM_IOCTL_CLEAR +#define NVRAM_IOCTL_COMMIT 0xA +#define TARGET_NVRAM_IOCTL_COMMIT NVRAM_IOCTL_COMMIT +#define NVRAM_IOCTL_GET 0x28 +#define TARGET_NVRAM_IOCTL_GET NVRAM_IOCTL_GET +#define NVRAM_IOCTL_SET 0x1e +#define TARGET_NVRAM_IOCTL_SET NVRAM_IOCTL_SET +IOCTL(NVRAM_IOCTL_COMMIT, 0, TYPE_NULL) +IOCTL(NVRAM_IOCTL_CLEAR, 0, TYPE_NULL) +IOCTL(NVRAM_IOCTL_SET, IOC_W, MK_PTR(MK_STRUCT(STRUCT_anvram_ioctl_t))) +IOCTL(NVRAM_IOCTL_GET, IOC_RW, MK_PTR(MK_STRUCT(STRUCT_anvram_ioctl_t))) @@ -642,3 +642,11 @@ STRUCT(usbdevfs_disconnect_claim, TYPE_INT, /* flags */ MK_ARRAY(TYPE_CHAR, USBDEVFS_MAXDRIVERNAME + 1)) /* driver */ #endif /* CONFIG_USBFS */ + +STRUCT(anvram_ioctl_t, + TYPE_INT, // size + TYPE_INT, // is_temp + TYPE_INT, // len_param + TYPE_INT, // len_value + TYPE_PTRVOID, // param + TYPE_PTRVOID) // value
然而,测试发现 QEMU 5.X 版本中 NVRAM 读写操作会因未知原因失败,而 QEMU 10.X 版本则能成功运行。鉴于 QEMU 代码库庞大,排查难度较高,因此考虑采用 QEMU 10.X 进行 AFL Fuzz。
目前公开的 QEMUAFL 支持的最高版本为 QEMU 5.X。若要使用 QEMU 10.X,需自行进行 Patch 适配。相关的 Patch 方案及过程将在后续文章中进行分享。