Skip to content

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.

Published on 3 min read

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.

A Windows DEP-bypass ROP chain flips the shellcode page to executable
ROP chaingadgets that set up the call
sets args
VirtualProtect(...)mark the shellcode region RX
returns into
shellcodenow executable, runs normally
codelibrarydata

The 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

MitigationEffect on the DEP bypass
DEPThe reason you need ROP at all — injected shellcode won't run
ASLRRandomizes gadget/module addresses; you need a non-ASLR module or a leak
CFGConstrains indirect calls to valid targets — see the mitigations guide
Hardware CETA 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:cf and 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 rop builds 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.

Related guides

0xa000 · Windows Exploitation

SEH Overwrites: Hijacking Windows Exceptions

Windows keeps a linked list of exception handlers on the stack. Overflow into one, point it at a pop-pop-ret, trigger a fault, and control is yours — the technique Windows made famous.

0x8000 · ARM64 Exploitation

ROP on ARM64: Gadgets and the Link Register

AArch64 gadgets end in ret, which branches to x30 — so the chain is threaded through the link register with ldp gadgets. Build a system("/bin/sh") chain, then watch PAC and BTI break it.

0x7000 · Exploitation Techniques

Seccomp Sandboxes and the open-read-write Chain

A seccomp policy that bans execve takes the shell off the table, so attackers switch goals: open the flag, read it, write it back. Build the ORW chain, and see what a tighter policy stops.