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, orNtSetInformationProcess(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
/GSis on and the cookie is between you andEIP, 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:cfon every loaded module - one non-ASLR DLL is enough to unlock the chain. Check withdumpbin /headersorGet-PESecurity. - Force-ASLR via
MitigationOptions/ Exploit Guard so opt-out modules still get randomized. - Watch for
VirtualProtectcalls 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:
EIPshows yourAAAA...bytes - direct overwrite path.EIPis sane but the SEH record atfs:[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:NOin 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.NtSetInformationProcesswithProcessExecuteFlags- turns DEP off process-wide on older Windows builds. Loud and obvious in telemetry.WriteProcessMemoryagainst self - sometimes the only way when bad chars excludeVirtualProtect’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:
- Take the Mona skeleton.
- Replace gadgets that touch bad characters.
- Re-order pushes so register clobbering does not destroy a value you set two gadgets ago.
- Patch in the correct
lpAddress(your shellcode location) anddwSize.
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 espsurvives ASLR - find one non-ASLR module, or pivot to SEH which gives you a stablepop-pop-ret. - Register clobbering mid-chain - track every register’s state across gadgets; tools like
ropper --semantichelp but for OSED-scale chains a paper trace is faster. VirtualProtectreturns success but execution still faults - you probably flipped the wrong page. RecomputelpAddressto be page-aligned anddwSizelarge 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 modulescarefully 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.Xdbgplugin 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.
Related
- stack-buffer-overflow
- stack-bof-walkthrough-end-to-end
- seh-overwrite
- bad-character-handling
- rop-chains
- mona-py
- dep-bypass
- aslr-bypass
- safeseh-bypass
- egghunters
- custom-windows-shellcode-writing
- pe-backdooring-and-code-caves
- oswe-roadmap
- osep-roadmap
- osee-roadmap
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/