Bypassing DEP on Windows with ROP
DEP makes stack shellcode unrunnable, so a Windows ROP chain calls VirtualProtect to mark the shellcode region executable, then jumps to it. Build it with mona, then see ASLR and CFG respond.
This is the second walkthrough in the Windows Exploitation area. The SEH overwrite gave us control of execution. But DEP — Windows' NX — means the shellcode on the stack will not run. As on Linux, the answer is ROP; the Windows idiom is to ROP just long enough to make the shellcode region executable.
Same rules: your own program, disposable Windows VM, WinDbg/x32dbg + mona.py. See the lab rules.
DEP: the shellcode is there, but not executable
With DEP on, the stack and heap are non-executable. You control the instruction pointer (from a stack overflow or the SEH overwrite), and your shellcode is in memory — but jumping to it faults. Rather than inject executable code, reuse existing code to change the shellcode's page protection.
ROP chaingadgets that set up the callVirtualProtect(...)mark the shellcode region RXshellcodenow executable, runs normallyThe VirtualProtect call
VirtualProtect(lpAddress, dwSize, flNewProtect, lpflOldProtect) changes protection on an existing region. The ROP chain must set up these four arguments (on 32-bit, pushed on the stack in order) with flNewProtect = 0x40 (PAGE_EXECUTE_READWRITE), point lpAddress/dwSize at the shellcode region, give lpflOldProtect a writable scratch address, and set the return address to the shellcode.
Doing this by hand means finding gadgets to load registers and write them into the argument slots. In practice mona generates the whole chain from the modules in the process:
!mona rop -m *.dll -cpb '\x00\x0a\x0d' # build a VirtualProtect ROP chain, avoiding bad bytes
It emits a ready chain (and reports which imports it used). The exploit then becomes:
# exploit.py — 32-bit DEP bypass via VirtualProtect (lab target)
import struct
def p(x): return struct.pack("<I", x)
offset = 260
rop = b"".join(p(g) for g in mona_rop_chain) # generated by !mona rop
nops = b"\x90" * 16
payload = b"A" * offset # overflow to the saved return address
payload += rop # ROP: VirtualProtect(shellcode, len, 0x40, scratch)
payload += nops + shellcode # made executable by the chain, then run
open("poc.txt","wb").write(payload)
The chain calls VirtualProtect on the region holding the NOP sled + shellcode, marks it PAGE_EXECUTE_READWRITE, and returns into it. DEP is satisfied because the page is now legitimately executable.
Stack alignment and bad bytes
Two practical Windows details: chosen gadget and argument bytes must avoid the input's bad bytes (\x00, \x0a, \x0d for string copies), and the chain must keep the stack consistent — mona handles both, but understanding them is what lets you debug a chain that faults mid-way.
Turn the mitigations back on
| Mitigation | Effect on the DEP bypass |
|---|---|
| DEP | The reason you need ROP at all — injected shellcode won't run |
| ASLR | Randomizes gadget/module addresses; you need a non-ASLR module or a leak |
| CFG | Constrains indirect calls to valid targets — see the mitigations guide |
| Hardware CET | A shadow stack breaks the ret-driven chain |
A modern, fully hardened target requires defeating ASLR (a leak) and contending with CFG/CET — DEP bypass alone is necessary but no longer sufficient.
What this teaches a defender
- DEP is necessary but not sufficient — exactly as on Linux. It ended shellcode injection and attackers answered with a VirtualProtect ROP chain. Enable it (it is default), but do not treat it as complete on its own.
- ASLR is what makes DEP bite. The single most common enabler of a Windows DEP bypass is a module in the process compiled without ASLR (
/DYNAMICBASE). Audit every loaded DLL, including third-party ones, for ASLR; one non-ASLR module hands the attacker fixed gadgets. - Move toward CFG and CET. Compile with
/guard:cfand run on CET-capable hardware with/CETCOMPAT; these target the ROP itself, covered next.
Key takeaways
- DEP (Windows NX) stops stack shellcode, so a ROP chain calls VirtualProtect to make the shellcode region executable, then jumps to it.
- Set
flNewProtect = 0x40(PAGE_EXECUTE_READWRITE) and point the call at your shellcode region;mona ropbuilds the chain. - Avoid bad bytes and keep the stack consistent — the usual practical hurdles.
- ASLR (needs a leak or non-ASLR module), CFG and CET are the mitigations that stop this on a modern target.
Next: Windows flow-integrity mitigations — SafeSEH, SEHOP, CFG and CET, and how they close the techniques in this area.