技术现状
模糊测试物联网设备并不像模糊测试一个开源项目那样容易。通常源代码是专有的,这使得灰盒模糊测试——通过为最佳模糊测试性能对源代码进行插桩——变得不可能。此外,CPU架构通常不被模糊测试器原生支持,这需要一个像QEMU这样的模拟器,这也减慢了模糊测试的速度。另一个问题是硬件外设,这使得开发一种通用方法变得复杂。论文“嵌入式模糊测试:挑战、工具和解决方案的综述” 提供了不同模糊测试策略的概述,例如基于硬件的嵌入式模糊测试。大多数这些策略需要目标程序的源代码,例如当将模糊测试器的源代码,如AFL,移植到基于ARM的物联网设备上,在物联网硬件上运行模糊测试器。在设备的硬件上运行模糊测试器也存在性能问题,因为它们通常具有低级别的CPU,这些CPU比普通的桌面CPU慢。本文提出的另一种方法是仿真基础的嵌入式模糊测试。无论是在模拟器中执行单个目标程序以执行基于覆盖的模糊测试,还是执行整个系统。
上述方法都直接针对二进制文件,通过使用模拟器或通过对源代码进行插桩。这些方法需要一个模糊测试设置,通常必须为单个物联网设备特别定制,并且很难概括。为此,研究人员创建了一个名为IoTFuzzer
的程序,它旨在成为一个自动化的模糊测试框架,目的是“在没有访问其固件图像的情况下找到内存损坏漏洞”。IoTFuzzer
基于这样一个观察,即大多数物联网设备都有一个移动应用程序来控制它们,这些应用程序包含有关与设备通信所使用的协议的信息。然后,程序识别并重用特定于程序的逻辑来变异测试用例,以有效地测试物联网目标。
背景
框架
框架描述了一系列API调用,处理模糊测试器提供的输入。与通常不需要框架的普通应用程序不同,实现可重用函数的库必须使用正确的参数调用,并且还要按正确的顺序调用,以便在多个共享函数调用之间调用状态。在没有构建状态机的情况下随机模糊测试库不太可能成功,并且相反,当库依赖性没有得到强制执行时,会创建很多假阳性崩溃。当模糊测试器跳过缓冲区大小检查,导致虚假的缓冲区溢出时,就会发生这种情况。
在本文中,将模糊测试普通应用程序,但由于使用套接字和多线程的硬件依赖性,我们也需要为它们创建框架。框架在二进制文件的上下文中加载,并且可以调用目标程序的内部函数,如[代码10]所示。
语料库
“语料库”一词描述了有效的输入样本或测试用例,并在模糊测试过程中作为生成新输入数据的基础参考。在[代码10]中,这将是一个HTTP请求。然后模糊测试器利用这个语料库创建变异或多样化的测试用例,通过探索各种输入场景来帮助检测软件漏洞。
寻找潜在目标
黑盒模糊测试中最耗时的部分是在固件中找到一个潜在的易受攻击的函数。第一步是找到有趣的二进制文件,例如,可以通过网络访问,使用不安全函数或没有启用像堆栈金丝雀这样的安全特性,这是缓冲区溢出保护。我们上一篇论文(IoVT)已经描述了如何从目标路由器中提取固件以及如何找到一个潜在危险的二进制文件。为此,使用了工具EMBA(emba)。EMBA根据不安全函数(如strcpy
)、网络访问和安全保护(如堆栈金丝雀或NX位)的数量对固件中发现的所有二进制文件进行排名,这些在利用缓冲区溢出时变得有趣,可以在[代码1]中找到。
[+] STRCPY - top 10 results:
235 : libcmm.so : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | No Networking |
77 : wscd : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | Networking |
[snip]
28 : httpd : common linux file: yes | RELRO | No Canary | NX enabled | No Symbols | Networking |
27 : cli : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | No Networking |
由于本论文的目标是找到一个可以在网络上利用且无需知道管理员凭据的内存漏洞,因此易受攻击的函数必须可以通过网络调用,并且应该直接与提供的用户提供的输入交互。但具有网络交互并不意味着二进制文件也可以直接通过网络访问。为了找出哪些二进制文件正在监听,我们可以使用UART root shell,这在IoVT中已经建立。
~ # netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:20002 0.0.0.0:* LISTEN 1045/tmpd
tcp 0 0 0.0.0.0:1900 0.0.0.0:* LISTEN 1034/upnpd
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1027/httpd
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1224/dropbear
udp 0 0 0.0.0.0:20002 0.0.0.0:* 1048/tdpd
[...]
使用UART root shell执行netstat的代码示例,显示了哪些进程正在监听网络连接。
二进制文件逆向
看起来有希望的第一个二进制文件是wscd
。这个二进制文件拥有最多的不安全strcpy
调用(除了libcmm.so库),并且具有网络交互,对于wscd
来说,这意味着它连接到一个UPnP
设备,并不监听特定端口。如后文所示,它有一个容易模糊测试的函数,这就是为什么这个二进制文件被选为本论文的示例,以解释一般程序。在逆向之前,我们可以使用UART root shell来找出二进制文件是否正在运行以及它是如何启动的。
$ ps
PID USER VSZ STAT COMMAND
962 admin 1096 S wscd -i ra0 -m 1 -w /var/tmp/wsc_upnp/
1018 admin 1080 S wscd_5G -i rai0 -m 1 -w /var/tmp/wsc_upnp_5G/
使用ps
我们不仅可以看到二进制文件正在运行,还可以看到参数是什么,这些参数对于验证潜在函数是否被调用非常重要。这些参数的含义可以从CLI帮助中获得,当不传递任何参数调用二进制文件时会显示这个帮助。
$ chroot root /qemu-mipsel-static /usr/bin/wscd
Usage: wscd [-i infName] [-a ipaddress] [-p port] [-f descDoc] [-w webRootDir] -m UPnPOpMode -D [-d debugLevel] -h
-i: Interface name this daemon will run wsc protocol(if not set, will use the default interface name - ra0)
e.g.: ra0
-w: Filesystem path where descDoc and web files related to the device are stored
e.g.: /etc/xml/
-m: UPnP system operation mode
1: Enable UPnP Device service(Support Enrolle or Proxy functions)
2: Enable UPnP Control Point service(Support Registratr function)
3: Enable both UPnP device service and Control Point services.
[...]
由于wscd以“启用UPnP设备服务”启动,看起来有希望。在验证路由器上实际上正在运行二进制文件之后,然后可以使用Ghidra分析二进制文件,以搜索可疑函数。对于模糊测试,解析函数特别有趣,因为它们通常很复杂,而且通常解析的输入具有包含数据的长度字段,例如TCP数据包包含有效载荷的长度。
![实战 QEMU+ AFL++ Fuzz TP-Link WR902AC路由器 实战 QEMU+ AFL++ Fuzz TP-Link WR902AC路由器](https://ctfiot.oss-cn-beijing.aliyuncs.com/uploads/2024/07/10-1721015150.png)
另一个解析函数的好处是,它们通常不与代码的其他部分交互,或者通过网络与用户交互。因此,可以直接用输入调用解析函数而无需修改二进制文件或覆盖其他函数,这样就可以进行模糊测试。
在开始对函数进行模糊测试之前,应该先检查该函数是否真的被触发,因为只有当该函数使用用户可控的输入被调用时,它才有意义。为此,可以使用Ghidra搜索对目标函数的引用。以parser_parse
函数为例,有多种方式。因为我们知道程序是如何启动的,可以将其调用减少到单个函数调用树,如图[代码5]所示。
main()
if ((WscUPnPOpMode & 1) != 0) // 参数 -m 1
WscUPnPDevStart()
UpnpDownloadXmlDoc() -> my_http_Download() -> http_Download()
if (http_MakeMessage())
http_RequestAndResponse()
http_RecvMessage()
找到目标函数后,现在我们可以创建一个模糊测试设置来测试该函数,这将在下一部分中描述。但首先,将展示其他潜在的易受攻击的函数。
其他潜在的易受攻击的函数
对于这篇论文,我们手动分析了多个潜在的二进制文件,以寻找可疑的函数。以下是其他可能目标的简短摘要。
二进制文件httpd是管理员网页界面的后端。该二进制文件可以通过80端口在网络上访问。httpd
中的一个有趣函数是httpd_parser_main
函数。使用Ghidra浏览解析器实现时,可以识别到几个不同的可疑代码部分。其中一个可疑部分是解析Content-Type
。下面是一个基本的HTTP请求示例。
POST / HTTP/1.1rn
Content-Type: multipart/form-data; boundary=X;rn
Host: example.comrn
rn
rn
DATArn
下面是httpd_parser_main
函数的一个片段,它解析用户提供的HTTP请求中的Content-Type
。
// user_input_ptr指向
// "Content-Type: multipart/form-data; boundary=X;rnHost: example.comrn..."
cursor = strstr(user_input_ptr,"multipart/form-data");
if (user_input_ptr == cursor) {
cursor = strstr(user_input_ptr,"boundary=");
user_input_ptr = cursor + 9;
// user_input_ptr现在指向"X;rnHost: example.comrn..."
if (cursor != (char *)0x0) {
do {
while (cursor = user_input_ptr, *cursor == " ") {
user_input_ptr = cursor + 1;
}
user_input_ptr = cursor + 1;
} while (*cursor == "t");
// cursor现在指向"X;rnHost: example.comrn..."
// strchr返回用户请求中";"的第一个出现位置的指针。
// 如果没有找到";",函数返回一个空指针。
user_input_ptr = strchr(cursor, ";");
if (user_input_ptr != (char *)0x0) {
// 将";"字符替换为null字节以终止字符串
*user_input_ptr = " ";
// cursor现在指向"X rnHost: example.comrn..."
}
// DAT_00444050全局数组从0x00444050到0x0044414f(255字节)
strcpy(&DAT_00444050, cursor);
// DAT_00444050现在包含"X"
}
}
代码 6: 函数parser_parse的调用树
这段代码中的漏洞是strcpy
函数调用和假设Content-Type
以分号结束。因为strcpy
会复制缓冲区直到遇到下一个null字节,并且如[代码6]所示,只有当找到分号时才会添加null字节。通过移除分号,下一个null字节位于输入缓冲区的末尾,例如,HTTP请求的末尾。因此,全局变量DAT_00444050可能会溢出,随后覆盖地址0x0044414f之外的数据。具有挑战性的部分不仅是找到一个有趣的全局变量在这个地址之外可以被覆盖,而且还因为strcpy
的存在,不能使用null字节。但当存在这样一个错误时,可能还有更多类似的错误等待发现。
二进制文件tdpd由移动应用程序使用,并且可以通过本地网络上的UDP进行访问。tdpd
几乎拥有与tmpd
相同的功能,但大多数功能从未被调用。主函数仅监听UDP端口上的消息,并总是以基本的路由器信息响应,例如名称或型号。几乎没有与用户提供的输入交互,因此不适合模糊测试。
另一对有趣的二进制文件是upnpd和ushare。这两个二进制文件处理UPnP
消息,因此需要解析XML。因为在二进制文件中可以找到版权声明,可以假设这些程序不是由TP-Link开发的。
$ strings usr/bin/ushare | grep "(C)"
Benjamin Zores (C) 2005-2007, for GeeXboX Team.
这两个二进制文件都加载了共享库libupnp.so
和libixml.so
,这些库与开源项目pupnp
的功能相同。由于本文的重点在于黑盒模糊测试,因此忽略了这些二进制文件。但是,灰盒模糊测试这个库可能有潜力,因为在2021年在libixml.so
中发现了一个内存泄漏。
二进制文件tmpd是移动应用程序的后端。有趣的部分是路由器和移动应用程序通过自定义的二进制协议进行通信。下面显示了从客户端到服务器的消息。
00000000 01 00 05 00 00 08 00 00 00 00 00 17 50 7b 6e fe |............P{n.|
00000010 01 01 02 00 00 00 00 00 |........ |
为了理解二进制协议,使用Ghidra对二进制文件tmpd
进行了逆向。有了这些信息,[代码 7]中的消息可以分解如下:
01 00 05 00 : 版本
00 08 00 00 : 大小(8字节)
00 00 00 17 : 数据类型
50 7b 6e fe : 校验和(CRC32)
01 01 : 选项
02 00 : 功能ID
00 00 00 00 : 功能参数
这看起来很有希望,因为这样的二进制协议必须被解析。但是二进制协议最可疑的部分不是长度字段,而是功能ID和功能参数的使用。
![实战 QEMU+ AFL++ Fuzz TP-Link WR902AC路由器 实战 QEMU+ AFL++ Fuzz TP-Link WR902AC路由器](https://ctfiot.oss-cn-beijing.aliyuncs.com/uploads/2024/07/8-1721015155.png)
[图 2]显示了自定义协议的解析函数的一部分。在第16行,提取了功能ID,然后在第29行调用了相应的函数。可疑的行为是,函数被调用时,从用户可控的输入缓冲区提取的参数没有经过任何检查。我们现在可以尝试在[图 3]中显示的跳跃表中找到一个可能危险的功能,例如,当参数被用作缓冲区的索引或被解释为字符串时。与其手动逆向和搜索100多个功能,这将非常耗时,我们可以使用模糊测试器自动完成这项工作。
![实战 QEMU+ AFL++ Fuzz TP-Link WR902AC路由器 实战 QEMU+ AFL++ Fuzz TP-Link WR902AC路由器](https://ctfiot.oss-cn-beijing.aliyuncs.com/uploads/2024/07/4-1721015157.png)
[图 3]显示了tmpd
解析功能ID及其参数的函数的一部分。
不幸的是,tmpd
二进制文件只能通过网络本地访问,如[代码 2]所示。要连接到这个二进制文件,应用程序首先通过SSH以direct-tcpip
模式连接到路由器,这仅仅是将数据包转发到本地进程。而SSH连接由管理员凭据保护。但是,如IoVT中所述,SSH连接很容易被破坏,因为应用程序从不检查服务器的主机密钥。通过丢弃所有路由到互联网的数据包,可以在执行中间人攻击以窃取凭据的同时,诱使管理员登录到路由器。
使用AFL++和QEMU进行模糊测试
在这一部分中,我们将为之前找到的函数之一开发一个测试框架。测试框架开发完成后,将使用最先进的模糊测试工具AFL++进行目标函数的模糊测试。由于这些二进制文件是为mipsel
架构编译的,因此使用模拟器QEMU来执行二进制文件。本文中使用的基本模糊测试设置大多受Adam Van Prooyen的“固件模糊测试101”博客条目的启发。
模糊测试环境
为了轻松创建一个可复现的模糊测试环境,Docker是最佳选择。我们创建了一个Dockerfile,它安装了所有必要的工具,如用于mipsel
CPU架构的交叉编译器或可用于调试测试框架的gdb-multiarch
。
此外,AFLplusplus与QEMU一起下载并编译,QEMU构建的版本有小的调整,允许在afl-fuzz下运行非插桩二进制文件。
FROM debian:latest
RUN apt update && apt install -y
curl
vim
gcc-mipsel-linux-gnu
openssh-server
qemu-user-static
gdb-multiarch
# Qemu静态文件安装在/usr/bin/qemu-mipsel-static
# 编译AFL++
RUN apt install -y git make build-essential clang ninja-build pkg-config libglib2.0-dev libpixman-1-dev
RUN git clone https://github.com/AFLplusplus/AFLplusplus /AFLplusplus
WORKDIR /AFLplusplus
RUN make all
WORKDIR /AFLplusplus/qemu_mode
RUN CPU_TARGET=mipsel ./build_qemu_support.sh
RUN echo "#!/bin/bashnnsleep infinity" >> /entry.sh
RUN chmod +x /entry.sh
WORKDIR /share
ENTRYPOINT [ "/entry.sh" ]
安装必要工具的Dockerfile。
然后可以使用docker build
构建镜像。
docker build -t fuzz .
构建镜像后,可以使用docker run
轻松使用它,然后启动容器。
docker run -d --rm -v $PWD/:/share --name fuzz fuzz
使用-d
选项将在后台启动容器。使用docker exec
可以在容器内启动多个shell,这有助于在一个会话中使用QEMU启动可执行文件,在另一个会话中使用gdb-multiarch
。
docker exec -it fuzz /bin/bash
重写main函数
在前一节中,我们确定了潜在的模糊测试目标。问题是在执行二进制文件时,我们永远不会到达函数调用,因为只有当通过套接字接收到TCP数据包时才会调用parser_parse
函数。这不仅对性能不利,而且难以设置。因此,模糊测试器的入口应该在与正常main函数不同的位置。为此,可以使用环境变量LD_PRELOAD
,它允许注入可以访问内部函数的测试框架。正如负责在运行时链接可执行文件所需的共享库的ld.so
的手册页所述,LD_PRELOAD
可以用于”有选择地覆盖其他共享对象中的函数”。
__uClibc_main
函数最适合此目的。要覆盖此函数,必须创建一个包含同名函数的C文件。
void __uClibc_main(void *main, int argc, char** argv) {
// Harness code, e.g. call the function parser_append
printf("My custom __uClibc_main was called!");
}
C文件随后可以使用mipsel-linux-gnu-gcc
交叉编译为mipsel架构的共享对象。选项-fPIC
启用“位置无关代码”,这意味着机器代码不依赖于位于特定地址,而是使用相对寻址而不是绝对寻址。
$ mipsel-linux-gnu-gcc parser_parse_hook.c -o parser_parse_hook.o -shared -fPIC
然后可以通过将环境变量LD_PRELOAD
添加到QEMU命令来加载新创建的共享库。
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o /usr/bin/wscd
My custom __uClibc_main was called!
使用chroot
命令可以更改当前和根目录以供提供的命令使用。这很有帮助,因为可执行文件wscd
会打开其他文件,比如固件中的共享库。我们可以通过将-strace
参数添加到QEMU来看到这种行为。
chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o -strace /usr/bin/wscd /corpus/notify.txt
38180 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|0x4000000,-1,0) = 0x7f7e7000
38180 stat("/etc/ld.so.cache",0x7ffffa48) = -1 errno=2 (No such file or directory)
38180 open("/parser_parse_hook.o",O_RDONLY) = 3
38180 fstat(3,0x7ffff920) = 0
38180 close(3) = 0
38180 munmap(0x7f7e6000,4096) = 0
38180 open("/lib/libpthread.so.0",O_RDONLY) = 3
38180 open("/lib/libc.so.0",O_RDONLY) = 3
...
如我们所见,该可执行文件在固件的/lib/
文件夹中打开了多个库,而不是在主机上。
开发和调试测试框架
创建设置后,我们现在可以开始开发测试框架。如背景部分所述,测试框架是模糊测试器和目标函数之间的驱动程序。测试框架加载由AFL++存储在文件中的模糊输入。使用文件路径作为参数,测试框架随后调用模糊测试目标;在这种情况下,将是parser_append
。可以通过使用地址来调用函数。
void __uClibc_main(void *main, int argc, char** argv)
{
// 验证是否提供了文件名
if (argc != 2) exit(1);
// 创建指向模糊测试目标的函数指针
int (*parser_request_init)(void *, int) = (void *) 0x00412564;
int (*parser_append)(void *, void *, int) = (void *) 0x00412e98;
// 打开模糊输入文件
int fd = open(argv[1], O_RDONLY);
char fuzz_buf[2048 + 1];
int fuzz_buf_len = read(fd, fuzz_buf, sizeof(fuzz_buf) - 1);
if (fuzz_buf_len < 0) exit(1);
fuzz_buf[fuzz_buf_len] = 0;
// 调用目标函数
uint8_t parsed_data[220];
parser_request_init(parsed_data, 8);
int status = parser_append(parsed_data, fuzz_buf, fuzz_buf_len);
printf("Response is %dn", status);
exit(0);
}
如[代码 10]所示,函数parser_parse
不是直接调用的,而是通过使用函数parser_append
。在调用此函数之前,必须先调用初始化函数parser_request_init
,该函数初始化parser_parse
函数的输出结构。
虽然在parser_parse
的情况下,测试框架很容易设置,但其他目标需要更复杂的测试框架,比如httpd_parser_main
函数。例如,在调用目标之前,必须调用函数http_init_main
,该函数以SIGSEGV结束。为了找出这个段错误是由哪里引起的,使用像gdb
这样的调试器调试代码非常有用。为此,可以使用带有-g
选项的QEMU启动,该选项在提供的端口上生成一个gdb-server
。
chroot root /qemu-mipsel-static -strace -g 1234 -E LD_PRELOAD="/httpd_parser_main.o" /usr/bin/httpd
corpus/httpd/simple.txt
由于二进制文件是mipsel
架构的,所以必须使用gdb-multiarch
。启动gdb后,可以使用sources <path to script>
命令加载以下初始化脚本。
set solib-absolute-prefix /share/root/
file /share/root/usr/bin/httpd
target remote :1234
# 在调用模糊测试目标之前设置断点
# 断点 __uClibc_main
break http_parser_main
display/4i $pc
由于使用了chroot,脚本首先更改了绝对前缀路径,以便当二进制文件加载共享对象时,gdb能找到文件。然后设置目标文件,因为QEMU的gdb-server不支持文件传输,所以gdb尝试从磁盘加载文件。配置好gdb后,脚本使用target remote
连接到gdb-server,并在目标函数开始处创建断点。使用display命令可以改善输出,当单步执行时,将显示下四行汇编代码。使用si
(step instruction)可以单步执行一条指令,这在测试框架使用默认语料库时发生段错误非常有用,而默认语料库应该始终有效。如[代码 11]所示,二进制文件在fprintf
函数中发生了段错误。
(gdb) si
0x004059b0 in http_parser_makeHeader ()
1: x/4i $pc
=> 0x4059b0 <http_parser_makeHeader+120>: jalr t9
0x4059b4 <http_parser_makeHeader+124>: addiu a1,a1,16248
0x4059b8 <http_parser_makeHeader+128>: li v0,200
0x4059bc <http_parser_makeHeader+132>: lw gp,16(sp)
(gdb) ni
0x7f56a8ac in fprintf () from /share/root/lib/libc.so.0
1: x/4i $pc
=> 0x7f56a8ac <fprintf+44>: bal 0x7f56db80 <vfprintf>
0x7f56a8b0 <fprintf+48>: nop
0x7f56a8b4 <fprintf+52>: lw ra,36(sp)
0x7f56a8b8 <fprintf+56>: jr ra
0x7f56a8bc <fprintf+60>: addiu sp,sp,40
(gdb) n
在fprintf函数中单步执行,直到退出,该函数没有行号信息。
程序接收到信号SIGSEGV,段错误。
为了调查错误,可以使用Ghidra找出函数被调用时使用的参数。
fprintf(
*(FILE **)(iVar1 + 0x101c),
"HTTP/1.1 %d %srn",
*(undefined4 *)(&DAT_0042ee68 + (uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 8),
(&PTR_DAT_0042ee6c)[(uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 2]
);
SIGSEGV很可能是由于第一个参数不是文件描述符而是空指针引起的。其中iVar1
只是对httpd_parser_main
函数输入的引用。这意味着模糊测试输入必须在位置0x101c处有一个文件描述符。因此,输入必须调整为以下结构。
typedef struct {
int _a; // 4字节
int _b; // 4字节
int socket; // 4字节
int ip; // 4字节
int mac; // 4字节
unsigned char body[0x1008]; 0x101c - 4*5 = 0x1008字节
FILE * fd_out; // 应该是一个有效的文件描述符
} HttpMainT;
由于fd_out
必须只是一个有效的文件描述符指针,它可以很容易地设置为stdout
。再次执行httpd_parser_main
现在将产生有效的HTTP输出。
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/httpd_parser_main.o
/usr/bin/httpd /httpd_corpus.txt
bind: No such file or directory
[ dm_shmInit ] 086: shmget to exitst shared memory failed. Could not create shared memory.
rdp_getObj is called with: 4274932gdpr_getSystemGDPREntry Error
gdpr_getNewSystemGDPREntry OK
#Msg: getsockname error
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 24257
Set-Cookie: JSESSIONID=deleted; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Path=/; HttpOnly
Connection: close
<!DOCTYPE html>
[...]
测试框架现在可以使用了,可以在下一节中解释的AFL++的帮助下用于模糊测试该函数。
生成语料库数据
如背景部分所述,种子语料库描述了有效的输入样本,这些样本在模糊测试过程中作为生成新输入数据的基础参考。
这些输入通常被选择来代表目标程序的不同方面。种子语料库由模糊测试器用来生成变异或进化的测试用例,然后对目标软件运行这些测试用例,以发现错误、崩溃或其他问题。这个语料库在引导模糊测试器到程序的相关区域和增加检测漏洞或意外行为的可能性方面发挥着重要作用。通过提供多样化和有代表性的初始输入集,种子语料库帮助模糊测试器更快地探索目标的不同路径,从而增加了覆盖率。
当涉及到解析网络数据的函数时,可以通过使用Wireshark记录不同的数据包来创建这些输入。
对于函数httpd_parse_main
,创建了四种不同的语料库。每个语料库都针对二进制文件中的不同路径。一个例子是登录请求,其中包含用户名和密码。对于这个语料库,必须修改测试框架,因为TP-Link使用(弱)加密来“保护”密码。在这里,密码在浏览器中使用AES加密,然后在后端解密。密码在浏览器中生成,然后使用RSA加密。然后加密的数据被签名。由于模糊测试器不能创建签名或加密数据,一些函数被重写,现在只是从base64解码。为此,首先使用调试器从浏览器中提取明文数据,如图4所示。
![实战 QEMU+ AFL++ Fuzz TP-Link WR902AC路由器 实战 QEMU+ AFL++ Fuzz TP-Link WR902AC路由器](https://ctfiot.oss-cn-beijing.aliyuncs.com/uploads/2024/07/2-1721015160.png)
然后,在目标中重写了函数rsa_tmp_decrypt_bypart
,以用从base64解码的逻辑替换解密数据的逻辑。
// 用b64_decode替换逻辑
int rsa_tmp_decrypt_bypart(uint8_t *input, int input_len, uint8_t *output) { // 其他参数只是密钥数据
int (*b64_decode)(uint8_t *, int, uint8_t *, int) = (void *) 0x0040bf00;
b64_decode(output, 0x1000, input, input_len);
int * seqnumber = (int *) 0x00444db0;
*seqnumber = 0x3ac28e29-input_len+12;
return 0; // 表示正常
}
代码 12: 函数rsa_tmp_decrypt_bypart现在只是解码base64而不是解密数据。
在执行语料库时,目标函数总是返回一个带有错误”408 Request Timeout”的HTML文档。使用Ghidra和GDB可以确定问题。错误总是在调用http_stream_fgets
函数后发生。有问题的行是检查换行字符n
。
if (((cVar1 == 'n') && (param_3 < pcVar4)) && (pcVar4[-1] == 'r')) {
这个条件强制规定每个换行符之后必须跟一个回车符。添加回车符后,所有创建的语料库都工作正常。
模糊测试目标
在上一节中,我们使用QEMU开发了多个测试框架。在本节中,QEMU被AFL++替换,AFL++获取生成的语料库作为种子输入,以模糊测试目标函数。在”模糊测试环境”部分创建了一个docker镜像,该镜像已经从GitHub拉取了AFL++,然后使用AFL++提供的脚本构建了一个补丁版本的QEMU。所以现在可以使用以下命令启动AFL++,该命令得到了不同的参数,如-Q
,它告诉AFL++使用补丁版本的QEMU。
QEMU_LD_PREFIX=/share/root AFL_PRELOAD=/share/root/httpd_parser_main.o
/AFLplusplus/afl-fuzz -Q
-i /share/root/corpus/httpd/ -o /share/afl-out/httpd/
-- /share/root/usr/bin/httpd @@
与之前不同,命令chroot
不再需要,取而代之的是变量QEMU_LD_PREFIX
。它告诉QEMU在哪里搜索共享对象。LD_PRELOAD
变量也被AFL特定的版本AFL_PRELOAD
替换。命令中的最后一个参数是两个@
字符。它们将被AFL++替换为包含模糊测试输入的文件路径。启动后,AFL++使用图5所示的终端UI显示进度。
AFL++的状态屏幕提供了当前模糊测试过程的重要信息。AFL++的文档在状态屏幕中使用术语的概述。当使用以下环境变量调试语料库时,可以禁用UI,并使用AFL_DEBUG
启用详细日志记录,它显示当前的模糊测试器输入和目标程序的stdout
。
export AFL_DEBUG=1 && export AFL_NO_UI=1
unset AFL_DEBUG && unset AFL_NO_UI
正如在[图 5]中所示,模糊测试一个二进制文件可能需要相当长的时间。根据文档,我们应该预期它“需要运行几天或几周”,并且“有些工作将被允许运行数月”。为了缩短所需的时间,执行速度应该高于每秒100次执行。例如,当目标httpd_main_parser
被模糊测试时,开始时的执行速度大约是每秒30次。为了提高速度,我们寻找可能导致减速的可疑函数。其中一个可疑的函数是rsa_gdpr_generate_key
,因为生成RSA密钥已知是缓慢的。重写该函数后,速度提高到每秒600次执行。
一个有助于指示何时停止模糊测试的指标是循环计数器。当“模糊测试器已经有一段时间没有看到任何动作”时,AFL++会将数字突出显示为绿色,这有助于决定停止模糊测试器。
但最有趣的数字可能是“总崩溃次数”。这显示了程序因为当前的模糊测试输入而崩溃,很可能是与内存相关的漏洞。为了验证这是一个真正的漏洞,可以再次使用gdb
来找到漏洞的位置。
原文始发于微信公众号(3072):实战 QEMU+ AFL++ Fuzz TP-Link WR902AC路由器