Skip to content

When free() Meets the Stack – A Hidden Vulnerability in Mongoose


Overview: How a Simple Bug Crashed the Server

In this article, I walk you through a full reverse engineering journey where I discovered a stack memory corruption vulnerability in a closed-source Windows-based HTTP server (powered by Mongoose). it’s affected versions ( < 7.14) This wasn’t a simple null pointer bug. The crash required stress testing, memory tracing, disassembly analysis, and dynamic debugging to uncover.

Ultimately, I confirmed a classic C programming error: calling free() on stack memory.

This write-up is not just a technical report — it documents the mindset, the step-by-step techniques, and the lessons learned from debugging a real-world crash that didn’t always reproduce.


Step 1: Stress Testing the Target Server


I began by launching a stress test using a custom Python script that fires 1,0000 HTTP POST requests concurrently through multiple threads.

import requests
import threading
import time

URL = “http://192.168.166.131:8000”
THREAD_COUNT = 100
REQUESTS_PER_THREAD = 200
LARGE_BODY = “A” * 10000

def make_requests(thread_id):
for i in range(REQUESTS_PER_THREAD):
try:
r = requests.post(URL, data=LARGE_BODY, timeout=1)
print(f”[Thread {thread_id}] Request {i+1}: {r.status_code}”)
except requests.exceptions.RequestException as e:
print(f”[Thread {thread_id}] Request {i+1} failed: {e}”)

threads = []
start_time = time.time()
for i in range(THREAD_COUNT):
t = threading.Thread(target=make_requests, args=(i,))
t.start()
threads.append(t)

for t in threads:
t.join()

print(f”Completed in {time.time() – start_time:.2f} seconds”)


What I Observed:

  • Sometimes all requests succeeded.
  • Other times, the server silently stopped responding — no console crash.
  • A quick nmap scan revealed that port 8000 was closed.

Yet the process (mongoose.exe) was still running. This suggested a soft crash — not a complete process termination, but something broke the internal socket handling logic.


Step 2: Revival Clue – Hitting Enter

I noticed something odd. When I pressed Enter inside the Windows console where the server was running, it suddenly came back to life.

  • nmap showed the port reopened
  • Browser could connect again

This was a huge clue.

This meant the main loop wasn’t dead — it was likely blocked or corrupted by an error deeper inside.


Step 3: Binary Analysis with PE-bear and IDA Free


I loaded the binary into PE-bear and confirmed that free() is imported via MSVCRT.dll.

1- MSVCRT.dll Import Table Showing free()
Used to confirm that dynamic memory deallocation is performed by the binary.

Then, I loaded the binary into IDA Free and searched for cross-references to free().

2- Call ds:free on Stack Variable
IDA view confirming that the function is attempting to free the value stored in [ebp+Block], which we later confirm is a stack buffer.

3- Cross-References to free()
Used to identify all the locations in the binary where free() is called, helping isolate the vulnerable function.


Step 4: Tracing the Vulnerable Call Chain


This screenshot shows a different cross-reference to free(), specifically in sub_41090B+1E7, revealing the same risky behavior — free() is called on [ebp+Str].


Vulnerable Routine in sub_41090B+1E7 Another view of the free() call, this time in a different function. You can clearly see [ebp+Str] being passed to free() — and the function returning without validating heap allocation. This screenshot gives us another critical confirmation.


Deeper Inside sub_41090B You can clearly see that [ebp+var_D3C] is passed to free() just like [ebp+Str] earlier. This confirms the same pointer is being freed later, without tracking if it was already freed — suggesting a double-free possibility or invalid free.


Step 5: Stack Pointer Being Stored

Here, we clearly see this line: mov [ebp+var_D3C], eax

This is before the call to free(), meaning the pointer that will later be freed is first stored in [ebp+var_D3C]. Later, this variable is reused without being nullified or validated, leading to a use-after-return or invalid memory deallocation.


Storing the Pointer in var_D3C This instruction is the source of the vulnerability, where memory management tracking fails.


Step 6: Origin of the Bug

This screenshot shows the use of: call ds:sscanf

It confirms that user-controlled data is parsed from the HTTP request (likely a path like <!--#include virtual="...">). It uses the sscanf pattern to parse the request into a memory location ([ebp+Buffer]), which ends up being tracked via the pointer stored in var_D3C and later passed to free().


sscanf() Usage and Buffer Initialization This is the origin point of the user-controlled memory that causes the crash downstream after an unsafe free().


Why It Didn’t Crash Immediately

This bug didn’t always cause a crash on the first test. Why?

It’s because the result of free() on a stack address is undefined behavior. Sometimes it works. Sometimes it silently corrupts memory. Sometimes the heap manager doesn’t detect it… until it’s too late. On the 7th test run, it finally caused a state that the system couldn’t recover from. The port was closed, connections were dropped, and the internal socket state was poisoned.


Root Cause Summary

  • char buffer[256]; created on stack
  • Function returns &buffer
  • Caller stores pointer and later calls free()
  • Invalid memory deallocation corrupts heap or thread state

This is: CWE-590: Free of Invalid Pointer Not on Heap


Impact

This vulnerability could be exploited in the following ways:

  • Remote DoS via crafted HTTP flood
  • Potential heap grooming for advanced exploitation
  • Intermittent server hangs that evade detection

For a production HTTP server, this is a high-severity memory safety issue.


Final Thoughts

This vulnerability took a few hours to isolate but was extremely satisfying to unravel. The process combined binary reverse engineering, dynamic analysis, debugging, and a real-world test script to prove the impact.

Crashes that occur randomly are usually the ones worth investigating.

Memory safety is not about whether the code runs — it’s about whether it breaks under stress.

If you’re a developer: never return stack pointers.
If you’re a red teamer: always test edge cases under load.

Vendor Acknowledgement

The vendor was reported with the bug and fix it in version (7.15 >).

You can find the Python script here.

https://packetstorm.news/files/id/200820

Published inUncategorized
Hacking is to Know the Unknown - & Break Boundaries Guided by Curiosity