Thursday, September 17, 2015

Broken, Abandoned, and Forgotten Code, Part 12

In the previous part, I described how to strip out all but the most essential services and libraries in the stock firmware in order to get the firmware image down to under 4MB. This avoids crashing upnpd, which allocates less than half enough memory to base64 decode a stock-sized firmware image.

In this part, we'll walk through a crasher you might encounter (or might not, depending how you formatted your ambit header) and how to sidestep it.

Updated Exploit Code

I last updated the exploit code for part 11, when we added a missing checksum to the ambit header that prevents the router from booting if missing. In this part I've updated the code to add an additional field to the ambit header that, in some cases, will prevent a post-exploitation crash in upnpd. If you've previously cloned the repository, now would be a good time to do a pull. You can clone the git repo from:
https://github.com/zcutlip/broken_abandoned

An Invalid free() Crashes the Party

Remember how this firmware updating "feature" in upnpd is buggy and only partially implemented? Well right at the very end, after upnpd writes the size/checksum footer to the flash partition, the decoded firmware buffer gets passed to free(). All good, right? Except not really, because it isn't exactly the decoded firmware buffer. It's the buffer plus ambit header size. Oh shit!

Free invalid pointer
Oh noes! Death can occur!

Here's what's happening.

ambit_trx_free
Oh snap! We free()ed the wrong thing.
This is super shitty. If upnpd crashes before it can reboot the target, we're sunk; we've lost control of the device at that point.

In some cases this won't crash the program, though who knows in what state the process's heap will be. Other times this definitely results in a crash. In order to know why, it helps to understand a little about how libc dynamically allocates memory.

Spelunking in free() and malloc()

uClibc, the C library the Netgear R6200 uses, has three different malloc/free implementations: malloc, malloc-standard, and malloc-simple. Which one gets used is determined at compile time. Which implementation our device uses can be verified by first finding a symbol that is only referenced by a single malloc implementation.


$ grep -rnl __malloc_state stdlib
stdlib/malloc-standard/malloc.c
stdlib/malloc-standard/malloc.h

Grepping through uClibc source, it appears __malloc_state is referenced only by the malloc-standard implementation. Check for that symbol in the target's libc.


$ strings libc.so.0  | grep -i malloc
malloc
__malloc_lock
__malloc_state
...

Presence of the __malloc_state symbol indicates the target's libc is built with the malloc-standard implementation. We can now focus source code analysis in the right place. Let's have a look at uClibc source, specifically /libc/stdlib/malloc-standard/malloc.h.

struct malloc_chunk {

  size_t      prev_size;  /* Size of previous chunk (if free).  */
  size_t      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd; /* double links -- used only if free. */
  struct malloc_chunk* bk;
};

/*
    An allocated chunk looks like this:


    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-/ /-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of previous chunk, if allocated    | |
            +-+-+-+-+-+-+-+-+-+-+-+-/ /-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk, in bytes                 |P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-/ /-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             User data starts here...                  .
            .                                                       .
            .             (malloc_usable_space() bytes)             .
            .                                                       |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-/ /-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk                             |
            +-+-+-+-+-+-+-+-+-+-+-+-/ /-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/

See, when you call malloc(), the pointer you get back (and later pass to free()) doesn't actually mark the beginning of the chunk of memory allocated. There is metadata prepended to your buffer. Although malloc implementations vary, what you see above is fairly typical. There is a size of the current allocated chunk, as well as the size of the previous chunk, if there is one.

If you pass an arbitrary address to free(), there's no telling what's going to happen. This is undefined behavior and what happens next depends on the malloc implementation, the state of the heap, and the chunk metadata. Maybe nothing will happen. Or there could be heap corruption, which may or may not be exploitable. Alternatively, the program could crash in free() if an invalid dereference occurs.

As I was reverse engineering the R6200's firmware header, upnpd crashed predictably under certain conditions. When the chunk metadata is used to compute a pointer to the next chunk, the result was an invalid address. Then the dereference of the nextchunk pointer caused a crash.

Crash in free()
Location of the crash in free() due to freeing an invalid pointer.


A way of avoiding the crash is to insert fake chunk metadata in the firmware header[1]. It is the address of the TRX image in memory that upnpd attempts to free. Unfortunately the only way to cause free() to bail immediately is to pass it a NULL pointer. However, if it thinks the allocated memory chunk is zero bytes, it takes a much shorter path and avoids the crash. So, right at the end of the firmware header and before the TRX image, you may insert a 4-byte "chunk metadata" field equal to zero.


    FREE_FIXUP_OFF=BOARD_ID_OFF+len(BOARD_ID)
    FREE_FIXUP=0x0
    def __build_header(self,checksum=0,logger=None):
        
        SC=SectionCreator(self.endianness,logger=logger)

        """
        ...abbreviated...
        """

        SC.string_section(self.BOARD_ID_OFF,self.BOARD_ID,
                            description="Board id string.")
        
        SC.gadget_section(self.FREE_FIXUP_OFF,self.FREE_FIXUP,
                            description="fake mem chunk metadata to avoid crashing.")


The resulting firmware header looks like:

fake chunk metadata


Referring back to the header/image diagram from above, the firmware layout now looks like:

ambit_trx_free2


This still may result in some heap corruption, but the firmware has already been written, and upnpd is moments away from rebooting the device. We only need to avoid crashing long enough for the reboot.

In the next two parts, we finish up with a discussion of post-exploitation. As of this part we have successfully exploited the SetFirmware SOAP action, causing upnpd to overwrite the firmware with arbitrary data of our choosing. The next steps will be to make that data useful for persisting remote access to the target. Stay tuned!

------------------------------
[1] Credit to former colleague @dongrote for suggesting playing games with malloc metadata might help avoid crashing in free().