OSED-style custom exploit walkthrough (Windows user-mode)

OSED-style custom exploit walkthrough (Windows user-mode)

TL;DR: A composite, exam-shape walkthrough of building a Windows user-mode exploit in the style of OSED (EXP-301): pick a target with a known overflow, classify the crash, find the offset with mona-py, enumerate bad characters, defeat DEP via a rop-chains chain (usually VirtualProtect), and land a clean stager into custom-windows-shellcode-writing. This note assumes prior reading of stack-buffer-overflow, stack-bof-walkthrough-end-to-end, seh-overwrite, and bad-character-handling. Nothing here reproduces course material - it is reconstructed from the public EXP-301 syllabus, vendor advisories, and community write-ups.

Why it matters

OSED is the only OffSec cert that forces you to write a working Windows user-mode exploit under time pressure, with no Metasploit, no pwntools, and no copy-paste public PoC. Even if you never sit the exam, the workflow it drills - fuzz, crash, classify, offset, bad-chars, ROP, shellcode - is the same loop you run against any closed-source Windows service or thick client during a real engagement. The pain points (ASLR-less modules, register clobbering, bad-character whack-a-mole) show up identically in CVE write-ups for IP cameras, VPN clients, and SCADA HMIs.

This note is the index card you want next to your debugger when something stops working at 2am. For the bigger pentest context see osep-roadmap and oswe-roadmap; the kernel-side analogue lives in osee-roadmap.

Classes, patterns, process

Bug classes in scope

OSED-shape exploitation usually pulls from three primitive families:

  • Linear stack overflow with direct EIP control - the friendliest case, covered end-to-end in stack-bof-walkthrough-end-to-end.
  • SEH-based overflow - return address is unreachable but the SEH chain is; see seh-overwrite and pair it with safeseh-bypass when the target binary opts in.
  • Format string writes are uncommon on Windows clients but conceptually similar to format-string-bugs; OSED’s flavour is the stack overflow path.

Egghunters (egghunters) appear when your payload space is too small for the full stager but you can drop the egg elsewhere in the process.

Mitigation landscape

Modern Windows user-mode targets ship with some mix of:

  • DEP / NX - on by default; defeated with rop-chains calling VirtualProtect, VirtualAlloc, or NtSetInformationProcess (DEP policy flip on older builds).
  • ASLR - per-module opt-in via /DYNAMICBASE. The exam-shape pattern hunts for a non-ASLR module loaded into the target. Background: aslr-bypass.
  • SafeSEH / SEHOP - register-based exception handler validation; bypass paths in safeseh-bypass.
  • Stack cookies / GS - cookies guard return addresses but not SEH; if /GS is on and the cookie is between you and EIP, pivot to an SEH overwrite instead.
  • CFG / XFG - rare on the dusty third-party software typical of exam-shape targets, but worth checking with dumpbin /headers /loadconfig.

The seven-step loop

1
2
3
4
5
6
7
1. Acquire target + repro crash
2. Classify crash (EIP / SEH / read AV / write-what-where)
3. Find offset to control (mona pattern_create + pattern_offset)
4. Enumerate bad characters against the actual sink
5. Pick a pivot / return address (jmp esp, pop-pop-ret, etc.)
6. Build ROP to make the buffer executable
7. Land shellcode, debug, iterate

Every step has its own failure mode. Most stuck-points come from skipping step 4 or mis-scoping the bad-char set in step 5.

Defensive baseline

If you are on the defender side staring at a crash dump that looks like one of these:

  • Confirm /DYNAMICBASE, /NXCOMPAT, /GS, /SAFESEH, /guard:cf on every loaded module - one non-ASLR DLL is enough to unlock the chain. Check with dumpbin /headers or Get-PESecurity.
  • Force-ASLR via MitigationOptions / Exploit Guard so opt-out modules still get randomized.
  • Watch for VirtualProtect calls that flip RX on a writable stack region - that is the ROP tell. EDR vendors expose this as “stack pivot” or “RWX from stack” telemetry; see detection-engineering-pyramid-of-pain for where this sits on the cost curve.
  • For the SEH variant, SEHOP at the OS level kills most public chains regardless of per-binary /SAFESEH.

Workflow to study

Step 1 - target acquisition and crash repro

Pull an old vulnerable build (vendor archive, internet archive, or waybackd installer). Mirror the network protocol or file format with a small Python script. The exam-shape pattern is a TCP service that crashes on an oversized field; write the smallest possible client that triggers it.

Attach Immunity or mona-py-loaded WinDbg before sending the trigger so you catch the first-chance exception, not just the second-chance unhandled one. With x64dbg use the SEH view; with WinDbg use !exchain after the crash.

Step 2 - classify the crash

Look at EIP, ESP, and the SEH chain at crash time:

  • EIP shows your AAAA... bytes - direct overwrite path.
  • EIP is sane but the SEH record at fs:[0] is yours - SEH path.
  • Access violation reading at a controlled address with intact EIP - read primitive, usually leads to info leak before exploitation.

Note the state of registers: which ones point into your buffer, which are clobbered, and how much room exists after the controlled pointer. This dictates whether you need an egghunter (egghunters) or can inline the stager.

Step 3 - offset discovery

Generate a cyclic pattern with Mona:

1
!mona pc 5000

Send it, catch the crash, then for the direct case:

1
!mona po <EIP value>

For SEH, find the offset to the nSEH / SEH pair instead. Always verify the offset by sending A * offset + B * 4 + C * (rest) and confirming EIP == 0x42424242. Skipping the verification step is the single most common reason exam attempts stall.

Step 4 - bad-character enumeration

Generate the full byte table:

1
!mona bytearray -b "\x00"

Send pattern + bytearray and compare the in-memory buffer to the on-disk file with:

1
!mona compare -f bytearray.bin -a <address>

Repeat - removing each new bad byte and regenerating - until the comparison is clean. See bad-character-handling for the longer discussion; the iceberg is that protocol-layer encoders (length prefixes, XOR, base64) silently rewrite bytes before the sink ever sees them.

Step 5 - return address selection

You want a module that is:

  • Non-ASLR (/DYNAMICBASE:NO in the PE header).
  • Non-SafeSEH if you are riding the SEH path.
  • Loaded into the target process at exploitation time.

!mona modules lists the candidates. Then !mona jmp -r esp -cpb "\x00\x0a\x0d" (or seh for the SEH path) gives addresses that survive your bad-char set. Pick the lowest-noise one - a single non-ASLR DLL is enough to unlock the chain even on a fully mitigated host. Background mechanics: aslr-bypass.

Step 6 - ROP to executable memory

DEP means the stack is RW, not RWX. The standard play is to call VirtualProtect on the stack buffer, flipping it to RWX, then ret into the shellcode that follows. Variants:

  • VirtualAlloc + copy - useful when the existing buffer is fragmented.
  • NtSetInformationProcess with ProcessExecuteFlags - turns DEP off process-wide on older Windows builds. Loud and obvious in telemetry.
  • WriteProcessMemory against self - sometimes the only way when bad chars exclude VirtualProtect’s argument bytes.

Auto-generate a starting chain:

1
!mona rop -m "vuln.dll,helper.dll" -cpb "\x00\x0a\x0d"

Mona writes rop_chains.txt, rop.txt, and stackpivot.txt. The auto-chain is not a finished exploit - it is a parts bin. You will typically:

  1. Take the Mona skeleton.
  2. Replace gadgets that touch bad characters.
  3. Re-order pushes so register clobbering does not destroy a value you set two gadgets ago.
  4. Patch in the correct lpAddress (your shellcode location) and dwSize.

Deeper mechanics: rop-chains and dep-bypass.

Step 7 - shellcode and stager

If you have enough room, drop a full reverse shell payload. If you do not, use a tiny stager that reads the rest from the socket or a known scratch buffer. The OSED-shape exam explicitly forbids msfvenom, so this is where custom-windows-shellcode-writing earns its keep - a hand-rolled WSAStartup -> WSASocket -> connect -> CreateProcess chain with cleanly resolved kernel32 and ws2_32 exports via PEB walking.

Re-run the bad-char filter against the final shellcode, not just against A * N. Encoders introduce their own constraints.

Common stuck-points

  • No jmp esp survives ASLR - find one non-ASLR module, or pivot to SEH which gives you a stable pop-pop-ret.
  • Register clobbering mid-chain - track every register’s state across gadgets; tools like ropper --semantic help but for OSED-scale chains a paper trace is faster.
  • VirtualProtect returns success but execution still faults - you probably flipped the wrong page. Recompute lpAddress to be page-aligned and dwSize large enough to cover the shellcode.
  • Bad char appears only after the encoder runs - run the byte-array test on the post-encoder payload, not the source.
  • Exploit works in debugger, dies standalone - heap and stack layout differ when a debugger is attached. Add NOP slack and re-verify offsets.
  • SafeSEH-protected module sneaks in - even one SafeSEH module in your pivot path can block the chain; check with !mona modules carefully and re-pick.
  • EDR kills the shellcode - separate concern from exploitation; see edr-bypass-at-exploitation-time and amsi-bypass.

Tooling cheat sheet

  • Immunity Debugger + Mona - canonical OSED setup; mona-py covers commands.
  • WinDbg + pykd / mona for WinDbg - works for both user and kernel; pairs with windows-kernel-debugging-deep when you graduate to osee-roadmap.
  • x64dbg - modern UI, scriptable; lacks Mona feature parity but ERC.Xdbg plugin fills the gap.
  • Ghidra / IDA / Binary Ninja - for static gadget hunting and sink analysis.
  • boofuzz / mutiny - to find your own crashes instead of relying on community PoCs.

References

  • OffSec EXP-301 syllabus (public): https://www.offsec.com/courses/exp-301/
  • Corelan team - “Exploit writing tutorial” series (parts 1-11): https://www.corelan.be/index.php/articles/
  • Mona.py project: https://github.com/corelan/mona
  • Microsoft - Data Execution Prevention reference: https://learn.microsoft.com/en-us/windows/win32/memory/data-execution-prevention
  • FuzzySecurity - Windows Exploit Development tutorials: https://www.fuzzysecurity.com/tutorials.html
  • MSRC - Exploit mitigation improvements catalogue: https://msrc.microsoft.com/blog/