IoT固件Fuzz:从Harness编写到QEMU适配

本文旨在探讨一种针对 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);
}

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

  1. Hook httpd 的 main 函数。
  2. 将 HTTP 请求置于文件中,文件名作为 httpd 参数输入。
  3. 伪造一个可读写的文件描述符,将输入的 HTTP 请求写入该描述符,供 httpd 读取。
  4. 伪造 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
// 兼容封装:优先使用 memfd_create 系统调用;不可用时回退到匿名临时文件
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
// Fallback: 使用匿名临时文件模拟(并非真正的 memfd,但可读写且不落磁盘路径)
FILE *tf = tmpfile();
if (!tf) return -1;
int fd = dup(fileno(tf));
fclose(tf);
return fd;
#endif
}

// 从文件读入全部内容到匿名内存文件(memfd),并回到偏移 0,返回该 fd
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; // 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 结构体定义如下:

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; /* next element */ \
struct type **tqe_prev; /* address of previous next element */ \
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) {

// 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
return uClibc_main_orig(main_hook, argc, argv, app_init, app_fini, rtld_fini, stack_end);
}

// 在 .init 段执行的 constructor
__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() {
// 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,存在以下三种方案:

  1. 若操作系统存在 NVRAM 包,可直接安装并尝试适配使用,该方案最为简便。
  2. 当默认 NVRAM 驱动与 IoT 设备不匹配时,可通过逆向 IoT 固件中的 NVRAM 驱动,参考其代码,借助 AI 编写适配当前机器的 NVRAM 驱动。
  3. 可通过逆向 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
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 方案及过程将在后续文章中进行分享。

IoT固件Fuzz:从Harness编写到QEMU适配

https://nobb.site/2025/11/13/fuzz2/

Author

Hcamael

Posted on

2025-11-13

Updated on

2025-12-16

Licensed under