ROP chains

ROP chains

TL;DR: Stitch short instruction sequences ending in ret (gadgets) from already-mapped executable modules into a chain that performs an arbitrary computation — the standard primitive for defeating DEP and reaching VirtualProtect-style APIs.

What it is

Return-orientated programming, formalised by Shacham (CCS 2007), turns a stack-write primitive into Turing-complete execution without injecting any new code. Each “gadget” is a small sequence of legitimate instructions ending in ret; by stacking gadget addresses interleaved with data values on the controlled stack, every ret pops the next gadget and resumes execution. Combined with API calls, a ROP chain typically marks an attacker-controlled buffer executable and pivots to shellcode (see dep-bypass).

Preconditions / where it applies

  • Stack-write primitive: saved-RIP overwrite, SEH chain hijack, structured pivot
  • Non-PIE module or a leak so gadget addresses are known (see aslr-bypass)
  • DEP enforced (otherwise just jump to shellcode); CET shadow stack (cet-shadow-stack) absent — CET breaks classic ret chains

Technique

Workflow:

  1. Find gadgets. ROPgadget, rp++, mona, or pwntools’ ROP module dump every ret-ended sequence.
    1
    2
    3
    
    ROPgadget --binary target.dll --only "pop|ret"
    rp++ --file=target.exe --rop=4 --unique
    !mona rop -m *.dll -cp nonull -n
    
  2. Plan the API call. x86 stdcall pushes args right-to-left then calls; x86_64 fastcall puts args in rcx/rdx/r8/r9 then call. Pick VirtualProtect, VirtualAlloc, or WriteProcessMemory.
  3. Assemble pad → arg-setting gadgets → API → return-to-shellcode. Use writable BSS/.data for lpflOldProtect.
  4. Bad-character handling. Eliminate null bytes, newlines, etc. via gadget swaps or encoders — see bad-character-handling.

Skeleton (x86, calling VirtualProtect):

1
2
3
4
5
6
7
8
[ pop_eax_ret      ]   ; eax = junk (placeholder)
[ &VirtualProtect  ]   ; pop_eax_ret target
[ ...                  ; set ebx, ecx, edx, esi, edi to args via pop_X_ret gadgets
[ &jmp_esp / shellcode_addr ] ; return address landing in shellcode
[ shellcode_addr   ]   ; lpAddress
[ 0x1000           ]   ; dwSize
[ 0x40             ]   ; PAGE_EXECUTE_READWRITE
[ &writable_dword  ]   ; lpflOldProtect

Advanced patterns:

  • Stack pivotxchg eax, esp ; ret or mov esp, ebp ; ret to move the chain into a sprayed heap buffer when stack space is tight
  • JOP/COP — when shadow stack is on, pivot to jump/call-oriented gadgets that survive backward-edge CFI
  • SROP — Linux analogue using sigreturn (see srop)

Detection and defence

  • DEP AlwaysOn, ASLR MandatoryASLR, CFG (control-flow-guard), XFG (xfg), and CET shadow stack (cet-shadow-stack) each break a layer
  • EAF/IAF (Export Address Filtering) trips on early gadgets that walk the PEB to resolve APIs
  • EDR detection: VirtualProtect/VirtualAlloc callers from non-image memory regions, especially with PAGE_EXECUTE_READWRITE
  • Build with /GUARD:CF /CETCOMPAT /HIGHENTROPYVA /DYNAMICBASE and audit non-compliant DLLs with mona modules

References

Related: dep-bypass, aslr-bypass, mona-py, cet-shadow-stack