中文文档 | English
Stack unwinding is often used to inspect caller information for sensitive APIs:
-
Detecting fileless injection (remote Shellcode calls):
After analyzing a cheat sample for a certain domestic extraction shooter game, I found that the cheat used manual mapping to inject a DLL into the game process.
By calling Unreal Engine (UE) related interfaces, it bypassed Object pointer encryption and coordinate encryption, and also leveraged the engine’s ray-casting functionality (
LineTraceSingle) for cover detection and similar logic, enabling various features that severely disrupted game balance.By performing stack unwinding on certain sensitive APIs, any caller outside the whitelist could be identified, and the abnormal call information could then be logged and reported to the server.
The analysis of this cheat sample is basically complete. If I have time later, I may publish it. If you are interested, feel free to follow.
-
Detecting moduleless drivers:
In today’s game security and kernel-level anti-cheat confrontation, in order to hide driver characteristics, this kind of scheme is usually divided into two layers. The outer driver acts only as a MapLoader (signed with WHQL or at least not blacklisted by the anti-cheat), responsible for PE mapping, relocation fixing, import table reconstruction, and so on. The inner functional module resides and executes in kernel mode (R0) in the form of shellcode or a moduleless image. In this case, common kernel enumeration / ARK tools cannot find the driver in the loaded driver list.
At this point, stack unwinding can be used again: once the caller is traced back to an address that does not fall within any legitimate module range, the abnormal call chain can be logged and reported to the server.
The effect is shown below:
An assembly function is needed here as a wrapper and as a general-purpose function invoker. Its responsibilities are:
- Encrypting and decrypting the return address on the stack at specific moments (using a custom or random key)
- Constructing a new call stack and invoking the real target function
The assembly code is as follows:
Asm_SpoofWrapper PROC
mov r11, g_XorKey
xor [rsp], r11
push rsi
push rdi
sub rsp, 300h
lea rsi, [rsp + 340h]
lea rdi, [rsp + 20h]
mov r10, rcx
mov ecx, 40h
rep movsq
mov rcx, r10
call qword ptr [rsp + 338h]
add rsp, 300h
pop rdi
pop rsi
mov r11, g_XorKey
xor [rsp], r11
ret
Asm_SpoofWrapper ENDP- There is still a problem at this point: the
callinstruction insideAsm_SpoofWrapperwill still push a return address belonging to the current driver onto the stack. So the idea is to move the wrapper into a legitimate module instead. The wrapper bytes are shown below. - However, moving the wrapper into a legitimate module introduces another issue: the key used for encryption/decryption is obtained via RIP-relative addressing, so its relative offset must be fixed up dynamically.
UCHAR SpoofShellCode[] =
{
// 1. Dynamically obtain the key and encrypt the return address
0x4C, 0x8B, 0x1D, 0x00, 0x00, 0x00, 0x00, // mov r11, XorKey
0x4C, 0x31, 0x1C, 0x24, // xor [rsp+0], r11
// 2. Save non-volatile registers
0x56, // push rsi
0x57, // push rdi
// 3. Allocate 0x300 bytes of stack space
0x48, 0x81, 0xEC, 0x00, 0x03, 0x00, 0x00, // sub rsp, 300h
// 4. Set source and destination addresses for memory copy
0x48, 0x8D, 0xB4, 0x24, 0x40, 0x03, 0x00, 0x00, // lea rsi, [rsp + 340h]
0x48, 0x8D, 0x7C, 0x24, 0x20, // lea rdi, [rsp + 20h]
// 5. Temporarily save rcx and perform memory copy (rep movsq)
0x4C, 0x8B, 0xD1, // mov r10, rcx
0xB9, 0x40, 0x00, 0x00, 0x00, // mov ecx, 40h
0xF3, 0x48, 0xA5, // rep movsq
0x49, 0x8B, 0xCA, // mov rcx, r10
// 6. Call the target function
0xFF, 0x94, 0x24, 0x38, 0x03, 0x00, 0x00, // call [rsp + 338h]
// 7. Restore stack space and registers
0x48, 0x81, 0xC4, 0x00, 0x03, 0x00, 0x00, // add rsp, 300h
0x5F, // pop rdi
0x5E, // pop rsi
// 8. Dynamically obtain the key and decrypt the return address
0x4C, 0x8B, 0x1D, 0x00, 0x00, 0x00, 0x00, // mov r11, XorKey
0x4C, 0x31, 0x1C, 0x24, // xor [rsp+0], r11
// 9. Return
0xC3 // retn
};
#pragma pack(push, 1)
typedef struct _SPOOF_SHELLCODE_TEMPLATE {
UCHAR mov_r11_opcode[3]; // mov r11, xxx
LONG32 first_xor_key_offset;
UCHAR pad_1[56];
UCHAR mov_r11_opcode_2[3]; // mov r11, xxx
LONG32 second_xor_key_offset;
UCHAR pad_2[5];
} SPOOF_SHELLCODE_TEMPLATE, *PSPOOF_SHELLCODE_TEMPLATE;
#pragma pack(pop)
#define OFFSET(type, field) ((ULONG_PTR)(&((type*)0)->field))Traverse the system’s loaded driver modules, randomly select a legitimately loaded module, and search for a code cave within its address range. If one is found, write the shellcode into it. Pay attention to the following:
- Prefer searching within the
.textsection of the driver module, because thePAGEsection may be paged out to disk when system memory is under pressure. - Avoid modules such as
ntoskrnl,win32k(graphics subsystem), andhal(hardware abstraction layer), since modifying them may trigger PatchGuard.
In theory, this solution supports both R0 and R3, and the implementation is largely similar on both sides. The R0 driver source code has already been open-sourced. The test environment is Win10 19044. If you are interested, feel free to give it a Star.

