Difficulty: Beginner

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

RSP ↓ (stack grows down, execution goes up)
[RSP+0x00] → addr of "pop rcx; ret" ← 1st gadget
[RSP+0x08] → 0x0000000000001000 ← value popped into RCX
[RSP+0x10] → addr of "pop rdx; ret" ← 2nd gadget
[RSP+0x18] → 0x0000000000003000 ← value popped into RDX
[RSP+0x20] → addr of "pop r8; ret" ← 3rd gadget
[RSP+0x28] → 0x0000000000000040 ← value popped into R8
[RSP+0x30] → addr of target function ← final call
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 PatternPurpose in SilentMoonwalkExample 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; RETStack 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; RETLoads a value from the stack into RCX (first argument register). Used to set up function parameters.ntdll.dll, kernel32.dll
POP RDX; RETLoads 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:

  1. Set up RBX to point to the target function's address
  2. Pre-arrange the stack with the desired fake return address
  3. Execute JMP [RBX] to transfer to the target API
  4. 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:

AspectExploit ROPSilentMoonwalk ROP
GoalAchieve code executionCreate a fake call stack
Gadget sourceAny modulePrefer ntdll/kernel32 (always loaded, trusted)
Gadget constraintsMinimal side effectsMust have valid RUNTIME_FUNCTION + matching unwind codes
Stack layoutJust needs to chain correctlyMust be a valid unwinding chain for RtlVirtualUnwind
Execution afterPayload runsExecution 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)

Gadget 1: Setup registers
Gadget 2: Stack pivot (ADD RSP)
Gadget 3: JMP [RBX] trampoline
Target API executes
Cleanup gadgets

Unwind View (EDR)

Frame: ntdll!SomeFunction+0x1A
Frame: kernel32!AnotherFunc+0x33
Frame: kernelbase!SomethingElse+0x7
Frame: kernel32!BaseThreadInitThunk
Frame: ntdll!RtlUserThreadStart

Pop Quiz: ROP Fundamentals

Q1: Why does SilentMoonwalk use JMP [RBX] instead of CALL to invoke the target API?

CALL pushes the return address onto the stack automatically. JMP transfers control without modifying RSP, so SilentMoonwalk can pre-place a fake return address on the stack before jumping to the API. When the API executes RET, it returns to the controlled fake address.

Q2: What is a "stack pivot" in ROP terminology?

A stack pivot changes where RSP points, effectively moving the "stack" to attacker-controlled memory. In SilentMoonwalk, ADD RSP, N gadgets advance RSP by precise amounts to traverse the synthetic frame layout.

Q3: How does SilentMoonwalk's use of ROP differ from traditional exploit ROP?

In exploit ROP, gadgets just need to chain correctly for execution. In SilentMoonwalk, each gadget must be inside a function with valid unwind metadata so that when RtlVirtualUnwind processes the stack, the frame sizes and register saves match the actual stack layout. The chain must be valid for both execution AND unwinding.