文章总结: 本文介绍了一种针对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,逆向分析,代码审计
原创 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);
}
基于此,可以构建如下思路:
- Hook httpd 的 main 函数。
- 将 HTTP 请求置于文件中,文件名作为 httpd 参数输入。
- 伪造一个可读写的文件描述符,将输入的 HTTP 请求写入该描述符,供 httpd 读取。
- 伪造
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 < 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 函数,将文件内容转换为可读写的文件描述符,实现逻辑如下:
// 兼容封装:优先使用 memfd_create 系统调用;不可用时回退到匿名临时文件
staticintmemfd_create_compat(constchar*name,unsignedint 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
// Fallback: 使用匿名临时文件模拟(并非真正的 memfd,但可读写且不落磁盘路径)
FILE *tf =tmpfile();
if(!tf)return-1;
int fd =dup(fileno(tf));
fclose(tf);
return fd;
#endif
}
// 从文件读入全部内容到匿名内存文件(memfd),并回到偏移 0,返回该 fd
staticintcreate_memfd_from_file(constchar*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; // EOF
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 结构体定义如下:
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;/* next element */ \
struct type **tqe_prev;/* address of previous next element */ \
TRACEBUF \
}
typedefunion{
struct sockaddr sa;
struct sockaddr_in sa_in;
#if defined (USE_IPV6)
struct sockaddr_in6 sa_in6;
#endif
} usockaddr;
typedefstruct conn_item {
TAILQ_ENTRY(conn_item) entry;
int fd;
#if defined (SUPPORT_HTTPS)
int ssl;
#endif
usockaddr usa;
} conn_item_t;
此外,在调试模式下,还需支持输出 HTTP 请求结果。
Hook main 函数的方法如下:
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){
// debug
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);
// ... and call it with our custom main function
returnuClibc_main_orig(main_hook, argc, argv, app_init, app_fini, rtld_fini, stack_end);
}
// 在 .init 段执行的 constructor
__attribute__((constructor))
staticvoidharness_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 端口之前,也会执行初始化操作,如设置管理员账号密码等,因此需执行同样的初始化动作。如下所示:
typedefvoid*(*HANDLE_RESET_LOGIN_DATA)(void);
HANDLE_RESET_LOGIN_DATA handle_reset_login_data =(HANDLE_RESET_LOGIN_DATA)0x402ED8;
typedefvoid*(*HANDLE_LOAD_NVRAM_AUTH)(void);
HANDLE_LOAD_NVRAM_AUTH handle_load_nvram_auth =(HANDLE_LOAD_NVRAM_AUTH)0x402DE0;
voidinit(){
// httpd处理请求前的初始化
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 数据并导入当前机器,以实现更真实的仿真。
鉴于代码篇幅较长,此处仅展示用法,如下所示:
$ 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 进行相应修改,经研究,较为简便的修改方案如下:
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 @@
#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)))
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,
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 方案及过程将在后续文章中进行分享。
往 期 热 门
(点击图片跳转)
戳“阅读原文”更多精彩内容!
查看原文:《原创 Paper | IoT 固件 Fuzz:从 Harness 编写到 QEMU 适配》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论