自实现 LoadLibrary 和 GetProcAddress
虽然现在有 AI 了,但对于 PE 结构,仍是我们学习 Windows 逆向绕不过去的关卡。
必须把基础打好,才能更加熟练地运用 AI。
完整工程(可编译对照):https://github.com/1903247335/manual-map-loadlibrary
为什么要自实现 LoadLibrary 和 GetProcAddress
我们可以对 PE 结构更加了解,明白底层 Loader 是怎么样加载程序、展开的。
因为这样可以免除掉部分 R3 层的检测,例如:
CreateToolhelp32Snapshot(TH32CS_SNAPMODULE) + Module32First / Next
EnumProcessModules / EnumProcessModulesEx(psapi)
GetModuleHandle / GetModuleHandleEx(按名字找;你的 DLL 本来也没注册名字)
- 自己遍历:
PEB → Ldr → InLoadOrderModuleList
InMemoryOrderModuleList
InInitializationOrderModuleList
需要的前置知识
- 对常规 Win API 有一定的了解
- 熟悉 PE 结构:只需要会导入表、导出表、重定位表原理
我们的目标是不考虑 TLS 表,只考虑简单的程序。单单会这个,别的也可以举一反三了。
接下来简单说下原理
LoadLibrary 是把 DLL 从文件结构直接拓展到内存结构,然后进行:
- 导入表修复(根据 INT 来把 IAT 换成真实函数地址)
- 重定位修复(根据写死的地址换成加载之后正确的地址)
很简单:
新地址 = 目标写死的地址 + 实际载入到内存的基地址 - 写死的基地址
修复完后找到 DLL 入口直接调用,传入 DLL_PROCESS_ATTACH,来实现 DLL 初始化函数执行。
GetProcAddress 是根据名字或者序号来读取 DLL 的导出表,拿到对应函数。
FreeLibrary 原理就是把 DLL 句柄传入,然后获取到 DLL 主入口,传入 DLL_PROCESS_DETACH 来调用卸载入口,再然后进行 VirtualFree 进行内存释放。
接下来就直接开始吧
我不会直接给代码,不然直接粘贴一堆代码太无聊了,我只说思路。
完整代码见仓库:https://github.com/1903247335/manual-map-loadlibrary
内存空间开辟
- 用
CreateFileA 打开 DLL 路径
GetFileSize 来获取 DLL 的文件大小
ReadFile 根据读出来的 File Size 移动到 buffer 当中做准备
从文件结构拓展到内存结构
这时候 buffer 已经是把 DLL 的文件结构一比一复制了,目的是为了可以直接读取它们的 PE 结构。
读取 DOS 头这些是常规操作了,直接到 OptionalHeader 里面拿到了 SizeOfImage。拿到它的目的是为了开辟「展开到内存」的结构——它代表着文件展开后的大小。
这时候可以用 VirtualAlloc 来真正开辟给 DLL 展开的空间了,同时得到的指针也是 baseAddress。
然后我们可以把 PE 头都给移动到 baseAddress 中,直接用 OptionalHeader.SizeOfHeaders。
(PE 头不需要按节去「拓展」;真正要按内存布局展开的是各个节区内容。节表告诉我们每个节该怎么搬。)
导入表修复
修复的原理是:从 INT 给出的信息来读取我们要找哪个外部 DLL、哪个函数;
找到它们的地址后,填充到 IAT 里面,以此来达到修复。
这一步是从 INT 中获取我们要找的 DLL(因为是导入系统 DLL,所以直接 LoadLibrary 不影响。
但你要是不爽,可以自己再写一个不需要系统函数也能加载系统 DLL 的——不过难度可能会非常的大)。
得到了 DLL 的句柄后,就是判断 INT 是用什么方式载入函数的——有可能是序号,也有可能是函数名。
但最核心的就是得到函数地址,并写回 IAT。
重定位表修复
这个实际也不难:
- 获取当前载入内存的地址,和实际 PE 的
OptionalHeader.ImageBase 进行比较,看看差了多少
- 获取重定位表的每一项,取出 offset
- 我们要改的地方就是:
当前载入内存的地址 + 页RVA + offset
- 然后给这个地方加上(当前载入内存的地址 - ImageBase),就是重定位成功了
实际一点都不难。
调用 DllMain
找到 entryPoint,然后直接传入规定参数:
DllMain(base, DLL_PROCESS_ATTACH, nullptr)
就可以实现调用了。
实现 GetProcAddress(根据函数名)
根据导出表来找到对应函数名。GetProcAddress 根据序号同理。
FreeLibrary 实现
找到 AddressOfEntryPoint,往里面传入 DLL_PROCESS_DETACH,
再 VirtualFree 释放内存,就这样。
写在最后
实际上并没有我们想象的那么难,就是过程代码实现可能曲折点,但思路是摆在那的。
现在有 AI 了,我们可以花更多时间在研究思路上,而不是实际代码上了。
完整工程:https://github.com/1903247335/manual-map-loadlibrary
仅供学习 Windows PE / Loader 原理,请勿用于未授权场景。