20.4 函数转发器
(1)函数转发器原理(下图是利用Dependency Walker打开Kernel32.dll得到)
①图中CloseThreadpool*等4个函数转发到NTDLL中相应的函数中去了,但我们调用CloseThreadpool*等函数时,exe会被动态地链接到Kernel32.dll。当执行exe时,加载程序会发现被转发的函数实际上在NTDLL.dll中,然后它会将NTDLL.dll模块一并载入。
②当我们调用CloseThreadpool*函数时,那么调用GetProcAddress先在Kernel32的导出段是查找,并发现CloseThreadpool*是一个转发器函数,于是它会递归调用GetProcessAddress,在NTDLL 的导出段中查找相应的函数。
(2)实现自己的函数转发器
#pragma comment(linker,"/export:MyFunc =OtherDll.OtherFunc")
//即正在编译的Dll输出一个名为MyFunc的函数,但实际上这个函数在另一个叫OtherDll.dll模块中,函数名为OtherFunc.
20.5 己知的DLL
(1)操作系统对某些DLL进行了特殊处理,这些Dll被称为己知的Dll。在载入它们的时候,总是从"%SystemRoot%System32"目录下查找。这些Dll被记录在注册表中如下的位置
(2)当LoadLibrary(TEXT("A_Dll"))时,系统会用正常的搜索规则来定位这个Dll。但如果调用LoadLibrary(TEXT("A_Dll.dll"))时,系统会先将扩展名.dll去掉,然后在注册表查找名称为"A_Dll"的一项,并将载入该项后面“数据”项里指向的Dll(注意,这个Dll会在"%SystemRoot%System32"目录里查找),如果载入不成功,会返回NULL,GetLastError将返回(ERROR_FILE_NOT_FOUND).
20.6 DLL重定向
(1)早期Windows为了文件共享,将多个应用程序共享的所有模块都放在Windows系统目录中,但会出现一个严重问题,因为安装程序会用老版本的文件覆盖这个目录中的文件,从而妨碍其他应用程序的正常运行。
(2)从Windows2000开始,新增了DLL重定向特性,使得应用程序首先从应用程序的目录中载入模块,只有当加载程序无法找到这个文件时,才会在其他目录中搜索。
(3)为了强制加载程序先检查应用程序的目录,可以在应用程序目录中,建一个文件名为AppName.local的文件(此处的AppName如MyApp.exe),内容无关紧要。
(4)LoadLibrary(Ex)在内部做了修改,来检查这个文件是否存在,如果应用程序目录中存在这个文件,便载入这个目录中的模块。如果不存在该.local文件,则工作方式与以往相同。
(5)由于安全性缘故,该特性默认是关闭的,因为它会使系统从应用程序的文件夹中载为伪造的系统DLL,而不是从Windows的系统文件夹中载入真正的系统DLL。为打开这个特性,可以HKLMSoftwareMicrosoftWindowsNTCurrentVesionImage File Execution Options注册表项中增一个项名为DevOverrideEnable的DWORD型项,并将值设为1。
20.7 模块的基地址重定位
(1)基址重定位的原因
int g_x; void Func(){ g_x = 5; //该行很重要,当编译和链接该行成会生成Mov [0xXXXXXXXX],5之类的代码 }
①一般EXE的首选基地址为0x00400000,DLL为0x1000000。当运行exe时,加载程序为进程创建一个虚拟地址空间,并将exe映射到0x00400000处,g_x变量的变成0x00414540之类的固定地址。而当该这些代码是位于DLL模块时,这个地址将被固定为类似0x10014540之类的地址(前提是该DLL被加载到首选地址处)。
②对于EXE来说,会被加载到首选基地址处而不会造成冲突。但DLL则不一样,如果有两个DLL的首选基地址都是0x10000000。加载程序会将第1个DLL加载到首选地址上,但会对第2个DLL模块进行重定位(如加载到0x20000000),则g_x的地址会被修改为0x20014540,即汇编代码为Mov [0x20014540],5。
(2)EXE或(Dll)重定位的缺点
①当链接器在构建模块时,会将重定位段(relocation section)嵌入在生成的文件中。重定位表中的有用数据是那些需要重定位机器码所使用到的内存地址的偏移量。
②如果加载程序无法将模块载入到它的首选基地址,那么系统会遍历重定位段中的所有条目,对每一个条目,加载程序会先找到机器指令的那个存储页面。然后将模块的首选基地址减去实际映射地址,将这个差值加到机器指令使用的地址上。因为加载程序必须遍历重定位段并修改模块中的大量代码,这个过程会牺牲程序的启动时间。
③当加载程序写入模块的代码页面中时,系统的写时复制会强制这些页面以系统的页交换文件为后备存储器。因为页交换文件是所有模块(如EXE或DLL)的代码页面的后备存储器,所以这也会损害性能,减少可供系统中所有进程使用的存储器的数量。
(3)指定DLL基地址的方法
①方法1:在“配置属性”→“链接器”→“高级”→“基址”输入如0x20000000之类。同时“固定基址”这项选项“是(/Fixed)”。注意,为了少地址空间碎片,应该总是先从高内存地址开始载入DLL,然后再到低内存地址。
②方法2:
A.创建一个文件文件,在其中按照下述语法指定每个DLL的基地址和大小(大小可选)
key address [size] ;comment
其中key是一个字母数字组成的字符串,不区分大小写;通常是Dll的名称,但不必非是;只要和在/BASE中指定一样就行。Address是十六进制或十进制表示的基址。Size是可选的,一般为最大DLL的size。
B.将这个文本文件放到链接器可以搜到的地方
C.在动态库项目的基址选项中指定@filename,key格式的命令,其中@是固定前缀,filename就是刚才的文本文件,可以指定完整路径名,key就是文本文件中指定的key
【CustomDLL程序】演示修改写DLL入口点函数及用方法2指定基地址
//CustomDll.dll
#include <tchar.h> #include <windows.h> #include <strsafe.h> #include <locale.h> //更改入口点:要在“配置属性”→“链接器”→“高级”→“入口点”中输入"MyDllMain" BOOL APIENTRY MyDllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved) { switch (dwReason) { case DLL_PROCESS_ATTACH: _tsetlocale(LC_ALL, TEXT("chs")); _tprintf(_T("进程[%u]调用线程[0x%x]加载DLL[0x%08x] "), GetCurrentProcessId(), GetCurrentThreadId(), hModule); break; case DLL_THREAD_ATTACH: _tprintf(_T("进程[%u]新建线程[0x%x]调用DLL[0x%08x]入口 "), GetCurrentProcessId(), GetCurrentThreadId(), hModule); break; case DLL_THREAD_DETACH: _tprintf(_T("进程[%u]线程[0x%x]退出调用DLL[0x%08x]入口 "), GetCurrentProcessId(), GetCurrentThreadId(), hModule); break; case DLL_PROCESS_DETACH: _tprintf(_T("进程[%u]退出通过线程[0x%x]调用DLL[0x%08x]入口 "), GetCurrentProcessId(), GetCurrentThreadId(), hModule); break; } return TRUE; } __declspec(dllexport) void Func(void){ _tprintf(_T("进程[%x]中线程[0x%x]调用DLL[0x%08x]测试函数Func "), GetCurrentProcessId(), GetCurrentThreadId(), GetModuleHandle(_T("20_CustomDll.dll"))); }
//Test.cpp
#include <tchar.h> #include <windows.h> #include <strsafe.h> void Func(void); #pragma comment(lib,"../../Debug/20_CustomDLL.lib") int _tmain() { Func(); return 0; }
(4)改变编译后的DLL基址——利用editbin工具(方法:VS2013的IDE“工具”菜单→“Visual Studio命令提示”下输入“editbin”,这个工具有个/Rebase选项。
20.8 模块的绑定
(1)模块绑定原因
①可执行模块的导入段中有一个 (IAT,Import Address Table) , 载入之前这个表是空的,可执行模块载入的时候,载入程序会加载需要的 dll ,获取 dll 的基地址,获取导出符号的 RVA ,基地址加上 RVA 就是导出符号的真实地址,每个导出符号的真实地址都会被加载程序填充到IAT表中;
②可执行模块运行期间如果调用了某个dll的导出函数,那么会跳转到IAT来得到这个导出函数的地址,然后进行调用。
③IAT 中填充的是 dll 载入到地址空间的基地址+RVA。RVA在 dll 的导出段中已经有了,而通过基地址重定位技术可以确定 dll 载入的基地址是多少。也就是说如果一个 dll 已经进行过重定位,就可以直接推算出它的IAT 应该填充哪些内容。如果一开始就将DLL导出符号的虚拟地址填充到这个IAT表的话,载入程序就不需要进行填充工作了,这可以加快应用程序的初始化速度。
④进行这种填充类似于将所需的 dll 与可执行文件绑定在一起,可执行文件的IAT 与 dll 的基地址和导出符号 RVA 一一对应,所以这种技术被称为模块绑定技术。
(2)Bind.exe工具
①工作原理:Bind的工作原理正如上面所写的那样,读取所有 dll 的基地址和RVA,将其填充到可执行文件的 Import Address Table 中
②Bind工作过程
A、它会打开指定的Exe映像文件的导入段
B、对导入段中列出的每个DLL,它会查看该DLL的文件头,来确定该DLL的首先基地址。
C、它会在DLL的导出段中查看每个符号。
D、它会取得符号的RVA,并将它与模块的首选基地址相加。并将计算得到的地址写入EXE的导入段的IAT表中。
E、在IAT表中除了写入符号的虚拟地址,还会添加一些额外的信息,如DLL模块的名称及各模块的时间戳。
③使用Bind的注意事项
A、当进程初始化的时候,所需的DLL应该都被载入到他们的首选基地址(这一点可使用前面介绍的Rebase工具来保证)
B、自从绑定完以后,DLL导出段中所引用的符号位置没有发生变化。加载程序会通过检查每个DLL的时间戳来验证这一点,这个时间戳是在前面提到的第5步中保存的。
C、何时使用 Bind.exe 进行模块绑定呢?因为不同的 Windows 版本系统 dll 可能会不同,所以针对不同版本的 Windows需要分别进行绑定,我们可以在应用程序的安装过程中来进行绑定。