D-Link: A Firmware Security Analysis – Part 4
This is the part you’ve been waiting for right? We’ve downloaded a firmware, scoured through it for hours, found a vulnerability, emulated it, and now it’s time to write an exploit. Our goal is simple: to gain control of the camera and make it do our bidding. Or at least run a command we supply.
In part three we overwrote the return address by sending an arbitrarily long string by holding down the A key for a while. This approach, while easy, is not very scientific and probably won’t result in reliable control over where we jump. Whip out your calculator, it’s time to do some math.
Understanding the Stack and Return Addresses
Before we get into overwriting the return address, it’s useful to know where the return address is. But first, let’s go over how the stack is laid out when the vulnerable function is called.
The stack will start at some high address, typically I’ve seen 0x7FFFFFFF, and grow down in memory. Reads and writes go up in memory. Now, with that knowledge let’s look at the prologue in the vulnerable function for a practical example.
The first operation,
addiu (add immediate unsigned), will move our stack pointer down in memory which is how it allocates space for the function’s local variables. The
sw (store word) operations below that are saving registers on the stack so they can be restored after this function is complete. The remaining space on the stack, which is not being used by the registers, is for local variables.
The top of the function’s disassembly in IDA the offsets of variables on the stack and based off the prologue we can rename the variables to be more easily tracked.
And what the stack actually looks like.
That Little Bit of Math
Now we are ready to figure out how many ’A’s to write to our buffer to test our ability to control execution. Looking back at the
strcpy in IDA, we need to find out which stack value is used as the destination buffer.
$a0 is populated with
var_30 which is the last piece of the puzzle. We now know where the destination buffer is on the stack, and we also know where the return address is stored.
Simple math time.
var_30, is at offset –30 and the return address,
$ra, is at offset –8 so to calculate the distance between the two is:
0x30 minus 0x8 which equals 0x28
If we write 0x28 bytes (40 decimal) into the buffer, it will take us right up to the return address. The next four bytes we write will overwrite the return address.
A wget command combined with IDA debugging will prove if this value is correct. I went back to user mode emulation for this so I didn’t need to set the referrer or provide a password.
wget http://127.0.0.1/wireless.htm?WEPEncryption=$(python3 -c "print('A' * 0x28, end=''); print('BBBB')")
$ra is now filled with 42424242 (BBBB) instead of 4141414 (AAAA) that we saw in part three? We have successfully overwritten the return address. We can now control execution. The next step is to identify a useful address to which we can jump to instead of 0x42424242.
Return to LibC Attack
The best option for executing a command on this camera: a return to libc attack. Basically, we want to call system with a string–provided by us–that will perform a useful operation. Calling
/sbin/reboot to restart the router is a great example. To accomplish this we need a few pieces of information.
- Address of libc
- Address of system
- Address of a gadget to place a string in $a0
- Offset on the stack to write our command
Finding the Address of LibC
Finding the address of libc is an easy task. Here’s why: the kernel for this firmware is 2.6.21. Library load randomization–a security feature–wasn’t a thing until kernel 2.6.36. Since we have an older kernel, the load address of the library will be consistent every time the server is run.
We need to
cat the process mapping for this process. The library for libc is libuClibc–0.9.28.so. Notice how it appears in the mappings three times. We need to chose the executable version so our code executes we when we jump to it.
The executable version is the one with the
x permission, or
r-xp. Based on the image below, our load address for libc is 0x2AB86000.
Finding the Address of System
Finding the address of system requires loading the libc library in IDA and viewing the offset of the system function. Once found, adding that offset to the load address of libc will provide the address of system when the process is running. From the image below and the address for libc, the loaded address of system should be
0x2AB86000 + 0x45080 = 0x2ABCB080
Finding the Address of a ROP Gadget
Next we need a gadget, known as a ROP gadget, that will move a stack address into
$a0, move an
$s register to
$t9, and call
$t9. This will allow us to call system with a command we provide in the overflow. Sadly, there are no single gadgets that move a stack pointer to
$a0 and call an
$s register, so this exploit required two gadgets. The two gadgets from libc I chose are:
---------------------------------------------------------------- | Gadget Name | Gadget Offset | Gadget Summary | ---------------------------------------------------------------- | ROP1 | 0x0004B0F8 | move $t9, $s0 | | | | jalr $t9 ; sub_4A890 | | | | addiu $s2, $sp, 0x1E8+var_F8 | ---------------------------------------------------------------- | ROP2 | 0x00018F40 | move $t9, $s1 | | | | jalr $t9 ; sub_18EB0 | | | | move $a0, $s2 | ----------------------------------------------------------------
Because both gadgets came from libc the math to find their loaded address is the same.
- ROP1 –
0x2AB86000 + 0x4B0F8 = 0x2ABD10F8
- ROP2 –
0x2AB86000 + 0x18F40 = 0x2AB9EF40
ROP1 will move a stack value from an offset into an
$s register. This operation happens in the third line of the gadget
addiu $s2, $sp, 0x1E8+var_F8. The string will come from offset
0x1E8 - 0xF8 = 0xF0 and be stored in register
ROP2 will move a function pointer from an
$s register to
$t9, move an
$s register to
$a0, and call
$t9. In our case, we will put the address of
$s1, so it will be moved to
$t9. We will place our string in
$s2 in the last gadget so that it will be moved to
$a0 which will become the the parameter when calling system.
In the end, we need our stack to look like this:
Time to Test the Firmware Exploit
The addresses are sent in reverse because of the little endian architecture in the firmware. The new URL looks like this:
If everything works out, this should call
system with the parameter
/sbin/reboot. If the system reboots, then we know that we can run any commands we want on this camera.
It worked! And I’m in.
Unfortunately, this exploitable vulnerability is present in a variety of D-Link cameras. Sadly, many remain unpatched at the time of this writing.
I sincerely hope you enjoyed this how-to on firmware security analysis and exploitation. For more information, you can watch my presentation here on YouTube.
About the Author
Evan Walls is a vulnerability analyst and developer at Tactical Network Solutions, focusing on embedded system exploitation. When he isn’t searching for new exploits, he teaches training courses developed by TNS, including IoT Firmware Exploitation. Prior to working at TNS he was a Windows developer for the Department of Defense. Now that he’s seen the glory and freedom that is Linux he vowed never to use another windows development machine again.
Backdoored firmware found in the supply chain of video surveillance chips from HiSilicon (a subsidiary of Huawei) allows remote access via Telnet.
A few days ago I decided to reverse engineer my router’s firmware image with binwalk. I’ve bought the TP-Link Archer C7 home router. Not one of the best, but good enough for my needs.
On February 4th, 2020 we deployed a new analyzer to the Centrifuge Platform, our automated firmware analysis platform which detects the presence of the Cable Haunt vulnerability in eCos-based firmware images.