

Windows 架构#
现代操作系统设计的核心基石之一,便是对处理器特权级别的划分。在Windows中,这体现为两种截然不同的工作模式:用户模式 (User Mode) 和 内核模式 (Kernel Mode)。
- 用户模式 (Ring 3):应用程序的运行环境。无论是浏览器、代码编辑器还是游戏,它们都受到严格的权限限制,无法直接访问物理硬件或干涉其他进程的核心数据,从而保证了系统的整体稳定性。
- 内核模式 (Ring 0):操作系统的核心领域。它拥有CPU的最高特权级别,能够执行所有指令,直接管理CPU、内存、I/O设备等硬件资源,是整个系统安全与资源调度的仲裁者。
经典模型#
这种隔离设计是现代操作系统的基石。应用程序无法独立完成任何有意义的功能(如读写文件、创建网络连接、创建销毁进程),因为这些操作都涉及到底层硬件资源。应用程序必须“请求”内核来代为完成。整体调用链条如下:
用户进程 (Application) → 子系统DLL (Subsystem DLL) → Ntdll.dll (Native API) → [模式切换] → 内核 (Kernel)
这就像一家餐厅的运作流程:
-
你 (用户进程):想吃一份牛排。
-
服务员 (子系统DLL):过来为你点单,用餐厅的术语记下“T-Bone, medium rare”。
-
传菜员 (Ntdll.dll):将标准化的菜单指令传递给厨房。
-
厨房大门 (模式切换):这是普通人无法进入的地方。
-
厨师 (内核):在厨房里,用火、锅、食材等真正地做出牛排。
现代模型#
需要注意的是,从win7开始,微软为了实现一种叫做 “API Sets” 的技术(也为了后来的UWP应用架构),微软对Win32 API层进行了一次重构,将 kernel32.dll 中的大部分实现代码移动到了一个新的DLL中,那就是 KernelBase.dll。现在的 kernel32.dll 变成了一个**“转发层” (Forwarding Layer)**。它里面几乎没有真正的代码,只有一些“跳板”指令(JMP),把API调用直接转发给 KernelBase.dll 中的同名函数去执行。调用链条如下:
用户进程 (Application) → kernel32.dll (转发) → KernelBase.dll (实现) → ntdll.dll (原生API) → [模式切换] → 内核 (Kernel)
函数调用流程#
本文以CreateThread
为例展示函数的调用流程。
#include <stdio.h>
#include <Windows.h>
DWORD WINAPI MyThreadFunction(LPVOID lpParam)
{
// 1. 将 LPVOID 类型的参数强制转换回它本来的类型。
// 我们知道我们传递的是一个字符串 (const char*)。
const char* message = (const char*)lpParam;
// 2. 在包装函数内部,安全地调用任何你需要的函数。
printf_s("Message from the new thread: %s\n", message);
// 3. 任务完成,返回一个 DWORD 值作为线程退出码。
return 0;
}
int main()
{
// 准备要传递给线程的参数
const char* threadMessage = "Hello from main()!";
// 创建线程
HANDLE hThread = CreateThread(
NULL, // 默认安全属性
0, // 默认栈大小
MyThreadFunction, // * 传递我们自己包装函数的地址
(LPVOID)threadMessage, // * 将我们的消息作为 LPVOID 类型的参数传递
0, // 默认创建标志
NULL); // 不需要返回线程ID
if (hThread == NULL)
{
printf_s("Failed to create thread. Error code: %d\n", GetLastError());
return 1;
}
// 等待子线程执行完毕,否则 main 函数可能先结束,导致子线程来不及执行。
WaitForSingleObject(hThread, INFINITE);
// 关闭线程句柄,释放系统资源。
CloseHandle(hThread);
printf_s("Thread has finished execution. Main function is exiting.\n");
return 0;
}
c1.用户进程#
使用x64dbg加载编译后的程序,通过符号表定位到main函数入口。代码执行到调用CreateThread
之前,我们可以清晰地看到x64调用约定下,各参数被依次载入RCX
, RDX
,R8
, R9
寄存器。
2.转发层 - Kernel32.dll#
步进我们在代码中调用的 CreateThread
函数,可以发现我们来到了 kernel32.dll 这个动态链接库中。
分析其汇编代码可以发现,CreateThread
并未包含复杂的逻辑,它更像是一个“前台接待”。它对参数进行了简单的重排,随即调用了一个功能更全面的API——CreateRemoteThreadEx
。这印证了kernel32.dll
作为转发层的角色。
CreateRemoteThreadEx
是一个非常强大的函数,可以在有正确权限和句柄的情况下在任意进程中创建线程。
CreateThread
则是仅仅在当前进程中创建一个线程。
3.实现层 - KernelBase.dll#
步进KernelBase.dll
后跟进代码,找到了NtCreateThreadEx
。
它会调用 ntdll.dll 中的一个原生API (Native API)。这些API通常以 Nt
或 Zw
开头,是用户模式与内核模式之间的“最后一道门”。对于创建线程而言,它最终会调用 NtCreateThreadEx
。
4.原生 API - ntdll.dll#
00007FFC09D | 4C:8BD1 | mov r10,rcx | r10:"Actx "
00007FFC09D | B8 C9000000 | mov eax,C9 |加载系统服务号(SSN),相当于告诉内核我要‘创建线程’
00007FFC09D | F60425 0803FE7F 0 | test byte ptr ds:[7FFE0308],1 |
00007FFC09D | 75 03 | jne ntdll.7FFC09D03415 |
00007FFC09D | 0F05 | syscall |执行系统调用!从这里,控制权将进入操作系统内核,我们的用户态跟踪到此结束。
00007FFC09D | C3 | ret |
plaintext5.模式切换#
在syscall
处进行步进可以发现,光标停在了 C3(ret)
处,这也印证了之前的描述,应用程序运行在用户模式下,用户态调试器没有权限进入内核空间。执行 syscall 后,CPU在内核中完成了所有创建线程的复杂工作,然后将结果(新线程的句柄)放入 rax 寄存器,最后返回到用户模式的 ntdll.dll 中,准备将结果一层层地传回给应用程序。
从免杀角度看API调用选择#
一个很自然的问题是:既然我们最终都要调用到ntdll.dll
中的原生API,为什么不一开始就直接调用它,反而要绕道kernel32.dll
和KernelBase.dll
呢?
从普通应用开发的角度看,答案是为了稳定和兼容。Win32 API是公开且稳定的接口,而原生API是未公开的,其函数签名甚至功能都可能在不同Windows版本间发生变化。
但从**恶意软件开发与免杀(Evasion)**的角度来看,这个问题就变得至关重要。API的选择直接决定了其行为能否绕过安全产品的监控。
调用高层API (kernel32.dll)#
这是最常规的编程方式,但对于恶意软件来说,也是最危险的方式。
- 优点:
- 简单稳定:代码编写简单,遵循官方文档,兼容性好。
- 缺点:
- 高频监控点 (Heavily Monitored):
kernel32.dll
和KernelBase.dll
中的函数是所有终端安全产品(EDR/AV)重点监控的对象。它们会通过API Hooking技术,在CreateThread
这类敏感函数的入口处插入自己的“探针”。一旦函数被调用,安全产品就能立刻捕获,并分析其行为(例如,是否用于代码注入),从而进行拦截。 - 意图暴露 (Obvious Intent):恶意软件如果静态链接了
kernel32.dll
并导入了CreateThread
函数,那么通过分析其导入地址表(IAT),就能轻易发现其意图。这是一种非常低级的、容易被静态查杀的特征。
- 高频监控点 (Heavily Monitored):
简单来说,调用kernel32.dll
就像从银行的正门进去,虽然路最简单,但到处都是摄像头和保安。
直接调用原生API (ntdll.dll)#
这是更高级、更隐蔽的攻击者偏爱的方式。其核心思想是:在运行时动态地获取API地址,而不是在编译时静态链接。
- 优点:
- 绕过用户态钩子 (Bypass User-Mode Hooks):这是最大的优势。如果EDR的钩子设置在
KernelBase!CreateThread
上,而恶意软件直接调用ntdll!NtCreateThreadEx
,就相当于**“跳过”**了EDR的用户态监控点,使其探针完全失效。 - 隐藏意图 (Hiding Intent):通过动态解析API,恶意软件的导入表中不会出现任何敏感函数名,极大地增加了静态分析的难度。
- 绕过用户态钩子 (Bypass User-Mode Hooks):这是最大的优势。如果EDR的钩子设置在
常见调用原生API方式之一:手动解析导出表#
攻击者通常不会使用GetProcAddress
,因为它本身也可能被监控。取而代之的是,他们会编写代码来手动遍历DLL的内存结构,直接从其**导出地址表(Export Address Table, EAT)**中找到所需函数的地址。
FARPROC MyGetProcAddress(HMODULE hModule, char* lpProcName) {
IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)hModule;
IMAGE_NT_HEADERS64* pNtHeaders = (IMAGE_NT_HEADERS64*)((char*)pDosHeader + pDosHeader->e_lfanew);
//LPVOID exports1 = (LPVOID)&(pNtHeaders->OptionalHeader.DataDirectory[0]);
//DWORD exports2 = pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress;
IMAGE_EXPORT_DIRECTORY* pExportDir = (IMAGE_EXPORT_DIRECTORY*)((char*)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
DWORD* pAddressOfNames = (DWORD*)((char*)pDosHeader + pExportDir->AddressOfNames);
WORD* pAddressOfOrdinals = (WORD*)((char*)pDosHeader + pExportDir->AddressOfNameOrdinals);
DWORD* pAddressOfFunctions = (DWORD*)((char*)pDosHeader + pExportDir->AddressOfFunctions);
for (DWORD i = 0; i < pExportDir->NumberOfNames; i++) {
LPCSTR pProcName = (LPCSTR)((char*)pDosHeader + pAddressOfNames[i]);
if (MyStrCmp((char*)pProcName, lpProcName) == 0) {
WORD ordinal = pAddressOfOrdinals[i];
DWORD functionRVA = pAddressOfFunctions[ordinal];
FARPROC functionPtr = (FARPROC)((char*)hModule + functionRVA);
return functionPtr;
}
}
return NULL;
}
c通过HMODULE ntdllHandle = GetModuleHandleA("ntdll.dll");
和上述MyGetProcAddress(ntdllHandle, "NtCreateThreadEx")
,恶意软件就能在不触碰任何敏感API的情况下,拿到原生函数的指针,从而执行隐蔽的操作。
直接系统调用#
最顶尖的攻击者甚至不调用ntdll.dll
中的函数。他们会自己编写一小段汇编代码,手动将系统服务号(SSN)放入eax
寄存器,然后执行syscall
。这可以绕过对ntdll.dll
本身的钩子,是目前最高级的用户态绕过技术之一。但这种方法也面临着SSN在不同系统版本间不一致的问题,增加了开发的复杂性。
总结#
特性 | 调用高层API (kernel32.dll) | 直接调用原生API (ntdll.dll) |
---|---|---|
实现方式 | 静态链接,直接调用 | 动态解析地址,或直接执行syscall |
优点 | 简单、稳定、兼容性好 | 隐蔽、可绕过用户态Hook、隐藏API导入意图 |
缺点 | 极易被EDR/AV监控和拦截,意图暴露 | 实现复杂、不稳定(依赖系统版本)、仍可能被内核态监控捕获 |
攻击者画像 | 初级攻击者,通用恶意软件 | 高级持续性威胁(APT),注重免杀的攻击框架 |
因此,API调用方式的选择,是攻击与防御之间一场永恒的“猫鼠游戏”。防御方在更高层设防,攻击方就往更底层钻。理解这背后的完整调用链和技术细节,不仅有助于理解操作系统原理,更是理解现代网络安全攻防对抗的基石。