深入分析 Windows API - LoadLibrary 的内部实现 Part 1
字数 2219 2025-08-05 11:39:26
Windows API - LoadLibrary 内部实现深度分析 Part 1
1. 概述
本文深入分析Windows API中LoadLibrary函数的内部实现机制,重点关注内核层面的执行流程。研究基于Windows 8.1内核版本9600。
2. 用户态执行流程
-
初始调用:
- 用户代码调用
LoadLibraryW(L"kerberos.dll") - 执行重定向到
KernelBase.dll
- 用户代码调用
-
KernelBase.dll处理:
- 调用
RtlInitUnicodeStringEx初始化UNICODE_STRING结构 - 将参数传递给
LdrLoadDLL(前缀Ldr表示Loader) LdrLoadDLL调用私有函数LdrpLoadDll
- 调用
-
完整性检查:
- 执行一系列完整性检查后进入内核态
- 第一个调用的内核函数是
NtOpenSection
3. NtOpenSection函数分析
3.1 参数说明
x64架构下使用__fastcall调用约定:
rcx:PHANDLE指针,接收对象句柄rdx:ACCESS_MASK,请求的对象访问权限r8:POBJECT_ATTRIBUTES,指向DLL的OBJECT_ATTRIBUTES
ACCESS_MASK可能包含的值:
#define SECTION_QUERY 0x0001
#define SECTION_MAP_WRITE 0x0002
#define SECTION_MAP_READ 0x0004
#define SECTION_MAP_EXECUTE 0x0008
3.2 函数执行流程
-
初始检查:
- 获取PreviousMode
- 检查PHANDLE值是否超过MmUserProbeAddress
- 错误会导致错误998("无效访问内存位置")
-
核心操作:
- 调用
ObOpenObjectByName - 从
MmSectionObjectType地址检索Section类型对象
- 调用
3.3 内核对象处理
-
对象创建信息:
- 调用
ObpCaptureObjectCreateInformation - 存储OBJECT_CREATE_INFORMATION结构
- 复制UNICODE_STRING对象名,最大长度改为0xF8h
- 调用
-
进程信息获取:
- 获取KTHREAD指针(gs:188h)
- 获取KPROCESS指针(KTHREAD + 98h -> ApcState + 20h -> Process)
- KPROCESS是EPROCESS的第一个元素
- 获取UniqueProcessId(EPROCESS + 2E0h)
-
访问权限检查:
- 调用
SepCreateAccessStateFromSubjectContext - 创建ACCESS_STATE对象(属于"安全参考监视器"组件)
- 调用
4. ObpLookupObjectName函数
4.1 函数参数
+-------------------+-------------------------------+
| 参数 | 描述 |
+-------------------+-------------------------------+
| OBJECT_ATTRIBUTES | 对象属性 |
| OBJECT_TYPE | 对象类型 |
| ACCESS_MASK | 请求的访问权限 |
| PACCESS_STATE | 访问状态指针 |
| ... | 其他参数 |
+-------------------+-------------------------------+
4.2 KnownDlls机制
-
KnownDlls目录:
- 通过
ObReferenceObjectByHandle获取引用 - 包含已加载到内存中的Section Objects列表
- 对应于注册表中的
HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Session Manager\KnownDLLs
- 通过
-
DLL查找流程:
- 计算DLL名称的Hash值
- 检查Hash是否匹配KnownDlls中的条目
- 不匹配则返回错误"c0000034"("找不到对象名")
4.3 Hash计算算法
伪代码实现:
QWORD res = 0;
DWORD hash = 0;
DWORD size = Dll.Length >> 1; // Unicode字符数
PWSTR dll_buffer = unicode_string_dll.Buffer;
if(size > 4) {
do {
QWORD acc = dll_buffer;
if(!(Dll_Buffer & 0xff80ff80ff80ff80))
acc = (QWORD*)Dll_Buffer & 0xffdfffdfffdfffdf;
size -= 4;
dll_buffer += 4;
res = acc + (res >> 1) + 3 * res;
} while(size >= 4);
hash = (DWORD)res + (res >> 0x20);
// 处理剩余不足4个字符的情况
}
obpLookupCtx.HashValue = hash;
obpLookupCtx.HashIndex = hash % 25;
4.4 对象匹配检查
- 检查计算出的HashIndex是否匹配Section的Hash
- 获取OBJECT_HEADER_NAME_INFO:
- 公式:
ObjectHeader - ObpInfoMaskToOffset - ObpInfoMaskToOffset[InfoMask & 3]
- 公式:
- 再次检查对象名称是否匹配
- 填充OBP_LOOKUP_CONTEXT结构
5. ObpCreateHandle函数
5.1 参数说明
rcx:OB_OPEN_REASON枚举值- ObCreateHandle = 0
- ObOpenHandle = 1
- ObDuplicateHandle = 2
- ObInheritHandle = 3
- ObMaxOpenReason = 4
rdx:引用对象(DLL Section Object)r9:ACCESS_STATE结构
5.2 句柄创建流程
-
初始检查:
- 检查是否为内核对象句柄
- 获取进程的ObjectTable(HANDLE_TABLE结构)
-
资源获取:
- 调用
ExAcquireResourceSharedLite获取PrimaryToken资源 - 调用
SeAccessCheck检查访问权限
- 调用
-
句柄计数:
- 调用
ObpIncrementHandleCountEx增加句柄计数 - 注意:增加计数器不意味着句柄已打开
- 调用
-
句柄分配:
- 从HANDLE_TABLE的FreeLists获取空闲句柄
- 计算句柄值并设置相关属性
5.3 句柄值计算
伪代码实现:
HANDLE_TABLE* HandleTable = KeGetCurrentThread()->ApcState.Process->ObjectTable;
HANDLE_TABLE_FREE_LIST* HandlesFreeList = HandleTable->FreeLists[indexTable];
// 获取第一个空闲句柄
NewHandle = HandlesFreeList->FirstFreeHandleEntry;
// 计算句柄值
tmp = *((NewHandle & 0xFFFFFFFFFFFFF000) + 8);
tmp1 = (NewHandle - (NewHandle & 0xFFFFFFFFFFFFF000)) >> 4;
HandleValue = tmp + tmp1 * 4;
// 设置句柄属性
NewHandle->LowValue = LowValue;
NewHandle->HighValue = HighValue;
6. 结论
- 内核代码复杂且涉及大量检查
- 此时获得的句柄是Section对象的句柄,不是LoadLibrary最终返回的模块句柄
- 此时DLL尚未加载到进程地址空间
- KnownDlls机制优化了常用DLL的加载过程
7. 后续分析
在Part 2中将分析:
- 当DLL不在KnownDlls中时的处理流程
- 调用NtOpenFile的过程
- DLL实际加载到进程地址空间的机制