dll的两种加载方式(pend)+ delayload
看过关于动态库的调用例子,于是决定动手做一做:
dll的对外接口声明头文件,Mydll.h:
//Mydll.h #include <stdio.h> #include <stdlib.h> #include "Public.h" #define DLL_EXPORT /*extern "c"*/ __declspec(dllexport) //导出 #define CUST_API _stdcall //标准调用 DLL_EXPORT void CUST_API DisplayVersion(TCHAR *Info); //显示版本 DLL_EXPORT int CUST_API Calc(int ia,int ib); //DLL_EXPORT int CUST_API MetiCalc(int ia,int ib); //新增加接口 //mydll.cpp #include "MyDll.h" void CUST_API DisplayVersion(TCHAR *Info) { wcscpy_s(Info,sizeof(VERSION),VERSION); //#define VERSION ver 1.0 return; } int CUST_API Calc(int ia,int ib) { return ia+ib; } int CUST_API MetiCalc(int ia,int ib) { return ia*ib; }
编译后,生成DllTest.lib 和 DllTest.dll
第一种方法:静态调用
理解:lib描述dll信息和函数入口地址,在编译时期加载到可执行程序中的。
若dll增加新API接口,新接口在使用时,必须要同时更新lib 才能使用,否则会找不到新接口函数的地址,由此可见,lib包含了描述dll 的接口描述信息。
//dlltest.h #include <iostream> #include <Windows.h> using namespace std; #pragma comment(lib,"..\\ApDll\\DllTest.lib") //加载lib库 #define DLL_EXPORT /*extern "c"*/ __declspec(dllexport) //导出 #define CUST_API _stdcall //标准调用 DLL_EXPORT void CUST_API DisplayVersion(TCHAR *Info); //dll中显示版本函数 DLL_EXPORT int CUST_API Calc(int ia,int ib); DLL_EXPORT int CUST_API MetiCalc(int ia,int ib); int _tmain(int argc, _TCHAR* argv[]) { TCHAR Version[50] = {0}; int a = 10,b=12; DisplayVersion(Version); wcout<<Version<<endl; wcout<<Calc(a,b)<<endl; } Result: ver 1.0 120
第二种方法:动态加载
首先,要定义指向动态库中所对外提供的函数类型,的函数指针。
函数指针定义的理解:
typedef void(_stdcall *FunName)(paramtypes 1,paramtypes 2);//定义指向调用类型为_stdcall,参数个数,类型如paramtypes1,paramtypes 2,返回值为void类型的函数指针
这里注意,定义函数指针时,返回值 (*pName)(参数),3个部分;
然后,LoadLibrary(Path); Path为dll所在路径,可以是system目录,也可以其他指定目录。加载成功之后会返回一个Hmodel模块句柄。
再利用这个模块句柄去,获取相应函数的地址。
函数指针调用时,不同于普通的指针,它不需要间接寻址,“*”;
用完dll之后要记得ReleaseLibrary() ;
#include <iostream> #include <Windows.h> using namespace std; typedef void (CUST_API *DisVer)(TCHAR *Info); typedef int (CUST_API *CalcOprt)(int ia,int ib); int _tmain(int argc, _TCHAR* argv[]) { TCHAR Version[50] = {0}; int a = 10,b=12; HMODULE hmodle = LoadLibrary(_T("..\\ApDll\\DllTest.dll")); //动态加载dll if(NULL == hmodle) { wcout<<"load dll failed!"<<endl; return -1; } DisVer displayVer = (DisVer)::GetProcAddress(hmodle,"DisplayVersion"); //根据模块地址,按找函数名,获取函数地址 DisplayVersion(Version); try { if (NULL == displayVer) { wcout<<_T("Load function error!")<<endl; } (displayVer)(Version); //用函数指针调用函数 wcout<<Version<<endl; } catch (...) { ; } system("pause"); return 0; }
看十,百遍,不如自己敲一遍,小小的动态库调用,也是有讲究的。
以下是delay load最基本的一些知识:
我们知道,dll的的载入有两种最基本的方法:隐式加载和显式加载。所谓隐式加载就是上一篇文章中介绍的方法,通过PE的输入表在进入入口函数之前将dll加载到内存空间。显式加载就是用LoadLibrary和GetProAddress的方法在需要的时候将dll加载到进程空间。这两种方法都是我们最常用的。那什么叫delay load呢?
被指定为delay load的dll只有当需要的时候才会真正载入进程空间,也就是说如果没有位于该dll中的函数被调用该dll将不被加载。而这个加载过程正是由LoadLibrary和GetProcAddress完成的,当然这一切对程序员是透明的。
这样做的好处是什么?毫无疑问,程序的启动速度快了,因为很多dll在启动的时候可能还没有使用到。甚至有些dll可能在整个生命周期中都未曾使用到,这样用delay load的话这个dll就不需占用任何内存。
那么如何使用delay load的呢?你需要做两件事情:
1. 在cpp的开头加上#pragma comment(lib, "DelayImp.lib"),如果你使用vs也可以在input里面加上这么一项。稍后会解释。
2. 在编译的时候加上:/link /DELAYLOAD:xxx.dll,如果使用vs只要在项目属性中找到delay load这一项,加上dll的名字。
(网上很多把#pragma comment(linker, "/DELAYLOAD:xxx.dll")加到cpp中,经证实,这种做法不可行。只能在命令行中作为参数使用)
现在让我利用现有的知识分析一下用delay load之后跟之前产生了什么变化:
1. 该dll相关的输入表肯定没了。毫无疑问,否则程序启动的时候还是会被无辜的加载。
2. 需要有什么字段记录dll的加载地址和函数的地址吧?否则每次调用都要LoadLibrary+GetProcAddress岂不是太不智能了?
3. 谁来调用LoadLibrary+GetProcAddress以及填充这些字段么?看来在链接的时候肯定要嵌入一下代码。那嵌入的代码哪里来?还记得之前的#pragma comment(lib, "DelayImp.lib")么?对了,就是这里来。
接下来我们就用一个最简单的例子来分析整个过程,让我再一次体会到了一个道理:作为程序员不学汇编真是寸步难行啊。在此之前我们再来回想一些PE结构中有什么跟delay load相关的东西么?对了!data directory中有一项IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT就是维护了所有跟delay load相关的信息。
我们先看一下对应的ImgDelayDescr结构体:
大小 | 成员 | 描述 |
DWORD | grAttrs | 这个结构的属性。目前唯一被定义的旗标是dlattrRva,表明这个结构中的字段应该被认为是RVA,而不是虚地址 |
RVA | rvaDLLName | 指向一个被输入的DLL的名称的RVA。这个字符串被传递给LoadLibrary |
RVA | rvaHmod | 指向一个HMODULE大小的内存位置的RVA。当延迟装入的DLL被装入内存后,它的模块句柄(hModule)被保存在这个地方 |
RVA | rvaIAT | 指向这个Dll的输入地址表的RVA,它与常规的IAT的格式相同 |
RVA | rvaINT | 指向这个DLL的输入名称表的RVA,它与常规的INT表格式相同 |
RVA | rvaBoundIAT | 可选的绑定IAT的RVA,指向这个DLL的输入地址表的绑定拷贝,它与常规的IAT表的格式相同,目前,这个IAT的拷贝并不是实际的绑定,但是这个特征可能会加到绑定程序的未来版本中 |
RVA | rvaUnloadIAT | 原始IAT的可选拷贝的RVA。它指向这个DLL的输入地址表的未绑定拷贝。它与常规的IAT表的格式相同,通常设为0 |
DWORD | dwTimeStamp | 延迟装入的输入DLL的时间/日期戳,通常设为0 |
看着是不是有点眼熟?对了,跟IMAGE_IMPORT_DESCRIPTOR有点像:都有dll name,INT,IAT,在继续往下看之前请确保对INT/IAT有基本的了解,没有还不是很清楚的话建议先看一下之前的一片文章。
到此为止,基本的知识介绍完毕,在给出我们的例子之前先介绍一下我使用的工具:ollydbg+stud_pe,ollydbg是一款强大的汇编级调试器,暂时属于摸索阶段,网上有不少教程,但是看懂这些教程本身需要一定的功力。stud_pe是一款很小的PE分析器,通过它可以很快的定位到PE的任意一部分,是初学PE的利器。接下来我的很多截图都源自这两个工具。
我们的例程尽量精简,又最好能覆盖所有的delay load知识:
当然您还需要一个delayLoad.dll,这个dll只需要导出两个函数export1+export2,函数的参数我们也省去了,加上不必要的参数只会增加汇编代码的复杂性,对我们的分析没有任何帮助。至于如何创建这个delayLoad.dll就不用我再具体说了吧,如果您还不会,建议你补补基础知识了哈~
编译+链接:cl sample.cpp /link /DELAYLOAD:delayload.dll
开始研究汇编之前我们先看一下sample.exe中ImgDelayDescr现在是什么情况:
我们先看一下最重要的几项(如何通过上面的virtual address获得文件中对应的内容不再介绍,参见上一篇文章):
rvaDLLName: 64 65 6C 61 79 4C 6F 61 64 2E 64 6C 6C (delayload.dll)
rvaIAT: 34 10 40 00 19 10 40 00(按照正常推理:这两项将用于保存函数地址)
rvaINT: 72 7A 00 00 68 7A 00 00
72 7A 00 00:00 00 65 78 70 6F 72 74 31(..export1)
68 7A 00 00:00 00 65 78 70 6F 72 74 32(..export2)
rvaINT与import table中的INT用法完全一样:每一项都指向了一个IMAGE_IMPORT_BY_NAME,前两个字节表示Hint(具体用途请查阅PE结构,对本无无用),后面的直接表示函数名,用ASCII码表示。
rvaIAT与import table中的IAT有点不同,import table的IAT在程序加载之前跟INT指向相同的内容,而这里确不是。另外import table的IAT在加载之前获得所有导入函数的地址并更新,而rvaIAT则会在函数被调用到的时候进行更新。那么在程序加载之前rvaIAT中的值是什么意义呢?别急,马上就知道了。
接下来用ollydbg打开sample.exe,找到入口函数,开始我们的体力活T_T:
00031000 55 PUSH EBP
00031001 8BEC MOV EBP,ESP
00031003 FF15 209B0300 CALL DWORD PTR DS:[39B20] //export1
00031009 FF15 209B0300 CALL DWORD PTR DS:[39B20] //export1
0003100F FF15 249B0300 CALL DWORD PTR DS:[39B24] //export2
00031015 33C0 XOR EAX,EAX
00031017 5D POP EBP
00031018 C3 RETN
00031019 B8 249B0300 MOV EAX,test.00039B24
0003101E E9 00000000 JMP test.00031023
00031023 51 PUSH ECX
00031024 52 PUSH EDX
00031025 50 PUSH EAX
00031026 68 1C7A0300 PUSH test.00037A1C
0003102B E8 0E000000 CALL test.0003103E
00031030 5A POP EDX
00031031 59 POP ECX
00031032 FFE0 JMP EAX
00031034 B8 209B0300 MOV EAX,test.00039B20
00031039 E9 E5FFFFFF JMP test.00031023
0003103E // 先不关心这里的代码
我们以第一个export1的调用为例:CALL DWORD PTR DS:[39B20]
39B20是什么?30000是加载地址(exe不是会加载到400000上么?为什么用ollydbg调试的时候会加载到30000的位置?不解...经测试ollydbg每次exe的加载地址都会随机变化),那么我们需要的其实是9B20,往上看看rvaIAT的值正是9B20!也就是rvaIAT中的第一项。我们之前的疑问马上要解开了,我们看到rvaIAT中第一项对应的数据是401034(1034),也就是说这个调用其实就是CALL 31034:
00051034 B8 209B0300 MOV EAX,test.00039B20
00051039 E9 E5FFFFFF JMP test.00031023
结合00031023中的代码我们已经可以推出如下结论:
1. 每一个rvaIAT项中保存了一个地址,该地址位于代码段中,由CALL DWORD PTR DS:[XXX]跳转进入到该代码段。(XXX是rvaIAT中某一项的地址)
2. 由CALL DWORD PTR DS:[XXX]跳转进入的代码段具有统一的格式:
1. 将该rvaIAT项的地址保存在EAX中。
2. 跳转到某一个地址(本程序中31023)
3. 在该地址中调用一个函数(本程序中3103E,代码暂时未给出,将在之后的介绍中详细分析),该函数将完成delay load的所有工作,并修改rvaIAT中的对应项使之拥有正确的函数地址。
4. 调用完该函数后JMP EAX,这个时候rvaIAT中对应的项已经有正确的函数地址了。
接下来我们重点研究3103E中的代码:
0003103E 8BFF MOV EDI,EDI 00031040 55 PUSH EBP 00031041 8BEC MOV EBP,ESP 00031043 83EC 44 SUB ESP,44 00031046 53 PUSH EBX 00031047 B8 00000300 MOV EAX,test.00030000 // EXE的加载地址 0003104C 56 PUSH ESI 0003104D 8B75 08 MOV ESI,DWORD PTR SS:[EBP+8] // EBP+8指向第二个参数(ESI:00037A1C ImgDelayDescr地址) 注:EBP+4指向返回地址 00031050 8B56 08 MOV EDX,DWORD PTR DS:[ESI+8] // ImgDelayDescr::rvaHmod(EDX:9B40) rvaHmod将来保存DLL加载地址 00031053 8B4E 04 MOV ECX,DWORD PTR DS:[ESI+4] // ImgDelayDescr::rvaDllName(ECX:6130) 00031056 8B5E 0C MOV EBX,DWORD PTR DS:[ESI+C] // ImgDelayDescr::rvaIAT(EBX:9B20) 00031059 03D0 ADD EDX,EAX // EDX: 39B40(rvaHmod) 0003105B 57 PUSH EDI 0003105C 8B7E 14 MOV EDI,DWORD PTR DS:[ESI+14] // ImgDelayDescr::rvaBoundIAT(EDI:7A7C) 0003105F 03F8 ADD EDI,EAX // EDI:37A7C(rvaBoundIAT) 00031061 03C8 ADD ECX,EAX // ECX:36130(rvaDllName) 00031063 8955 E8 MOV DWORD PTR SS:[EBP-18],EDX // EDX: 00031066 8B56 10 MOV EDX,DWORD PTR DS:[ESI+10] // ImgDelayDescr::rvaINT(EDX:7A5C) 00031069 03D8 ADD EBX,EAX // EBX: 39B20(rvaIAT) 0003106B 03D0 ADD EDX,EAX // EDX: 37A5C(rvaINT) 0003106D 8B46 1C MOV EAX,DWORD PTR DS:[ESI+1C] // ImgDelayDescr::dwTimeStamp(EAX:0) 00031070 8945 FC MOV DWORD PTR SS:[EBP-4],EAX // EAX: 0(dwTimeStamp) 00031073 8B45 0C MOV EAX,DWORD PTR SS:[EBP+C] // EBP+C指向第一个参数(EAX: 39B20) 该地址将来保存函数地址 00031076 894D C8 MOV DWORD PTR SS:[EBP-38],ECX // ECX: 36130(rvaDllName) 00031079 33C9 XOR ECX,ECX 0003107B 897D F4 MOV DWORD PTR SS:[EBP-C],EDI // EDI: 37A7C(rvaBoundIAT) 0003107E 8945 C4 MOV DWORD PTR SS:[EBP-3C],EAX // EAX: 39B20(函数地址保存位置) 00031081 33C0 XOR EAX,EAX 00031083 F706 01000000 TEST DWORD PTR DS:[ESI],1 // ZF=0 00031089 8D7D D0 LEA EDI,DWORD PTR SS:[EBP-30] // EDI=EBP-30=003EF9D8 0003108C C745 BC 24000>MOV DWORD PTR SS:[EBP-44],24 00031093 8975 C0 MOV DWORD PTR SS:[EBP-40],ESI // ESI: 37A1C(ImgDelayDescr) 00031096 894D CC MOV DWORD PTR SS:[EBP-34],ECX // ECX: 0 00031099 AB STOS DWORD PTR ES:[EDI] 0003109A 894D D4 MOV DWORD PTR SS:[EBP-2C],ECX 0003109D 894D D8 MOV DWORD PTR SS:[EBP-28],ECX 000310A0 894D DC MOV DWORD PTR SS:[EBP-24],ECX 000310A3 75 1F JNZ SHORT test.000310C4 // JMP 000310C4 因为grAttrs=1 ///////////////////////////////////////////////////////////////////////////////////// // 如果grAttrs==0,引发一个异常并JMP 00031272结束这段子程,也就是目前为止微软只支持RVA之中方式 // RaiseExcep的最后一个参数在做什么?看这个代码似乎不是很明朗...回头再看看能不能解决... 000310A5 8D45 BC LEA EAX,DWORD PTR SS:[EBP-44] 000310A8 8945 0C MOV DWORD PTR SS:[EBP+C],EAX 000310AB 8D45 0C LEA EAX,DWORD PTR SS:[EBP+C] 000310AE 50 PUSH EAX // parguments 000310AF 6A 01 PUSH 1 // nNumberOfarguments = 1 000310B1 51 PUSH ECX // dwExceptionFlags = 0(允许过滤器返回EXCEPTION_CONTINUE_EXECUTION) 000310B2 68 57006DC0 PUSH C06D0057 // dwExceptionCode = C06D0057 000310B7 FF15 18600300 CALL DWORD PTR DS:[<&KERNEL32.RaiseExcep>; 000310BD 33C0 XOR EAX,EAX 000310BF E9 AE010000 JMP test.00031272 ///////////////////////////////////////////////////////////////////////////////////// 000310C4 8B45 E8 MOV EAX,DWORD PTR SS:[EBP-18] // EAX: 39B40(rvaHmod) 000310C7 8B38 MOV EDI,DWORD PTR DS:[EAX] // EDI: 0(现在rvaHmod的值是0,表示此DLL还未加载过) 000310C9 8B45 0C MOV EAX,DWORD PTR SS:[EBP+C] // EAX: 39B20(参数一:最终函数的保存地址) 000310CC 2BC3 SUB EAX,EBX // 该函数的保存地址 - rvaIAT首地址 = 该函数在rvaIAT中的偏移,因为export1是第一项,所以为0 000310CE 83E0 FC AND EAX,FFFFFFFC // 4的倍数 000310D1 8B1402 MOV EDX,DWORD PTR DS:[EDX+EAX] // EDX: 7A72(通过偏移找到对应rvaINT中位置,并读取该位置的值,通过该值可以找到需要的函数名) 000310D4 8945 08 MOV DWORD PTR SS:[EBP+8],EAX // EAX: 0(偏移) 000310D7 8BC2 MOV EAX,EDX // EAX: 7A72(export1对应的IMAGE_IMPORT_BY_NAME的地址) 000310D9 C1E8 1F SHR EAX,1F 000310DC F7D0 NOT EAX 000310DE 83E0 01 AND EAX,1 000310E1 8945 CC MOV DWORD PTR SS:[EBP-34],EAX // 此时EAX=1 000310E4 8D82 02000300 LEA EAX,DWORD PTR DS:[EDX+30002] // EAX=EDX+30002=00037A74(此处正是除去Hint之后export1函数名的地址!) 000310EA 75 03 JNZ SHORT test.000310EF // JMP 000310EF 000310EC 0FB7C2 MOVZX EAX,DX 000310EF 8945 D0 MOV DWORD PTR SS:[EBP-30],EAX // EAX: 00037A74(export1函数名地址) 000310F2 A1 4C9B0300 MOV EAX,DWORD PTR DS:[39B4C] // 39B4C: 这个是什么地址?EAX: 0 000310F7 33DB XOR EBX,EBX 000310F9 3BC1 CMP EAX,ECX // CMP EAX, 0 000310FB 74 11 JE SHORT test.0003110E // JMP 0003110E ///////////////////////////////////////////////////////////////////////////////////// // 如果39B4C这个地址不是0,那就表示一个函数地址,用0,EDX(EBP-44=3EF9C4)分别作为两个参数传入 // 如果该函数的返回值不是0,JMP 00031255 000310FD 8D55 BC LEA EDX,DWORD PTR SS:[EBP-44] 00031100 52 PUSH EDX 00031101 51 PUSH ECX 00031102 FFD0 CALL EAX 00031104 8BD8 MOV EBX,EAX 00031106 85DB TEST EBX,EBX 00031108 0F85 47010000 JNZ test.00031255 ///////////////////////////////////////////////////////////////////////////////////// 0003110E 85FF TEST EDI,EDI // EDI: 0(DLL的加载地址) ###如果不是第一次调用该DLL中的函数,那就!=0,JMP 000311B8### 00031110 0F85 A2000000 JNZ test.000311B8 // No JMP. 因为DLL未加载过,先要LoadLibrary. ###如果DLL已经加载,JMP有效### 00031116 A1 4C9B0300 MOV EAX,DWORD PTR DS:[39B4C] // 39B4C: 这个是什么地址?EAX: 0 0003111B 85C0 TEST EAX,EAX 0003111D 74 0E JE SHORT test.0003112D // JMP 0003112D 因为EAX=0 ///////////////////////////////////////////////////////////////////////////////////// // 如果39B4C这个地址不是0,那就表示一个函数地址,用1,ECX(EBP-44=3EF9C4)分别作为两个参数传入 // 如果该函数的返回值不是0,JMP 0003117D(也就是调用InterlockedExchange的地方) 0003111F 8D4D BC LEA ECX,DWORD PTR SS:[EBP-44] 00031122 51 PUSH ECX 00031123 6A 01 PUSH 1 00031125 FFD0 CALL EAX 00031127 8BF8 MOV EDI,EAX 00031129 85FF TEST EDI,EDI 0003112B 75 50 JNZ SHORT test.0003117D ///////////////////////////////////////////////////////////////////////////////////// 0003112D FF75 C8 PUSH DWORD PTR SS:[EBP-38] // rvaDLLName: delayLoad.dll 00031130 FF15 14600300 CALL DWORD PTR DS:[<&KERNEL32.LoadLibrar>; // LoadLibraryA 00031136 8BF8 MOV EDI,EAX // EDI:6EE40000(DLL加载地址) 00031138 85FF TEST EDI,EDI 0003113A 75 41 JNZ SHORT test.0003117D // JMP 0003117D ///////////////////////////////////////////////////////////////////////////////////// // 如果LoadLibrary失败, 调用一系列的处理函数(GetLastError/RaiseException)并返回. 0003113C FF15 10600300 CALL DWORD PTR DS:[<&KERNEL32.GetLastErr>; [GetLastError 00031142 8945 DC MOV DWORD PTR SS:[EBP-24],EAX 00031145 A1 489B0300 MOV EAX,DWORD PTR DS:[39B48] 0003114A 85C0 TEST EAX,EAX 0003114C 74 0E JE SHORT test.0003115C 0003114E 8D4D BC LEA ECX,DWORD PTR SS:[EBP-44] 00031151 51 PUSH ECX 00031152 6A 03 PUSH 3 00031154 FFD0 CALL EAX 00031156 8BF8 MOV EDI,EAX 00031158 85FF TEST EDI,EDI 0003115A 75 21 JNZ SHORT test.0003117D 0003115C 8D45 BC LEA EAX,DWORD PTR SS:[EBP-44] 0003115F 8945 0C MOV DWORD PTR SS:[EBP+C],EAX 00031162 8D45 0C LEA EAX,DWORD PTR SS:[EBP+C] 00031165 50 PUSH EAX ; /pArguments 00031166 6A 01 PUSH 1 ; |nArguments = 1 00031168 6A 00 PUSH 0 ; |ExceptionFlags = EXCEPTION_CONTINUABLE 0003116A 68 7E006DC0 PUSH C06D007E ; |ExceptionCode = C06D007E 0003116F FF15 18600300 CALL DWORD PTR DS:[<&KERNEL32.RaiseExcep>; /RaiseException 00031175 8B45 D8 MOV EAX,DWORD PTR SS:[EBP-28] 00031178 E9 F5000000 JMP test.00031272 ///////////////////////////////////////////////////////////////////////////////////// 0003117D 57 PUSH EDI // InterlockedExchange参数2:6EE40000(DLL加载地址) 0003117E FF75 E8 PUSH DWORD PTR SS:[EBP-18] // InterlockedExchange参数1:0 00031181 FF15 0C600300 CALL DWORD PTR DS:[<&KERNEL32.Interlocke>; 00031187 3BC7 CMP EAX,EDI // 比较原来DLL加载地址中存放的值和通过LoadLibrary获得的值 00031189 74 26 JE SHORT test.000311B1 // No JMP 因为两个值不一样 0003118B 837E 18 00 CMP DWORD PTR DS:[ESI+18],0 // ImgDelayDescr::rvaUnloadIAT(0) 0003118F 74 27 JE SHORT test.000311B8 // JMP 000311B8 ///////////////////////////////////////////////////////////////////////////////////// // 这里暂时不知道在做什么... 00031191 6A 08 PUSH 8 00031193 6A 40 PUSH 40 00031195 FF15 00600300 CALL DWORD PTR DS:[<&KERNEL32.LocalAlloc> 0003119B 85C0 TEST EAX,EAX 0003119D 74 19 JE SHORT test.000311B8 0003119F 8970 04 MOV DWORD PTR DS:[EAX+4],ESI 000311A2 8B0D 449B0300 MOV ECX,DWORD PTR DS:[39B44] 000311A8 8908 MOV DWORD PTR DS:[EAX],ECX 000311AA A3 449B0300 MOV DWORD PTR DS:[39B44],EAX 000311AF EB 07 JMP SHORT test.000311B8 000311B1 57 PUSH EDI 000311B2 FF15 08600300 CALL DWORD PTR DS:[<&KERNEL32.FreeLibrar> ///////////////////////////////////////////////////////////////////////////////////// 000311B8 A1 4C9B0300 MOV EAX,DWORD PTR DS:[39B4C] // 39B4C: 这个是什么地址?EAX: 0 ###如果DLL加载过,直接JMP到这里### 000311BD 897D D4 MOV DWORD PTR SS:[EBP-2C],EDI // EDI: 6EE40000(DLL的加载地址) 000311C0 85C0 TEST EAX,EAX 000311C2 74 0A JE SHORT test.000311CE // JMP 000311CE ///////////////////////////////////////////////////////////////////////////////////// // 如果39B4C这个地址不是0,那就表示一个函数地址,用2,ECX(EBP-44=3EF9C4)分别作为两个参数传入 000311C4 8D4D BC LEA ECX,DWORD PTR SS:[EBP-44] 000311C7 51 PUSH ECX 000311C8 6A 02 PUSH 2 000311CA FFD0 CALL EAX 000311CC 8BD8 MOV EBX,EAX ///////////////////////////////////////////////////////////////////////////////////// 000311CE 85DB TEST EBX,EBX // EBX: 0 000311D0 75 7E JNZ SHORT test.00031250 // No JMP 000311D2 395E 14 CMP DWORD PTR DS:[ESI+14],EBX // CMP rvaBoundIAT, EBX 000311D5 74 2D JE SHORT test.00031204 // No JMP 000311D7 395E 1C CMP DWORD PTR DS:[ESI+1C],EBX // CMP dwTimeStamp, EBX 000311DA 74 28 JE SHORT test.00031204 // JMP 00031204 000311DC 8B47 3C MOV EAX,DWORD PTR DS:[EDI+3C] // 接下来一串的条件跳转,暂时跳过... 000311DF 813C38 504500>CMP DWORD PTR DS:[EAX+EDI],4550 000311E6 75 1C JNZ SHORT test.00031204 000311E8 8B4D FC MOV ECX,DWORD PTR SS:[EBP-4] 000311EB 394C38 08 CMP DWORD PTR DS:[EAX+EDI+8],ECX 000311EF 75 13 JNZ SHORT test.00031204 000311F1 3B7C38 34 CMP EDI,DWORD PTR DS:[EAX+EDI+34] 000311F5 75 0D JNZ SHORT test.00031204 000311F7 8B45 F4 MOV EAX,DWORD PTR SS:[EBP-C] 000311FA 8B4D 08 MOV ECX,DWORD PTR SS:[EBP+8] 000311FD 8B1C01 MOV EBX,DWORD PTR DS:[ECX+EAX] 00031200 85DB TEST EBX,EBX 00031202 75 4C JNZ SHORT test.00031250 00031204 FF75 D0 PUSH DWORD PTR SS:[EBP-30] // GetProcAdd参数2:37A74: export1的函数名 00031207 57 PUSH EDI // GetProcAdd参数1:DLL加载地址 00031208 FF15 04600300 CALL DWORD PTR DS:[<&KERNEL32.GetProcAdd> 0003120E 8BD8 MOV EBX,EAX // EBX: 6EE41000(export1在进程中的实际地址) 00031210 85DB TEST EBX,EBX 00031212 75 3C JNZ SHORT test.00031250 // JMP 00031250 因为GetProcAdd成功 ///////////////////////////////////////////////////////////////////////////////////// // 如果GetProcAdd失败, 调用一系列的处理函数(GetLastError/RaiseException)并返回. 00031214 FF15 10600300 CALL DWORD PTR DS:[<&KERNEL32.GetLastErr> 0003121A 8945 DC MOV DWORD PTR SS:[EBP-24],EAX 0003121D A1 489B0300 MOV EAX,DWORD PTR DS:[39B48] 00031222 85C0 TEST EAX,EAX 00031224 74 0E JE SHORT test.00031234 00031226 8D4D BC LEA ECX,DWORD PTR SS:[EBP-44] 00031229 51 PUSH ECX 0003122A 6A 04 PUSH 4 0003122C FFD0 CALL EAX 0003122E 8BD8 MOV EBX,EAX 00031230 85DB TEST EBX,EBX 00031232 75 1C JNZ SHORT test.00031250 00031234 8D45 BC LEA EAX,DWORD PTR SS:[EBP-44] 00031237 8945 08 MOV DWORD PTR SS:[EBP+8],EAX 0003123A 8D45 08 LEA EAX,DWORD PTR SS:[EBP+8] 0003123D 50 PUSH EAX 0003123E 6A 01 PUSH 1 00031240 6A 00 PUSH 0 00031242 68 7F006DC0 PUSH C06D007F 00031247 FF15 18600300 CALL DWORD PTR DS:[<&KERNEL32.RaiseExcep> 0003124D 8B5D D8 MOV EBX,DWORD PTR SS:[EBP-28] ///////////////////////////////////////////////////////////////////////////////////// 00031250 8B45 0C MOV EAX,DWORD PTR SS:[EBP+C] // EAX: 39B20(第一个参数也就是函数地址即将储存的位置) 00031253 8918 MOV DWORD PTR DS:[EAX],EBX // EBX: 6EE41000(export1在进程中的实际地址) 终于完成了我们期待的一步! 00031255 A1 4C9B0300 MOV EAX,DWORD PTR DS:[39B4C] // 39B4C: 这个是什么地址?EAX: 0 0003125A 85C0 TEST EAX,EAX 0003125C 74 12 JE SHORT test.00031270 // JMP 00031270 ///////////////////////////////////////////////////////////////////////////////////// // 如果39B4C这个地址不是0,那就表示一个函数地址,用5,ECX(EBP-44=3EF9C4)分别作为两个参数传入 0003125E 8365 DC 00 AND DWORD PTR SS:[EBP-24],0 00031262 8D4D BC LEA ECX,DWORD PTR SS:[EBP-44] 00031265 51 PUSH ECX 00031266 6A 05 PUSH 5 00031268 897D D4 MOV DWORD PTR SS:[EBP-2C],EDI 0003126B 895D D8 MOV DWORD PTR SS:[EBP-28],EBX 0003126E FFD0 CALL EAX ///////////////////////////////////////////////////////////////////////////////////// 00031270 8BC3 MOV EAX,EBX // EBX: 6EE41000(export1在进程中的实际地址) 00031272 5F POP EDI 00031273 5E POP ESI 00031274 5B POP EBX 00031275 C9 LEAVE 00031276 C2 0800 RETN 8
以上基本是针对第一次调用DLL中的函数的情况。注释已经写的很清楚了,虽然还有不少地方没有彻底搞清楚,但是核心的部分已经一目了然。正当我打算继续研究未明白的代码时,突然发现原来微软提供了这部分的源代码T_T: (delayhlp.cpp)
extern "C" FARPROC WINAPI __delayLoadHelper2(PCImgDelayDescr pidd, FARPROC* ppfnIATEntry) { // Set up some data we use for the hook procs but also useful for // our own use // InternalImgDelayDescr idd = { pidd->grAttrs, PFromRva<LPCSTR>(pidd->rvaDLLName), PFromRva<HMODULE*>(pidd->rvaHmod), PFromRva<PImgThunkData>(pidd->rvaIAT), PFromRva<PCImgThunkData>(pidd->rvaINT), PFromRva<PCImgThunkData>(pidd->rvaBoundIAT), PFromRva<PCImgThunkData>(pidd->rvaUnloadIAT), pidd->dwTimeStamp }; DelayLoadInfo dli = { sizeof DelayLoadInfo, pidd, ppfnIATEntry, idd.szName, { 0 }, 0, 0, 0 }; if (0 == (idd.grAttrs & dlattrRva)) { PDelayLoadInfo rgpdli[1] = { &dli }; RaiseException( VcppException(ERROR_SEVERITY_ERROR, ERROR_INVALID_PARAMETER), 0, 1, PULONG_PTR(rgpdli) ); return 0; } HMODULE hmod = *idd.phmod; // Calculate the index for the IAT entry in the import address table // N.B. The INT entries are ordered the same as the IAT entries so // the calculation can be done on the IAT side. // const unsigned iIAT = IndexFromPImgThunkData(PCImgThunkData(ppfnIATEntry), idd.pIAT); const unsigned iINT = iIAT; PCImgThunkData pitd = &(idd.pINT[iINT]); dli.dlp.fImportByName = !IMAGE_SNAP_BY_ORDINAL(pitd->u1.Ordinal); if (dli.dlp.fImportByName) dli.dlp.szProcName = LPCSTR(PFromRva<PIMAGE_IMPORT_BY_NAME>(RVA(UINT_PTR(pitd->u1.AddressOfData)))->Name); else dli.dlp.dwOrdinal = DWORD(IMAGE_ORDINAL(pitd->u1.Ordinal)); // Call the initial hook. If it exists and returns a function pointer, // abort the rest of the processing and just return it for the call. // FARPROC pfnRet = NULL; if (__pfnDliNotifyHook2) { pfnRet = ((*__pfnDliNotifyHook2)(dliStartProcessing, &dli)); if (pfnRet != NULL) { goto HookBypass; } } // Check to see if we need to try to load the library. // if (hmod == 0) { if (__pfnDliNotifyHook2) { hmod = HMODULE(((*__pfnDliNotifyHook2)(dliNotePreLoadLibrary, &dli))); } if (hmod == 0) { hmod = ::LoadLibrary(dli.szDll); } if (hmod == 0) { dli.dwLastError = ::GetLastError(); if (__pfnDliFailureHook2) { // when the hook is called on LoadLibrary failure, it will // return 0 for failure and an hmod for the lib if it fixed // the problem. // hmod = HMODULE((*__pfnDliFailureHook2)(dliFailLoadLib, &dli)); } if (hmod == 0) { PDelayLoadInfo rgpdli[1] = { &dli }; RaiseException( VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND), 0, 1, PULONG_PTR(rgpdli) ); // If we get to here, we blindly assume that the handler of the exception // has magically fixed everything up and left the function pointer in // dli.pfnCur. // return dli.pfnCur; } } // Store the library handle. If it is already there, we infer // that another thread got there first, and we need to do a // FreeLibrary() to reduce the refcount // HMODULE hmodT = HMODULE(InterlockedExchangePointer((PVOID *) idd.phmod, PVOID(hmod))); if (hmodT != hmod) { // add lib to unload list if we have unload data if (pidd->rvaUnloadIAT) { // suppress prefast warning 6014, the object is saved in a link list in the constructor of ULI #pragma warning(suppress:6014) new ULI(pidd); } } else { ::FreeLibrary(hmod); } } // Go for the procedure now. // dli.hmodCur = hmod; if (__pfnDliNotifyHook2) { pfnRet = (*__pfnDliNotifyHook2)(dliNotePreGetProcAddress, &dli); } if (pfnRet == 0) { if (pidd->rvaBoundIAT && pidd->dwTimeStamp) { // bound imports exist...check the timestamp from the target image // PIMAGE_NT_HEADERS pinh(PinhFromImageBase(hmod)); if (pinh->Signature == IMAGE_NT_SIGNATURE && TimeStampOfImage(pinh) == idd.dwTimeStamp && FLoadedAtPreferredAddress(pinh, hmod)) { // Everything is good to go, if we have a decent address // in the bound IAT! // pfnRet = FARPROC(UINT_PTR(idd.pBoundIAT[iIAT].u1.Function)); if (pfnRet != 0) { goto SetEntryHookBypass; } } } pfnRet = ::GetProcAddress(hmod, dli.dlp.szProcName); } if (pfnRet == 0) { dli.dwLastError = ::GetLastError(); if (__pfnDliFailureHook2) { // when the hook is called on GetProcAddress failure, it will // return 0 on failure and a valid proc address on success // pfnRet = (*__pfnDliFailureHook2)(dliFailGetProc, &dli); } if (pfnRet == 0) { PDelayLoadInfo rgpdli[1] = { &dli }; RaiseException( VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND), 0, 1, PULONG_PTR(rgpdli) ); // If we get to here, we blindly assume that the handler of the exception // has magically fixed everything up and left the function pointer in // dli.pfnCur. // pfnRet = dli.pfnCur; } } SetEntryHookBypass: *ppfnIATEntry = pfnRet; HookBypass: if (__pfnDliNotifyHook2) { dli.dwLastError = 0; dli.hmodCur = hmod; dli.pfnCur = pfnRet; (*__pfnDliNotifyHook2)(dliNoteEndProcessing, &dli); } return pfnRet; }
对比汇编,我们看看我们得到了什么新的信息:
1. RaiseException的最后一个参数指向了一个DelayLoadInfo. 可以在异常过滤器中获取相应的信息.
2. 之前一直很困惑我们的39B4C和终于知道什么用途了!是系统提供给我们的一个Hook:__pfnDliNotifyHook2,我们可以在自己的代码中定义这个函数,由系统在特定的时候调用。与此同时系统还提供了一个Hook:__pfnDliFailureHook2,在汇编代码中对应的是39B48. 这两个函数的用途将在后面介绍。
3. 还记得000311DC附近一系列的条件跳转么?这个是用来判断是否存在绑定信息的,如果一切正常的话就直接用绑定的地址,不需要GetProcessAdress了。为了确保这个绑定的地址还能正确使用,需要进行一系列的条件判断:
1. rvaBoundIAT & dwTimeStamp都不为0
2. IMAGE_NT_SIGNATURE & 时间戳一样 & 加载地址跟首选加载地址一致
那么绑定的加载地址哪里来?记得rvaBoundIAT么?这里分析汇编比cpp更简单: MOV EBX,DWORD PTR DS:[ECX+EAX],其中EAX是rvaBouldIAT的地址,ECX是偏移(该函数存放地址到rvaIAT首地址的偏移)
4. 如果DLL加载失败或者函数地址寻找失败,程序不会崩溃,而是会引发异常供开发者处理。
5. 如果该DLL可能会被unload(使用__FUnloadDelayLoadedDLL2),那么我们需要准备一些数据结构:new ULI(pidd);
到现在为止,基本上我们已经非常清楚delay load的工作原理的,那么再让我们思考一下当第二次调用export1时发生了什么事情呢?还是调用CALL DWORD PTR DS:[39B20],但是此时39B20已经存放了正确的export1地址,以后再使用到这个函数的话就可以直接使用了!
再从头回顾一下,还有没有什么内容没有介绍到:
1. __pfnDliNotifyHook2 & __pfnDliFailureHook2
2. unload...
3. 如何让绑定工作起来?显然在这个例子中没有绑定。
接下来的工作我们针对上面的三个方面一一介绍:
__pfnDliNotifyHook2 & __pfnDliFailureHook2
FARPROC WINAPI delayHook(unsigned dliNotify, PDelayLoadInfo pdli) { switch (dliNotify) { case dliStartProcessing : // If you want to return control to the helper, return 0. // Otherwise, return a pointer to a FARPROC helper function // that will be used instead, thereby bypassing the rest // of the helper. break; case dliNotePreLoadLibrary : // If you want to return control to the helper, return 0. // Otherwise, return your own HMODULE to be used by the // helper instead of having it call LoadLibrary itself. break; case dliNotePreGetProcAddress : // If you want to return control to the helper, return 0. // If you choose you may supply your own FARPROC function // address and bypass the helper's call to GetProcAddress. break; case dliFailLoadLib : // LoadLibrary failed. // If you don't want to handle this failure yourself, return 0. // In this case the helper will raise an exception // (ERROR_MOD_NOT_FOUND) and exit. // If you want to handle the failure by loading an alternate // DLL (for example), then return the HMODULE for // the alternate DLL. The helper will continue execution with // this alternate DLL and attempt to find the // requested entrypoint via GetProcAddress. break; case dliFailGetProc : // GetProcAddress failed. // If you don't want to handle this failure yourself, return 0. // In this case the helper will raise an exception // (ERROR_PROC_NOT_FOUND) and exit. // If you choose you may handle the failure by returning // an alternate FARPROC function address. break; case dliNoteEndProcessing : // This notification is called after all processing is done. // There is no opportunity for modifying the helper's behavior // at this point except by longjmp()/throw()/RaiseException. // No return value is processed. break; default : return NULL; } return NULL; } /* and then at global scope somewhere PfnDliHook __pfnDliNotifyHook2 = delayHook; */
上面的例子再清楚不过,接下来我们结合__delayLoadHelper2的实现看看我们能为delayHook自定义什么行为:
1. dliStartProcessing: 如果在这里就获得了函数地址,直接跳到__delayLoadHelper2的最后。
2. dliNotePreLoadLibrary: LoadLibrary之前. 这个时候我们可以自己找到DLL的地址并返回,如果返回0,由__delayLoadHelper2调用LoadLibrary.
3. dliNotePreGetProcAddress: 在收到这个flag的时候我们可以自己获得函数地址. 如果返回0,则由__delayLoadHelper2负责.
4. dliNoteEndProcessing: 所有操作都结束了准备从__delayLoadHelper2返回。
__pfnDliFailureHook2的用法相似:
1. dliFailLoadLib: 当__delayLoadHelper2调用LoadLibrary出错的时候. 再这里我们可以继续尝试Load这个DLL或者做一些错误处理。
2. dliFailGetProc: 当__delayLoadHelper2调用GetProcAddress失败. 同理.
UNLOAD
默认情况下延迟加载的DLL不具备unload功能. 什么意思呢?
1. FreeLibrary无论如何不能用. 因为FreeLibrary不会清理函数地址. 当下一次调用该DLL中的函数的可以就会导致异常访问。
2. 既然不能FreeLibrary那也没办法unload的了。默认情况下就是这个样子的。
当然微软不会这样傻,你可以在delayhlp.cpp中找到一个名为__FUnloadDelayLoadedDLL2的函数,就是专门用来unload延迟加载的DLL的,但是要把它加到自己的程序中需要一个链接开关:/delay:unload. 如果没有设定这个开关,那么调用__FUnloadDelayLoadedDLL2什么也不会做。除此之外当然要加上#include<delayimp.h>&#include<windows.h>保证编译能够通过。
我们再来看一下__FUnloadDelayLoadedDLL2做了什么?
extern "C" BOOL WINAPI __FUnloadDelayLoadedDLL2(LPCSTR szDll) { BOOL fRet = FALSE; PUnloadInfo pui = __puiHead; for (pui = __puiHead; pui; pui = pui->puiNext) { LPCSTR szName = PFromRva<LPCSTR>(pui->pidd->rvaDLLName); size_t cbName = __strlen(szName); // Intentionally case sensitive to avoid complication of using the CRT // for those that don't use the CRT...the user can replace this with // a variant of a case insenstive comparison routine // if (cbName == __strlen(szDll) && __memcmp(szDll, szName, cbName) == 0) { break; } } if (pui && pui->pidd->rvaUnloadIAT) { PCImgDelayDescr pidd = pui->pidd; HMODULE * phmod = PFromRva<HMODULE*>(pidd->rvaHmod); HMODULE hmod = *phmod; OverlayIAT( PFromRva<PImgThunkData>(pidd->rvaIAT), PFromRva<PCImgThunkData>(pidd->rvaUnloadIAT) ); ::FreeLibrary(hmod); *phmod = NULL; delete reinterpret_cast<ULI*> (pui); fRet = TRUE; } return fRet; }
1. 遍历所有的ImgDelayDescr, 找到相同名字的DLL对应的ImgDelayDescr
2. 如果该ImgDelayDescr对应的rvaUnloadIAT不为0,那么将rvaUnloadIAT中的数据覆盖rvaIAT中的。
提出一个小问题:rvaUnloadIAT存放了什么?有兴趣的读者可以自己尝试一下,其实不需要尝试我们也应该可以想明白。因为unload之后我们还是可以调用该DLL中的函数进行延迟加载,那么覆盖之后的rvaIAT必须和初始时(从未调用过该DLL中的函数)rvaIAT中的数据一致。也就是说,在程序未加载之前,rvaUnloadIAT中存放了一份rvaIAT的拷贝。
绑定
关于绑定,还有一大堆的内容可以介绍。因为不是本文的重点,我们就简单的介绍一下:
我们知道,一般情况下,从一个dll导入一个函数的话这个函数的地址是在加载时获得并填入IAT中的。这样势必导致加载时间变长。绑定所要做的事情就是将这个工作提前。那么就有一个问题了,dll加载的地址是不定的,如何得到正确的函数地址呢?其实绑定有个前提条件,就是dll的加载地址一定要跟PE中定义的加载地址一致,绑定才会有效。否则,还是会在加载的时候通过INT重新获得函数名及函数地址。除此之外,还需要做一系列的判断,比如dll的时间戳,因为重新编译过后的dll地址可能都变了,之前的绑定也是无效的。
那么如何绑定呢?微软提供了一个名为bind的工具。用法如下BIND -u sample.exe delayLoad.dll
运行bind的命令后我们可以看到data directory中有一项变化了:IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT. 如果不是延迟加载的dll相信运行这个命令以后绑定就启作用了。可惜的是在这个例子中因为使用了delay load, 我尝试着使用BIND进行绑定,却没有成功(ImgDelayDescr中的时间戳没有改变,所以在比较时间戳的时候失败了,结果还是通过GetProcAddress取得了函数地址)。具体原因不知道,暂时先告一个段落。
说到加载地址,还有一个工具不得不提,就是rebase, rebase的作用就是调整dll的首选加载地址,使得每个dll都能加载到首选地址上,这样就达到了一定程度的优化。通常微软建议的做法是先运行rebase再运行bind,这样能保证bind后都是有效地。这里相关的内容还有不少,有兴趣的读者可以自己再找找资料研究一下,如果能再写个程序测试一下bind, rebase之后程序的加载时间加快了多少那就再好不过了:)