• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2025-09-30 11:13 Aet 隐藏边栏 |   抢沙发  2 
文章评分 1 次,平均分 5.0

启动流程

BasicEntry

  1. 作为dll导出接口
  2. PrepareEnv
    1. 先构建运行时
      ntdll.dll
      kernel32.dll
    2. 获取当前dll句柄
    3. EXE被当做DLL加载时,首先修复重定位数据,此时的全局变量等还无法访问
      因为Windows操作系统不会修复当做DLL加载的EXE程序
      FixRelocationTablePE文件重定位表修复
      详见下文重定位项
    4. 作为DLL的入口点重设
      详见下文
    5. 修复导入表
      详见下文

  1. RunCommonEntry
    1. 启动一个线程,入口点为WinMainCrtEntry,阻塞等待该线程处理完成
    2. FixLdrInfo
      详见下文
    3. _DllMainCRTStartup
      如果是EXE作为DLL运行,需要调用_DllMainCRTStartup函数执行初始化,否则程序不能正常运行
    4. wWinMain

wWinMain

  1. _DllMainCRTStartup被调用后,最后会调到wWinMain这里

  1. 如果是exe模式,走test::InstallMain

  1. 如果是dll模式,走BasicEntryEx

其他

获取表目录

关于重定位表项

  1. 每个重定位项是一个16位(2字节)的值,其结构通过位域解析
  2. 4位(Bits 12-15):类型
    1. 常见值 3(IMAGE_REL_BASED_HIGHLOW) 表示这是一个需要完整修正的32位地址(在64位系统上也可能是64位)
  3. 12位(Bits 0-11):偏移
    1. 表示需要修正的地址距离所在块 VirtualAddress的偏移量
  4. 判断
    1. 代码首先从PE文件头的数据目录表中获取重定位表的位置和大小
    2. 如果表不存在或为空,说明这个模块不需要重定位(比如它被加载到了预期的基地址),函数直接返回 FALSE

  1. 计算基址差值
    1. module_handle是模块当前在内存中的实际基地址
    2. odule_default_base是模块期望被加载到的基地址(存储在PE头部的 ImageBase字段中)
    3. 这两个地址的差值(Delta)就是所有绝对地址需要调整的量

  1. 定位重定位表项
    1. 通过 VirtualAddress(一个RVA)加上基地址,得到内存中重定位表的起始指针
    2. for循环的条件是判断当前块是否有效(地址和大小均不为0
      一个全零的 IMAGE_BASE_RELOCATION结构标志着重定位表的结束

  1. 处理每个重定位块内的项
    1. 一个块的大小减去 IMAGE_BASE_RELOCATION结构本身的大小(8字节),剩下的空间全部用于存储 WORD(双字节)项
    2. 所以项的数量是 (SizeOfBlock - 8) / 2

  1. 解析每个重定位项并进行修正
    1. 这是最核心的循环。对块内的每一个项,提取其类型和偏移
    2. 只有当类型是 IMAGE_REL_BASED_HIGHLOW(值为3) 时,才进行地址修正,这对应着32位或64位的绝对地址寻址

  1. 核心修正算法
    1. 公式就是:新地址 = 旧地址 - 期望基址 + 实际基址

  1. 移动到下一个块并清理
    1. 根据当前块的 SizeOfBlock跳转到下一个重定位块

判断是不是DLL

将当前PE结构中的EXE标志改为DLL标志

入口点重设

  1. 实现了一项非常底层的技术:在内存中将一个PEPortable Executable)模块从可执行程序(EXE)的动态特性改造为动态链接库(DLL

  1. 流程:
    1. 修改PE文件头Characteristics标志
    2. 取消exe标志IMAGE_FILE_EXECUTABLE_IMAGE
    3. 设置dll标识IMAGE_FILE_DLL
    4. 重定向入口点至_DllMainCRTStartup
    5. 计算相对虚拟地址RVA
    6. 更新AddressOfEntryPoint字段
    7. 详细解析如下
  2. 获取PE头结构并修改文件特征标志
    1. GetNtHeaders是一个辅助函数,目的是从模块基地址(module_handle)定位到PE文件的NT头结构(IMAGE_NT_HEADERS32
      NT头包含了PE文件的签名、文件头和可选头
    2. 由于PE头在内存中通常被设置为只读(PAGE_READONLY),直接写入会导致访问违规
      因此,代码使用 runtime._VirtualProtect(一个通过函数指针抽象的系统API调用)临时将内存页的属性改为可读可写(PAGE_READWRITE
      这是进行底层修改的关键步骤
    3. FileHeader.Characteristics字段包含了描述文件属性的一系列标志
    4. 修改完成后,立即将内存保护属性恢复原状

  1. 重定向入口点至DLLCRT启动函数
    1. OptionalHeader是NT头的一部分,它包含了对于加载器至关重要的信息,其中就包括 AddressOfEntryPoint
      这个字段指定了系统加载模块后,执行代码的起始地址(相对于模块基地址的偏移量,即RVA
    2. 计算新的入口点RVA,代码中将入口点设置为 _DllMainCRTStartup
      _DllMainCRTStartup是C/C++运行时库(CRT)为DLL提供的标准入口函数
      详见下文
    3. (ULONG_PTR)((LPBYTE)_DllMainCRTStartup - (LPBYTE)module_handle)
      这行代码计算的是 _DllMainCRTStartup函数在当前内存中相对于模块基地址的偏移量(RVA
      因为 AddressOfEntryPoint需要的是一个RVA,而不是绝对地址

修复导入表

  1. PE文件导入表(Import Address Table, IAT)的动态修复功能
    1. 模拟了Windows操作系统加载器在运行程序时的一个核心步骤:将程序代码中调用外部DLL函数的"桩"地址,替换成DLL函数在内存中的实际地址

  1. 获取导入表并初始化
    1. GetDataDirectory函数从PE文件头的数据目录表中获取导入表的位置和大小
    2. 如果获取失败(例如,该模块没有导入表),函数直接返回 FALSE
    3. 计算导入表在内存中的实际起始地址
      module_handle是模块的基地址,加上 VirtualAddress(一个相对虚拟地址RVA),得到第一个 IMAGE_IMPORT_DESCRIPTOR结构的指针

  1. 遍历导入表(外层循环:处理每个DLL
    1. while循环遍历导入表,直到遇到一个全零的 IMAGE_IMPORT_DESCRIPTOR结构,这表示导入表的结束
    2. 对于每个描述符,它指向该模块所依赖的一个DLL
    3. 根据描述符中的 Name字段(RVA),计算出该DLL文件名字符串在内存中的地址
    4. 调用 Runtime->_LoadLibraryA动态加载这个DLL,并获取其模块句柄(HMODULE

  1. 遍历导入函数(内层循环:处理当前DLL中的每个函数)
    1. 根据描述符的 FirstThunk字段(指向IAT)定位到函数地址列表
    2. 内层 while循环遍历这个列表,直到遇到一个 NULLIMAGE_THUNK_DATA32项,这表示该DLL的导入函数列表结束
    3. 每个 thunk结构体最初并不直接包含函数地址,而是包含一个提示如何找到函数的信息

  1. 核心修复逻辑:名称导入
    1. 当最高位为0时,thunkValue是一个RVA,指向一个 IMAGE_IMPORT_BY_NAME结构
    2. 这个结构包含一个提示(Hint)和真正的函数名称字符串
    3. 通过 Runtime->_GetProcAddress使用函数名来获取该函数在已加载DLL中的实际地址
    4. WriteMemory函数将这个实际地址写回到 thunk->u1.Function指向的内存位置,即 修复IAT中该函数的项

  1. 核心修复逻辑:序号导入
    1. 当最高位为1时,thunkValue的低31位直接就是函数的导出序号
    2. Runtime->_GetProcAddress通过这个序号(强制转换为 LPCSTR)来获取函数地址
    3. 同样,将获取到的地址写回IAT

_DllMainCRTStartup

  1. 它的主要职责是初始化CRT环境(如全局变量、静态对象等)
  2. 然后调用开发者编写的 DllMain函数
  3. 并正确处理 fdwReason(如 DLL_PROCESS_ATTACH, DLL_THREAD_ATTACH等事件)

Ldr信息修复

  1. 用于在 运行时手动修复当前模块在PEB(进程环境块)的加载器数据结构中的信息
    1. 相关概念peb见下文

  1. 安全检查与初始化
    1. MemoryBarrier()是一个内存屏障调用,用于确保在获取PEB指针之前,之前的所有内存操作都已完成,防止CPU或编译器的乱序执行导致问题
    2. GetPeb获取当前进程PEB指针
      x86架构上,这通常通过读取fs段寄存器的特定偏移量来实现(例如 mov eax, fs:[0x30]
    3. pLdr指向了PEB中的加载器数据结构,这是操作的目标

  1. 遍历模块列表
    1. 获取了InLoadOrderModuleList链表的头节点(Flink指向第一个节点)和尾节点(Blink指向最后一个节点)
    2. for循环遍历链表

  1. 定位目标模块
    1. 循环体内,将链表节点指针(PLIST_ENTRY)强制转换为PLDR_DATA_TABLE_ENTRY
      这是因为LDR_DATA_TABLE_ENTRY结构的第一个成员就是LIST_ENTRY
      这种技巧在Windows内核编程中非常常见,称为"侵入式链表"
    2. 然后比较当前节点的DllBase(模块基地址)是否与我们要找的g_CurrentModuleHandle相等
      如果相等,就找到了目标模块的加载器数据表项,记录下指针并跳出循环

  1. 找到目标模块的LDR_DATA_TABLE_ENTRY后,函数开始执行关键的修复步骤:
    1. EntryPoint设置为_DllMainCRTStartup。这是C/C++运行时库(CRT)提供的标准DLL入口函数,它负责初始化全局对象,然后调用用户编写的DllMain函数
      这个操作强制将模块的入口行为标记为DLL
    2. ImageDll = 1:明确告知系统此模块是一个DLL镜像,而不是EXE
    3. ProcessAttachCalled = 1:这是一个非常关键且具有"欺骗性"的操作
      它告诉系统,该模块的DllMain(在传递DLL_PROCESS_ATTACH通知时)已经被调用过了
      这样做的目的可能是为了阻止系统加载器再次尝试初始化该模块,避免重复初始化或冲突,常用于绕过正常的模块加载序列

  1. 被注释的TLS(线程本地存储,TLS允许每个线程拥有全局变量的独立副本)处理
    1. PE头部获取TLS目录
    2. 为当前模块分配一个TLS入口
    3. 将其加入到系统的TLS管理列表中,确保每个新线程创建时能正确初始化该模块的TLS数据

PEB(Process Environment Block)

  1. 每个进程都有一个PEB,它包含了进程的全局信息,如命令行参数、环境变量、已加载模块列表等
  2. PEB_LDR_DATA
    1. 这是PEB中一个非常重要的成员,它维护着三个双向链表,记录了进程加载的所有模块(EXEDLL)的信息
    2. 函数中使用的 InLoadOrderModuleList就是其中之一,它按模块的加载顺序排列
  3. LDR_DATA_TABLE_ENTRY
    1. 链表中的每个节点都是这个结构。它详细描述了一个已加载模块的信息,包括:
    2. DllBase:模块加载到内存中的基地址(HMODULE
    3. EntryPoint:模块的入口点地址(例如DllMain
    4. Flags:模块的属性标志
    5. ImageDll:标记该模块是否是DLL镜像
    6. ProcessAttachCalled:标记该模块的DllMain是否已因进程附加而被调用过

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享