March 20, 2015

Resolving the Base Pointer of the Linux Program Interpreter with Shellcode

Oftentimes an exploit developer desires the ability to call functions from shared libraries with their shellcode and a guarantee of proper function resolution. The way that the Linux Executable and Linkable Format (ELF) Application Binary Interface (ABI) works, a program interpreter (nearly always ld-linux) is specified in the binary's .INTERP section. When a program is executed, the program interpreter loads all of the associated shared libraries and then populates the program's import table, called the Global Offset Table (GOT), with pointers to the appropriate functions.

Because of Address Space Layout Randomization (ASLR), the pointers to these shared libraries and functions are different each execution of the program. This can make calling functions not referenced in an application's GOT from shellcode more difficult. By isolating the address space of the program interpreter, it is possible to parse its binary format and re-use its code to load shared objects and call functions not referenced by an application's import table. The code examples here cover locating the memory space of ld-linux; however they do not cover the code integration aspect. That will be covered either here at a later date, in our planned shellcode workshop, or both.

The GOT can be populated one of two ways, depending on the mode of relocate read-only (RELRO) selected at compile-time:

  1. Partial RELRO contexts populate the GOT as the functions are needed.
  2. Full RELRO contexts populate the entire GOT before jumping to the application's entry point.

Helpful VMA's

Before diving into resolution of the program interpreter, the reader should note the following Virtual Memory Addresses (VMAs) which are present 100% of the time for their ABI/Architecture.

ELF32

  • 0x8048000 is the base pointer of the currently executing binary, 100% of the time.
  • 0x80480bc contains a 4-byte VMA that is a pointer to the currently executing binary's DYNAMIC section.

ELF64

  • 0x400000 is the base pointer of the currently executing binary, 100% of the time.
  • 0x400130 contains a 4-byte VMA that is a pointer to the currently executing binary's DYNAMIC section.

Partial RELRO

The partial RELRO context is default on most variants of GCC on Linux; and does not clean up all of the references to ld-linux at runtime. In fact, in partial RELRO contexts, the program interpreter leaves behind a small gem, a pointer to _dl_runtime_resolve. This pointer is left at the third index of the Procedure Linkage Table (PLT) so that functions can be resolved as they are called. Simply migrating backwards in memory space from _dl_runtime_resolve until ELF magic (0x7f 0x45 0x4c 0x46) is reached can isolate the base pointer of ld-linux on partial RELRO contexts.

The example code below locates .GOT.PLT[2] by traversing through the dynamic section. Starting at the dynamic section, the next occurance of a pointer to the dynamic section is always .GOT.PLT[0]. This code places the ld-linux base pointer into the %rcx register.

# ABI="ELF64"
.section .text
.global main
main:             
  # read the dynamic header
  push $0x400130ff
  pop %rbx
  shr $0x08, %ebx          #  %rbx  = 0x400130
                           # (%rbx) = location of dynamic section

skip_to_dynamic:  
  # this is a vma, so 32 bit reg is fine.
  mov (%rbx), %esi         # put dynamic section location into %rsi

fix_dflag:        
  cld                      # make the dflag go forwards...

find_got_plt:     
  # Search past the dynamic section until it finds
  # another pointer to the dynamic section.  This
  # will be the beginning of .got.plt
  lodsl                    # lodsl for magically short searching
  cmpl %eax, (%rbx)        # save a couple bytes because its a vma.
  jne find_got_plt

found_resolver:   
  mov 0xc(%rsi), %rcx      # %rcx = qword pointer to resolver
                           # usually _dl_runtime_resolve (.got.plt[2])

find_base:        
  xor %cl, %cl             # it'll be an address ending in 00
                           # if this doesn't happen, it may also find false bases
                           
  cmpl $0x464c457f, (%rcx) # check for ELF magic
  loopne find_base         # loopne automatically does a dec %rcx

libdl_base_found: 
  # Make %rcx a direct pointer to libdl_base after that,
  # the loop decrements it one too many times.
  inc %rcx
compile/assemble: gcc partial-relro.s -o partial_relro

There is still a problem with this example, though. This code can't isolate the base pointer in a full RELRO environment. In a full RELRO environment, the PLT is cleaned up a bit and pointers to the resolver are removed before jumping to the entry point. After a lot of observations at runtime using gdb, and inspecting executable files on-disk using the readelf utility, a solution was devised.

Full RELRO

After looking at many binaries, it was discovered that every dynamic section contains a pointer to the PLT. In every single binary observed, the pointer just before this points to a DEBUG section (blank on disk). In gdb this section is recognized as r_debug. The fifth pointer in r_debug (offset 0x10 in 32 bit and offset 0x20 in 64 bit) is a pointer directly to the ld-linux base. So the process is as follows:

  1. Traverse to the dynamic section.
  2. Find got.plt by looping fowards in memory until a pointer to _DYNAMIC_ is found.
  3. Loop backwards in memory from got.plt until a pointer to got.plt is found.
  4. Grab the pointer just before the pointer to .got.plt, this is a pointer to DEBUG.
  5. Put the fifth pointer in DEBUG into a register -- this is the base pointer to ld-linux.

The ELF32 proof-of-concept code below illustrates this process effectively, placing the base pointer into the ebx register. Note it is compiled with gcc's "-s" option, which removes debug information from the on-disk binary. The ABI populates the DEBUG segment this code uses at runtime, which is why this code works.

# ABI="ELF32"
.section .text
.global main
main:
  mov 0x80480bc, %ebx   # move pointer to _DYNAMIC_ into ebx
  push %ebx
  pop %esi              # copy ptr to esi


find_got:               # loop until the GOT is found
  lodsl
  cmp %ebx, %eax
  jne find_got


find_debug:             # loop backwards until a pointer
  xchg %ebx, %esi       # to GOT is found
  sub $0x4, %ebx

find_debug_loop:
  lodsl
  cmp %ebx, %eax
  jne find_debug_loop


found_debug:
  mov -0xc(%esi), %eax  # Grab the entry in the symbol table 
                        # before GOT (r_debug)

get_interp_base:        # move the fifth pointer in DEBUG
  mov 0x10(%eax), %ebx  # into ebx - ptr to ld-linux base
compile/assemble: gcc -fstack-protector-all -s -fPIE -Wl,-z,relro,-z,now relro-ld-basefinder.s -o relro-ld-basefinder

This technique works on ELF64 as well as ELF32. It also appears to work regardless of whether the above compile options are present, and thusly is a reliable method for determining the address of ld-linux's ELF magic. This code has been used to start a new abicode project on our github.

Update - ELF64 version of the above code for full relro added to abicode. These "full relro" versions work in partial relro and non-relro environments too.

Related Links & Resources

No comments:

Post a Comment

Note: Please keep comments academic in nature.