Tuesday, October 23, 2012

Specifying Preferred Load Addresses for ELF Shared Libraries

[NOTE: This was going to be a post about how to relocate a shared library that is loaded using LD_PRELOAD such that a program's linked libraries get loaded at their normal addresses.  Sadly, the trick I thought would do that didn't actually work for me.  The library got relocated, but the other libraries weren't restored to their natural base addresses.  That said, it still is interesting and worth writing up.]

I'm currently developing a buffer overflow exploit for an application that runs on an embedded device.  I don't have console access to the live device--in fact the main goal for this exploit is to get an interactive shell so that I can do more analysis.  Since I don't have console access, I have to debug the application and the exploit entirely in the QEMU emulator.

I have to use several ROP gadgets in my exploit, so I need to test and develop with the libraries containing the gadgets loaded at their normal addresses.  Hence, I'm using QEMU full system emulation running Debian MIPS. QEMU binary emulation won't load the application's libraries in the right location.

There's a hitch, however.  The emulated system doesn't provide the same hardware that the target device has, so I have to convince the target application it has the right environment.  One of the many tricks required is to intercept NVRAM queries and provide the right answers.  In order to do this, I wrote an NVRAM faker library and LD_PRELOAD it when I run the target application.

Did I say there's a hitch? Actually there are a bunch of hitches.  Another problem is that when you LD_PRELOAD a library, it gets loaded before the application's other libraries.  As a result, the load addresses are all off from where they would normally be.  Since the whole point here is to run the application with the libraries loaded at the proper addresses, this won't work.

Here's a look at the application's memory layout without LD_PRELOAD:

Above, you see libc (specifically libc's text segment) loaded at 0x2aaf9000.

If we run the program, using LD_PRELOAD to load the nvram library:
export LD_PRELOAD=/mylib/libnvram-faker.so,
the load addresses change.

Here's another look at the program's memory maps, this time with libnvram-faker.so preloaded:

Here, you can see libnvram-faker.so mapped right after the dynamic loader itself, at 0x2aabe000.  This has shifted everything down, and libc is now loaded at 0x2ab3a000.

However, the linker, GNU ld, has an option to specify a preferred address for the text segment.  If we give GCC the option, '-Wl,-Ttext-segment=', it will pass that option on to the linker.

There are a couple of things to note about the address you specify.  First, the address needs to be a multiple of the maximum page size, generally 4K, or 0x1000.  Second, this address is going to be an offset from where the dynamic linker would normally load your library.  Since that is generally right after the linker's load address, you can subtract that address from your desired address, rounding up to the nearest page, to get the offset.

0x2abee000 - 0x2aabe000 = 0x13000

So we tell gcc: -Wl,Ttext-segment=0x13000.

Now with the new version of the library loaded, let's look at the memory map for our application:

We see libnvram-faker now located at higher addresses than all the other libraries: at 0x2abee000.  So specifying a preferred load address for the text segment had the desired effect.  Sadly (for my specific issue) this didn't result in the other libraries being restored to their original base addresses.

This is, of course, if your linker knows about the Ttext-segment linker script.  I found that some older versions of GNU ld did not.  You may be constrained to an older toolchain in order to be compatible with your target system.  To quote the (in)famous Cormen algorithms text: "this problem is left as an exercise for the reader."