原创Paper|IoT固件Fuzz:从Harness编写到QEMU适配

admin 2025-12-22 04:06:02 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍了一种针对IoT设备的AFL++Fuzz新方案,重点解决了网络程序通过Socket输入输出的技术难点。以ASUSRT-N56U设备的httpd程序为例,详细阐述了如何编写Harness,包括Hookmain函数、创建内存文件描述符、伪造参数等步骤。同时,文章讨论了处理IoT设备依赖问题的方法,特别是NVRAM依赖的三种解决方案:安装NVRAM包、编写适配驱动或Hook核心函数。最后,文章指出需要对QEMU进行修改以支持NVRAM的ioctl调用,并建议使用QEMU10.X版本进行AFLFuzz,因为QEMU5.X版本存在NVRAM读写操作失败的问题。 综合评分: 85 文章分类: IoT安全,漏洞分析,Fuzzing,逆向分析,代码审计


cover_image

原创 Paper | IoT 固件 Fuzz:从 Harness 编写到 QEMU 适配

原创

404实验室

知道创宇404实验室

2025年12月17日 11:29 湖北

者:知道创宇404实验室

本文旨在探讨一种针对 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 请求,如下所示:

if(!fgets(v95,4096, a1))
{
    v4 ="Bad Request";
    v5 ="No request found.";
LABEL_14:
    v6 =400;
LABEL_15:
    v7 =0;
returnsend_error(v6, v4, v7, v5, a1);
}

基于此,可以构建如下思路:

  1. Hook httpd 的 main 函数。
  2. 将 HTTP 请求置于文件中,文件名作为 httpd 参数输入。
  3. 伪造一个可读写的文件描述符,将输入的 HTTP 请求写入该描述符,供 httpd 读取。
  4. 伪造 handle_request 所需的参数。

基于上述逻辑,编写 Fuzz 函数如下:

voidfuzz(constchar*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&nbsp;<&nbsp;r){
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ssize_t w&nbsp;=write(STDOUT_FILENO,&nbsp;buf&nbsp;+&nbsp;off,&nbsp;r&nbsp;-&nbsp;off);
if(w&nbsp;<0){
perror("write stdout dump");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; write_failed&nbsp;=1;
break;
}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; off&nbsp;+=(size_t)w;
}
if(write_failed){
break;
}
}
if(write_failed){
&nbsp;// 如果写失败,确保后续读取状态被清理
clearerr(single_handle);
}
}
}
}

首先,Fuzz 函数的参数源于命令行参数。接着,编写 create_memfd_from_file 函数,将文件内容转换为可读写的文件描述符,实现逻辑如下:

// 兼容封装:优先使用 memfd_create 系统调用;不可用时回退到匿名临时文件
staticintmemfd_create_compat(constchar*name,unsignedint&nbsp;flags)
{
#ifdef&nbsp;SYS_memfd_create
return(int)syscall(SYS_memfd_create,&nbsp;name,&nbsp;flags);
#elif&nbsp;defined(__NR_memfd_create)
return(int)syscall(__NR_memfd_create,&nbsp;name,&nbsp;flags);
#else
&nbsp;// Fallback: 使用匿名临时文件模拟(并非真正的 memfd,但可读写且不落磁盘路径)
&nbsp; &nbsp; FILE&nbsp;*tf&nbsp;=tmpfile();
if(!tf)return-1;
int&nbsp;fd&nbsp;=dup(fileno(tf));
fclose(tf);
return&nbsp;fd;
#endif
}

// 从文件读入全部内容到匿名内存文件(memfd),并回到偏移 0,返回该 fd
staticintcreate_memfd_from_file(constchar*filename)
{
int&nbsp;fd&nbsp;=memfd_create_compat("harness_mem",&nbsp;MFD_CLOEXEC);
if(fd&nbsp;<0){
perror("memfd_create");
return-1;
}

int&nbsp;file_fd&nbsp;=open(filename,&nbsp;O_RDONLY);
if(file_fd&nbsp;<0){
perror("open file");
close(fd);
return-1;
}

char&nbsp;buf[8192];
for(;;){
&nbsp; &nbsp; &nbsp; &nbsp; ssize_t r&nbsp;=read(file_fd,&nbsp;buf,sizeof(buf));
if(r&nbsp;==0)break;&nbsp;// EOF
if(r&nbsp;<0){
if(errno&nbsp;==&nbsp;EINTR)continue;
perror("read file");
close(file_fd);
close(fd);
return-1;
}
&nbsp; &nbsp; &nbsp; &nbsp; ssize_t off&nbsp;=0;
while(off&nbsp;<&nbsp;r){
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ssize_t w&nbsp;=write(fd,&nbsp;buf&nbsp;+&nbsp;off,&nbsp;r&nbsp;-&nbsp;off);
if(w&nbsp;<0){
if(errno&nbsp;==&nbsp;EINTR)continue;
perror("write memfd");
close(file_fd);
close(fd);
return-1;
}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; off&nbsp;+=&nbsp;w;
}
}
close(file_fd);

if(lseek(fd,0,&nbsp;SEEK_SET)<0){
perror("lseek memfd");
close(fd);
return-1;
}
return&nbsp;fd;
}

随后,使用 fdopen 函数将 int 类型的文件描述符转换为 FILE* 类型。最后构造 fake_item 结构体,作为 handle_request 的第二个参数,fake_item 结构体定义如下:

struct&nbsp;qm_trace&nbsp;{
char*&nbsp;lastfile;
int&nbsp;lastline;
char*&nbsp;prevfile;
int&nbsp;prevline;
};

#define&nbsp;TRACEBUF &nbsp; &nbsp;struct qm_trace trace;
#define&nbsp;TAILQ_ENTRY(type) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \
struct{&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \
struct&nbsp;type&nbsp;*tqe_next;/* next element */&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \
struct&nbsp;type&nbsp;**tqe_prev;/* address of previous next element */&nbsp; \
&nbsp; &nbsp; TRACEBUF &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;\
}

typedefunion{
struct&nbsp;sockaddr sa;
struct&nbsp;sockaddr_in sa_in;
#if&nbsp;defined (USE_IPV6)
struct&nbsp;sockaddr_in6 sa_in6;
#endif
}&nbsp;usockaddr;

typedefstruct&nbsp;conn_item&nbsp;{
TAILQ_ENTRY(conn_item)&nbsp;entry;
int&nbsp;fd;
#if&nbsp;defined (SUPPORT_HTTPS)
int&nbsp;ssl;
#endif
&nbsp; &nbsp; usockaddr usa;
}&nbsp;conn_item_t;

此外,在调试模式下,还需支持输出 HTTP 请求结果。

Hook main 函数的方法如下:

int__uClibc_main(
int(*main)(int,char**,char**),
int&nbsp;argc,
char**argv,
void(*app_init)(void),
void(*app_fini)(void),
void(*rtld_fini)(void),
void*stack_end){

&nbsp;// debug
printf("do __uClibc_main(argc=%d, argv=%p)\n",&nbsp;argc,&nbsp;argv);

if(!uClibc_main_orig){
LOG("dlsym(RTLD_NEXT, __uClibc_main_orig) failed: %s\n",dlerror());
_exit(1);
}
LOG("uClibc_main_orig = %p\n",&nbsp;uClibc_main_orig);
&nbsp;// ... and call it with our custom main function
returnuClibc_main_orig(main_hook,&nbsp;argc,&nbsp;argv,&nbsp;app_init,&nbsp;app_fini,&nbsp;rtld_fini,&nbsp;stack_end);
}

// 在 .init 段执行的 constructor
__attribute__((constructor))
staticvoidharness_init(void)
{
LOG("constructor executed: harness.so loaded\n");
&nbsp; &nbsp; uClibc_main_orig&nbsp;=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",&nbsp;uClibc_main_orig);
}

}

由于大多数 IoT 设备使用 uClibc 库,因此需要 Hook __uClibc_main 函数。

针对不同设备,需根据具体情况和架构进行差异化处理。在本例中,目标 httpd 程序在监听 Web 端口之前,也会执行初始化操作,如设置管理员账号密码等,因此需执行同样的初始化动作。如下所示:

typedefvoid*(*HANDLE_RESET_LOGIN_DATA)(void);
HANDLE_RESET_LOGIN_DATA handle_reset_login_data&nbsp;=(HANDLE_RESET_LOGIN_DATA)0x402ED8;
typedefvoid*(*HANDLE_LOAD_NVRAM_AUTH)(void);
HANDLE_LOAD_NVRAM_AUTH handle_load_nvram_auth&nbsp;=(HANDLE_LOAD_NVRAM_AUTH)0x402DE0;

voidinit(){
&nbsp;// httpd处理请求前的初始化
handle_reset_login_data();
handle_load_nvram_auth();
&nbsp; &nbsp; currentPWD&nbsp;=malloc(MAX_PATH);
if(getcwd(currentPWD,&nbsp;MAX_PATH)!=&nbsp;NULL){
printf("Current working directory: %s\n",&nbsp;currentPWD);
}else{
perror("Error getting current working directory");
}

}

Harness 编写思路至此结束,后续需针对具体 IoT 程序进行差异化操作,以确保程序正常运行。

处理 Fuzz 程序依赖问题

参考资料

本例中,httpd 仅需解决 NVRAM 依赖问题。IoT 设备通常使用 NVRAM 存储配置信息,但运行 Fuzz 的主机通常不包含 NVRAM 驱动,不过部分操作系统可能存在 NVRAM 驱动包,可自行安装。为使 httpd 正常使用 NVRAM,存在以下三种方案:

  1. 若操作系统存在 NVRAM 包,可直接安装并尝试适配使用,该方案最为简便。
  2. 当默认 NVRAM 驱动与 IoT 设备不匹配时,可通过逆向 IoT 固件中的 NVRAM 驱动,参考其代码,借助 AI 编写适配当前机器的 NVRAM 驱动。
  3. 可通过逆向 IoT 固件中的 libnvram 共享库,参考其代码,Hook 核心函数,如:nvram_getnvram_set 等。

接下来,需获取设备的配置文件,或导出设备上的 NVRAM 数据并导入当前机器,以实现更真实的仿真。

鉴于代码篇幅较长,此处仅展示用法,如下所示:

$ hexdump -C /var/lib/soft_nvram.bin |tail
00001fd0 &nbsp;74 73 70 3d 30 00 6e 66 &nbsp;5f 61 6c 67 5f 73 69 70 &nbsp;|tsp=0.nf_alg_sip|
00001fe0 &nbsp;3d 30 00 70 72 65 66 65 &nbsp;72 72 65 64 5f 6c 61 6e &nbsp;|=0.preferred_lan|
00001ff0 &nbsp;67 3d 45 4e 00 6c 6f 67 &nbsp;69 6e 5f 74 69 6d 65 73 &nbsp;|g=EN.login_times|
00002000 &nbsp;74 61 6d 70 3d 33 33 30 &nbsp;38 38 35 39 00 00 00 00 &nbsp;|tamp=3308859....|
00002010 &nbsp;00 00 00 00 00 00 00 00 &nbsp;00 00 00 00 00 00 00 00 &nbsp;|................|
*
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'
&nbsp; CC [M] &nbsp;/home/debian/nvram_driver/soft_nvram.o
&nbsp; MODPOST /home/debian/nvram_driver/Module.symvers
&nbsp; CC [M] &nbsp;/home/debian/nvram_driver/soft_nvram.mod.o
&nbsp; LD [M] &nbsp;/home/debian/nvram_driver/soft_nvram.ko
&nbsp; 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 进行相应修改,经研究,较为简便的修改方案如下:

diff --git a/linux-user/ioctls.h b/linux-user/ioctls.h
index 3b41128fd7..e8b636badc 100644
--- a/linux-user/ioctls.h
+++ b/linux-user/ioctls.h
@@ -758,3 +758,17 @@
&nbsp;#ifdef&nbsp;TUNGETDEVNETNS
&nbsp; &nbsp;IOCTL(TUNGETDEVNETNS, &nbsp;IOC_R, TYPE_NULL)
&nbsp;#endif
+
+// nvram
+#define&nbsp;NVRAM_IOCTL_CLEAR 0x14
+#define&nbsp;TARGET_NVRAM_IOCTL_CLEAR NVRAM_IOCTL_CLEAR
+#define&nbsp;NVRAM_IOCTL_COMMIT &nbsp;0xA
+#define&nbsp;TARGET_NVRAM_IOCTL_COMMIT NVRAM_IOCTL_COMMIT
+#define&nbsp;NVRAM_IOCTL_GET &nbsp; 0x28
+#define&nbsp;TARGET_NVRAM_IOCTL_GET NVRAM_IOCTL_GET
+#define&nbsp;NVRAM_IOCTL_SET &nbsp; 0x1e
+#define&nbsp;TARGET_NVRAM_IOCTL_SET NVRAM_IOCTL_SET
+IOCTL(NVRAM_IOCTL_COMMIT, 0, TYPE_NULL)
+IOCTL(NVRAM_IOCTL_CLEAR, &nbsp;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)))

diff --git a/linux-user/syscall_types.h b/linux-user/syscall_types.h
index 6dd7a80ce5..e82b654df3 100644
--- a/linux-user/syscall_types.h
+++ b/linux-user/syscall_types.h
@@ -642,3 +642,11 @@ STRUCT(usbdevfs_disconnect_claim,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;TYPE_INT, /* flags */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;MK_ARRAY(TYPE_CHAR, USBDEVFS_MAXDRIVERNAME + 1)) /* driver */
&nbsp;#endif&nbsp;/* CONFIG_USBFS */
+
+STRUCT(anvram_ioctl_t,
+ &nbsp; &nbsp; &nbsp; TYPE_INT, // size
+ &nbsp; &nbsp; &nbsp; TYPE_INT, // is_temp
+ &nbsp; &nbsp; &nbsp; TYPE_INT, // len_param
+ &nbsp; &nbsp; &nbsp; TYPE_INT, // len_value
+ &nbsp; &nbsp; &nbsp; TYPE_PTRVOID, // param
+ &nbsp; &nbsp; &nbsp; 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 方案及过程将在后续文章中进行分享。

往 期 热 门

(点击图片跳转)

戳“阅读原文”更多精彩内容!


查看原文:《原创 Paper | IoT 固件 Fuzz:从 Harness 编写到 QEMU 适配》

评论:0   参与:  5