深入分析 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. 用户态执行流程

  1. 初始调用

    • 用户代码调用LoadLibraryW(L"kerberos.dll")
    • 执行重定向到KernelBase.dll
  2. KernelBase.dll处理

    • 调用RtlInitUnicodeStringEx初始化UNICODE_STRING结构
    • 将参数传递给LdrLoadDLL(前缀Ldr表示Loader)
    • LdrLoadDLL调用私有函数LdrpLoadDll
  3. 完整性检查

    • 执行一系列完整性检查后进入内核态
    • 第一个调用的内核函数是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 函数执行流程

  1. 初始检查

    • 获取PreviousMode
    • 检查PHANDLE值是否超过MmUserProbeAddress
    • 错误会导致错误998("无效访问内存位置")
  2. 核心操作

    • 调用ObOpenObjectByName
    • MmSectionObjectType地址检索Section类型对象

3.3 内核对象处理

  1. 对象创建信息

    • 调用ObpCaptureObjectCreateInformation
    • 存储OBJECT_CREATE_INFORMATION结构
    • 复制UNICODE_STRING对象名,最大长度改为0xF8h
  2. 进程信息获取

    • 获取KTHREAD指针(gs:188h)
    • 获取KPROCESS指针(KTHREAD + 98h -> ApcState + 20h -> Process)
    • KPROCESS是EPROCESS的第一个元素
    • 获取UniqueProcessId(EPROCESS + 2E0h)
  3. 访问权限检查

    • 调用SepCreateAccessStateFromSubjectContext
    • 创建ACCESS_STATE对象(属于"安全参考监视器"组件)

4. ObpLookupObjectName函数

4.1 函数参数

+-------------------+-------------------------------+
| 参数              | 描述                          |
+-------------------+-------------------------------+
| OBJECT_ATTRIBUTES | 对象属性                      |
| OBJECT_TYPE       | 对象类型                      |
| ACCESS_MASK       | 请求的访问权限                |
| PACCESS_STATE     | 访问状态指针                  |
| ...               | 其他参数                      |
+-------------------+-------------------------------+

4.2 KnownDlls机制

  1. KnownDlls目录

    • 通过ObReferenceObjectByHandle获取引用
    • 包含已加载到内存中的Section Objects列表
    • 对应于注册表中的HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Session Manager\KnownDLLs
  2. 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 对象匹配检查

  1. 检查计算出的HashIndex是否匹配Section的Hash
  2. 获取OBJECT_HEADER_NAME_INFO:
    • 公式:ObjectHeader - ObpInfoMaskToOffset - ObpInfoMaskToOffset[InfoMask & 3]
  3. 再次检查对象名称是否匹配
  4. 填充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 句柄创建流程

  1. 初始检查

    • 检查是否为内核对象句柄
    • 获取进程的ObjectTable(HANDLE_TABLE结构)
  2. 资源获取

    • 调用ExAcquireResourceSharedLite获取PrimaryToken资源
    • 调用SeAccessCheck检查访问权限
  3. 句柄计数

    • 调用ObpIncrementHandleCountEx增加句柄计数
    • 注意:增加计数器不意味着句柄已打开
  4. 句柄分配

    • 从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. 结论

  1. 内核代码复杂且涉及大量检查
  2. 此时获得的句柄是Section对象的句柄,不是LoadLibrary最终返回的模块句柄
  3. 此时DLL尚未加载到进程地址空间
  4. KnownDlls机制优化了常用DLL的加载过程

7. 后续分析

在Part 2中将分析:

  • 当DLL不在KnownDlls中时的处理流程
  • 调用NtOpenFile的过程
  • DLL实际加载到进程地址空间的机制
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可能包含的值: 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 函数参数 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计算算法 伪代码实现: 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 句柄值计算 伪代码实现: 6. 结论 内核代码复杂且涉及大量检查 此时获得的句柄是Section对象的句柄,不是LoadLibrary最终返回的模块句柄 此时DLL尚未加载到进程地址空间 KnownDlls机制优化了常用DLL的加载过程 7. 后续分析 在Part 2中将分析: 当DLL不在KnownDlls中时的处理流程 调用NtOpenFile的过程 DLL实际加载到进程地址空间的机制