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

启动流程

BasicEntry

  1. 作为dll导出接口
  2. PrepareEnv
    1. 先构建运行时
      ntdll.dll
      kernel32.dll
    2. 获取当前dll句柄
    3. EXE被当做DLL加载时,首先修复重定位数据,此时的全局变量等还无法访问
      因为Windows操作系统不会修复当做DLL加载的EXE程序
      FixRelocationTable:PE文件重定位表修复
      详见下文重定位项
    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. 实现了一项非常底层的技术:在内存中将一个PE(Portable 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. 重定向入口点至DLL的CRT启动函数
    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循环遍历这个列表,直到遇到一个 NULL的 IMAGE_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中一个非常重要的成员,它维护着三个双向链表,记录了进程加载的所有模块(EXE和DLL)的信息
    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 最后编辑于:2025-10-12
Everything will be better.

发表评论

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