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!
Here’s what’s happening.
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.
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:
Referring back to the header/image diagram from above, the firmware layout now looks like:
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()
.