Module 3: ROP Fundamentals
Turning legitimate code fragments into an arbitrary execution engine.
Why ROP Matters for Stack Spoofing
SilentMoonwalk doesn't just fake a call stack — it uses Return-Oriented Programming (ROP) to actually execute a chain of legitimate code fragments that set up the spoofed stack, make the target API call, and then restore everything. Understanding ROP is essential because the entire spoofing mechanism is driven by a carefully constructed ROP chain.
What Is Return-Oriented Programming?
ROP is a code-reuse technique. Instead of injecting new executable code, you chain together small sequences of instructions that already exist in loaded modules (DLLs). Each sequence ends with a RET instruction, which pops the next address from the stack and jumps to it. By controlling the stack, you control execution flow.
Each of these small instruction sequences is called a gadget:
x86-64 ASM; Example gadgets found in ntdll.dll:
; Gadget 1: Pop a value into RCX, then return
pop rcx
ret
; Gadget 2: Pop a value into RDX, then return
pop rdx
ret
; Gadget 3: Move a value and return
mov rax, rcx
ret
; Gadget 4: Add to RSP and return (stack pivot)
add rsp, 0x28
ret
How a ROP Chain Works
A ROP chain is a sequence of addresses laid out on the stack. When the first gadget's RET executes, it pops the next address from the stack and jumps there. That gadget executes, hits its own RET, pops the next address, and so on:
ROP Chain Execution Flow
C++// Conceptual: building a ROP chain to call VirtualProtect(addr, size, RWX, &old)
// Each entry on the stack is either a gadget address or a data value
struct ROPChain {
PVOID gadget_pop_rcx; // pop rcx; ret --> loads 1st arg
PVOID arg1_address; // RCX = address to protect
PVOID gadget_pop_rdx; // pop rdx; ret --> loads 2nd arg
PVOID arg2_size; // RDX = size
PVOID gadget_pop_r8; // pop r8; ret --> loads 3rd arg
PVOID arg3_protect; // R8 = PAGE_EXECUTE_READWRITE
PVOID gadget_pop_r9; // pop r9; ret --> loads 4th arg
PVOID arg4_old_protect; // R9 = pointer to receive old protection
PVOID target_function; // Address of VirtualProtect
};
Gadget Types Relevant to SilentMoonwalk
SilentMoonwalk uses specific gadget patterns, not for traditional exploit purposes, but to manipulate the stack during the spoofing process:
| Gadget Pattern | Purpose in SilentMoonwalk | Example Source |
|---|---|---|
JMP [RBX] | Indirect jump to function pointer stored in RBX. Used as a trampoline to call the target API through a register rather than a CALL instruction (avoiding pushing a return address). | ntdll.dll, kernel32.dll |
ADD RSP, N; RET | Stack pivot gadget. Adjusts RSP to skip over synthetic frame data and land on the next gadget address. Critical for frame size matching. | ntdll.dll |
POP RCX; RET | Loads a value from the stack into RCX (first argument register). Used to set up function parameters. | ntdll.dll, kernel32.dll |
POP RDX; RET | Loads second argument. Used in conjunction with POP RCX for multi-parameter calls. | ntdll.dll |
RET (bare) | Simple stack pivot. Pops next address and jumps. Used to chain between frames. | Anywhere |
ROP in Evasion vs ROP in Exploitation
In traditional exploitation, ROP chains are used to bypass DEP (Data Execution Prevention) by chaining existing code to achieve arbitrary code execution. In SilentMoonwalk, the purpose is fundamentally different: ROP is used to manipulate the stack layout so that when an EDR or the OS unwinds the stack, it sees a clean, legitimate-looking call chain. The ROP chain is the mechanism that bridges the gap between the real execution state and the fake stack the EDR sees.
The JMP [RBX] Trampoline
One of the most important gadgets for stack spoofing is JMP [RBX] (or JMP QWORD PTR [RBX]). This instruction jumps to the address stored at the memory location pointed to by RBX. Why is this crucial?
Normally, when you CALL a function, the CPU pushes a return address onto the stack. But we're trying to control exactly what's on the stack. A JMP does NOT push anything — it transfers control without modifying the stack. By using JMP [RBX], SilentMoonwalk can:
- Set up RBX to point to the target function's address
- Pre-arrange the stack with the desired fake return address
- Execute JMP [RBX] to transfer to the target API
- When the API executes RET, it returns to the fake return address
x86-64 ASM; How JMP [RBX] works as a trampoline:
; Setup (done by earlier gadgets in the chain):
; RBX = pointer to memory containing address of NtWaitForSingleObject
; RSP points to a carefully crafted fake return address
; The gadget:
jmp qword ptr [rbx] ; Jumps to NtWaitForSingleObject
; Does NOT push return address
; Stack remains exactly as we arranged it
; When NtWaitForSingleObject hits RET:
; It pops our fake return address from the stack
; Execution continues where WE decided, not where CALL would have set up
Stack Pivoting
A stack pivot is a technique to redirect RSP to a different memory region, effectively changing where the CPU looks for the next return address. In SilentMoonwalk, ADD RSP, N; RET gadgets serve as controlled stack pivots that advance RSP by a precise amount to skip over frame data:
x86-64 ASM; Stack pivot with ADD RSP, 0x38; RET
; Before: RSP = 0x7FF000100
; [RSP+0x00] = (frame data - skipped)
; [RSP+0x08] = (frame data - skipped)
; [RSP+0x10] = (frame data - skipped)
; [RSP+0x18] = (frame data - skipped)
; [RSP+0x20] = (frame data - skipped)
; [RSP+0x28] = (frame data - skipped)
; [RSP+0x30] = (frame data - skipped)
; [RSP+0x38] = address_of_next_gadget <-- RET pops this
add rsp, 0x38 ; RSP becomes 0x7FF000138
ret ; Pops [0x7FF000138] = address_of_next_gadget
; RSP becomes 0x7FF000140
The Key Insight
The ADD RSP, N value in a gadget determines the frame size that this gadget implicitly creates. When RtlVirtualUnwind processes the function containing this gadget, the function's UNWIND_INFO will describe an allocation of size N (via UWOP_ALLOC_SMALL or UWOP_ALLOC_LARGE). SilentMoonwalk finds gadgets where the ADD RSP value matches the function's declared unwind allocation, ensuring the stack walk produces consistent results.
ROP Chain Construction for Evasion
Building a ROP chain for stack spoofing differs from exploitation in several ways:
| Aspect | Exploit ROP | SilentMoonwalk ROP |
|---|---|---|
| Goal | Achieve code execution | Create a fake call stack |
| Gadget source | Any module | Prefer ntdll/kernel32 (always loaded, trusted) |
| Gadget constraints | Minimal side effects | Must have valid RUNTIME_FUNCTION + matching unwind codes |
| Stack layout | Just needs to chain correctly | Must be a valid unwinding chain for RtlVirtualUnwind |
| Execution after | Payload runs | Execution returns cleanly to real code |
Chaining It All Together
In SilentMoonwalk's approach, the ROP chain serves as the execution backbone. The spoofed stack is simultaneously a valid ROP chain (for actual execution) and a valid unwind chain (for EDR inspection). This dual nature is the core innovation — the same stack layout serves two masters:
Dual Purpose Stack
Execution View (ROP)
Unwind View (EDR)
Pop Quiz: ROP Fundamentals
Q1: Why does SilentMoonwalk use JMP [RBX] instead of CALL to invoke the target API?
Q2: What is a "stack pivot" in ROP terminology?
Q3: How does SilentMoonwalk's use of ROP differ from traditional exploit ROP?