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之后程序的加载时间加快了多少那就再好不过了:)

posted @ 2016-07-28 14:26  0giant  阅读(1999)  评论(0编辑  收藏  举报