一. 简介
延迟加载Dll是一个混合方式,它通过LoadLirary和GetProcAddress获得延迟加载函数的地址,然后直接转向对延迟加载函数的调用。
延迟加载不是操作系统的一个特征,它完全是通过链接器和运行库加入额外的代码和数据来实现的。同样地,无法在WINNT.H里找到关于延迟加载的更多参考,不过,可以在延迟装入数据(Delayload Data)和常规的输入数据之间看到一定的相似之处。
Datadirectory数组中的IMAGE_DIRECTORY_ENTRY_IMPORT条目指向延迟加载的数据。这是一个指向ImgDelayDescr结构数组的RVA,这个结构定义在VisualC++的DelayImp.h中,表1.1说明了它的内容。每一个被延迟加载的Dll都对应一个ImgDelayDescr结构。
表1.1 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 |
ImgDelayDescr结构的关键在于它包括了对应了DLL的IAT和INT的地址,这些表在格式上与常规的输入表是相同的,唯一的区别是他们有运行库代码而不是由操作系统进行写入和读出。当第一次从一个延迟装入的DLL中调用一个API函数时,运行库调用LoadLibrary(如果需要),然后是GetProcAddress,最后得到的地址被存在延迟加载的IAT表中。这样以后每次调用这个API时都会直接到这里来。
在Visual C++6.0 中有延迟装入数据最初的原型。ImgDelayDescr中所有包含地址的域均是虚拟地址,而不是RVA。换句话说,它们包含延迟装入数据所在位置的实际地址。这些域是双字,是x86上一个指针的大小。现在向IA-64的快速移植正在被支持。很显然4个字节不足以装下一个完整的地址,在这一点上,微软做了件正确的事情,已将包含地址的字段改为RVA。表1-10中已经使用修正的结构定义和名称。
关于在ImgDelayDescr结构中使用RVA还是虚拟地址,现在仍有争论,在这个结构中有一个字段是旗标值。当grAttrs字段被设为1时,结构成员被当做RVA。这是惟一与Visual Studio.net和64位编译器一起出现的选项。如果在grAttrs中的这个位关掉,ImgDelayDescr字段将是虚拟地址。
二. 逆向分析
这里以urlmon.dll模块首次调用被设置为延迟加载的Wininet.dll文件中的InternetReadFile函数为例
BOOL InternetReadFile( HINTERNET hFile, LPVOID lpBuffer, DWORDdwNumberOfBytesToRead, LPDWORD lpdwNumberOfBytesRead );
Parameters
hFile [in] Handle returned from a previous call to InternetOpenUrl, FtpOpenFile,GopherOpenFile, or HttpOpenRequest.
lpBuffer [out] Pointer to a buffer that receives the data.
dwNumberOfBytesToRead [in] Number of bytes to be read.
lpdwNumberOfBytesRead [out] Pointer to a variable that receives the number of bytes read. InternetReadFile sets this value to zero before doing any work or error checking.
第一次调用被延迟加载的HttpReadfile函数:
75C7BBAE 50 push eax ;参数lpdwNumberOfBytesRead [out]
75C7BBAF 8B45 08 mov eax, dword ptr [ebp+8]
75C7BBB2 51 push ecx ;参数dwNumberOfBytesToRead [in]
75C7BBB3 03C7 add eax, edi
75C7BBB5 50 push eax ;参数lpBuffer [out]
75C7BBB6 FF76 6C push dword ptr [esi+6C] ;参数hFile
75C7BBB9 33DB xor ebx, ebx
75C7BBBB 895D FC mov dword ptr [ebp-4], ebx
75C7BBBE FF15 3020CD75 call dword ptr [75CD2030] ; InternetReadFile
(图一)
其中地址75CD2030 中保存的是一个函数指针,它指向0x75c7bb08,如图(二)
75CD2030 08 BB C7 75 47 8A 6A 76 A4 68 CC 75 B3 68 CC 75 磺uG妀v蘵砲蘵
75CD2040 C2 68 CC 75 B4 90 6A 76 DA 58 69 76 D1 68 CC 75 耯蘵磹jv赬iv裩蘵
75CD2050 FC 73 69 76 CD 36 69 76 38 71 68 76 E0 68 CC 75 黶iv?iv8qhv鄅蘵
75CD2060 EF 68 CC 75 FE 68 CC 75 0D 69 CC 75 1C 69 CC 75 飄蘵蘵.i蘵i蘵
75CD2070 2B 69 CC 75 3A 69 CC 75 49 69 CC 75 58 69 CC 75 +i蘵:i蘵Ii蘵Xi蘵
75CD2080 67 69 CC 75 76 69 CC 75 85 69 CC 75 94 69 CC 75 gi蘵vi蘵卛蘵攊蘵
75CD2090 A3 69 CC 75 B2 69 CC 75 C1 69 CC 75 78 33 6A 76 蘵瞚蘵羒蘵x3jv
75CD20A0 D0 69 CC 75 68 68 CC 75 A5 B1 68 76 69 62 69 76 衖蘵hh蘵ケhvibiv
75CD20B0 88 9A C7 75 00 00 00 00 00 00 00 00 00 00 00 00 垰莡............
(图二)
该函数指针指向的代码如(图三),offset InternetReadFile的值是75CD2030
.text:75C7BB08 B8 30 20 CD 75 mov eax, offset InternetReadFile
.text:75C7BB0D E9 DE 03 FF FF jmp loc_75C6BEF0
(图三)
将该值赋值给eax寄存器后,跳转到地址75C6BEF0,所有延迟加载的Dll文件中的函数第一次调用是都jmp到这个地址,不同的只是eax寄存器中的值。
75C6BEF0处的代码如(图四)所示:
75C6BEF0 51 push ecx
75C6BEF1 52 push edx
75C6BEF2 50 push eax
75C6BEF3 68 30E8CC75 push 75CCE830 ; 指向ImgDelayDescr结构体
75C6BEF8 E8 88FDFFFF call 75C6BC85 ; 有两个参数,获得延迟加载函
; 数的地址,并将其值写到eax
; 的值75CD2030中,idata段相 ; 的位置处
75C6BEFD 5A pop edx
75C6BEFE 59 pop ecx
75C6BEFF FFE0 jmp eax ; 正式执行延迟加载的函数,此
; 时函数返回到75c73a5
(图四)
在(图四)中压入栈中的寄存器的值分别为
ecx =6a 保存的是参数dwNumberOfBytesToRead [in]的值,为准备读出的Buffer的长度
edx =18e3ec
eax =75cd2030 将来获得InternetReadFile函数所在的地址,保存在这里
call 75C6BC85的参数有两个分别为:
push eax ;其中eax值是75cd2030 实际上是ImgDelayDescr. rvaIAT中数组中的InternetReadFile函数相应的元素,如图(二)中第一个DWORD的值
push 75CCE830 ; 两个值的含义是75CCE830指向ImgDelayDescr结构体数组中,InternetReadFile所在的Dll文件 Wininet.dll所对应的元素。
以下是call 75C6BC85函数的具体实现,所有延迟加载的Dll文件中的函数第一次调用是都调用这个函数,不同的只是函数的参数。
75C6BC85 8BC0 mov eax, eax
75C6BC87 55 push ebp
75C6BC88 8BEC mov ebp, esp
75C6BC8A 83EC 44 sub esp, 44
75C6BC8D 53 push ebx
75C6BC8E B8 0000C675 mov eax, 75C60000 ; eax是所加载dll在内存块中的基址
75C6BC93 56 push esi
75C6BC94 8B75 08 mov esi, dword ptr [ebp+8] ; 该处存放75cce830
75C6BC97 8B56 08 mov edx, dword ptr [esi+8] ; rvaHmod
75C6BC9A 8B4E 04 mov ecx, dword ptr [esi+4] ; rvaDllName
75C6BC9D 8B5E 0C mov ebx, dword ptr [esi+C] ; rvaIAT
75C6BCA0 03D0 add edx, eax ; rvaHmod的相对地址
75C6BCA2 57 push edi
75C6BCA3 8B7E 14 mov edi, dword ptr [esi+14] ; rvaBoundIAT绑定地址
75C6BCA6 03F8 add edi, eax ; 获得绑定IAT表的相对地址
75C6BCA8 03C8 add ecx, eax ; 指向Dll字符串的地址-》WININET.DLL
75C6BCAA 8955 E8 mov dword ptr [ebp-18], edx ; ImgDelayDescr.ravHmod 的地址值压入堆栈中
75C6BCAD 8B56 10 mov edx, dword ptr [esi+10] ; rvalINT
75C6BCB0 03D8 add ebx, eax ; rvalIAT的相对地址
75C6BCB2 03D0 add edx, eax ; rvalINT的相对地址
75C6BCB4 8B46 1C mov eax, dword ptr [esi+1C] ; 时间戳dwTimeStamp
75C6BCB7 8945 FC mov dword ptr [ebp-4], eax ; 时间戳放到这里
75C6BCBA 8B45 0C mov eax, dword ptr [ebp+C] ; 第二个参数,用来保存所加载函数的地址
75C6BCBD 894D C8 mov dword ptr [ebp-38], ecx ; 所加载的dll文件的名称
75C6BCC0 33C9 xor ecx, ecx
75C6BCC2 897D F4 mov dword ptr [ebp-C], edi ; rvaBoundIAT绑定地址
75C6BCC5 8945 C4 mov dword ptr [ebp-3C], eax ; 用来存储dll 函数的地址
75C6BCC8 33C0 xor eax, eax
75C6BCCA F706 01000000 test dword ptr [esi], 1 ; 判断ImgDelayDescr的似一个元素,是否是1
75C6BCD0 8D7D D0 lea edi, dword ptr [ebp-30] ; lpProcName edi的值是13F998
75C6BCD3 C745 BC 2400000>mov dword ptr [ebp-44], 24
75C6BCDA 8975 C0 mov dword ptr [ebp-40], esi
75C6BCDD 894D CC mov dword ptr [ebp-34], ecx ; 将一下个处清零
75C6BCE0 AB stos dword ptr es:[edi]
75C6BCE1 894D D4 mov dword ptr [ebp-2C], ecx
75C6BCE4 894D D8 mov dword ptr [ebp-28], ecx
75C6BCE7 894D DC mov dword ptr [ebp-24], ecx
75C6BCEA 0F84 05C40100 je 75C880F5
75C6BCF0 8B45 E8 mov eax, dword ptr [ebp-18] ; Target
75C6BCF3 8B38 mov edi, dword ptr [eax] ; Wininet所在的基地址76680000
75C6BCF5 8B45 0C mov eax, dword ptr [ebp+C] ; 用于存放找到的延迟加载的函数的位置
75C6BCF8 2BC3 sub eax, ebx ; ebx=75cd2014 eax=75cd2030 算出和首地址的距离
75C6BCFA C1F8 02 sar eax, 2 ; 逻辑右移
75C6BCFD C1E0 02 shl eax, 2 ; 算数左移,目的是使该值成为4的整数倍
75C6BD00 03D0 add edx, eax ; edx==75cce91c
75C6BD02 8B12 mov edx, dword ptr [edx] ; 执行后edx的值是6eab0
75C6BD04 8945 08 mov dword ptr [ebp+8], eax
75C6BD07 8BC2 mov eax, edx
75C6BD09 C1E8 1F shr eax, 1F
75C6BD0C F7D0 not eax
75C6BD0E 83E0 01 and eax, 1
75C6BD11 8945 CC mov dword ptr [ebp-34], eax ; 这个值是1
75C6BD14 0F84 A1000000 je 75C6BDBB
75C6BD1A 8D82 0200C675 lea eax, dword ptr [edx+75C60002] ; 获得dll中函数的名称,基地址是75c60002
75C6BD20 8945 D0 mov dword ptr [ebp-30], eax
75C6BD23 A1 6455CD75 mov eax, dword ptr [75CD5564]
75C6BD28 33DB xor ebx, ebx
75C6BD2A 3BC1 cmp eax, ecx
75C6BD2C 0F85 E2C30100 jnz 75C88114
75C6BD32 85FF test edi, edi ; 这里是所延迟加载的WININET.dll的基地址
75C6BD34 74 4C je short 75C6BD82
75C6BD36 A1 6455CD75 mov eax, dword ptr [75CD5564]
75C6BD3B 85C0 test eax, eax
75C6BD3D 897D D4 mov dword ptr [ebp-2C], edi
75C6BD40 0F85 5DC40100 jnz 75C881A3
75C6BD46 85DB test ebx, ebx
75C6BD48 75 1D jnz short 75C6BD67
75C6BD4A 395E 14 cmp dword ptr [esi+14], ebx
75C6BD4D 0F85 5FC40100 jnz 75C881B2
75C6BD53 FF75 D0 push dword ptr [ebp-30]
75C6BD56 57 push edi
75C6BD57 FF15 2412C675 call dword ptr [<&KERNEL32.GetProcAdd>; 调用GetProcessAddress函数获得地址
75C6BD5D 8BD8 mov ebx, eax
75C6BD5F 85DB test ebx, ebx
75C6BD61 0F84 90C40100 je 75C881F7
75C6BD67 8B45 0C mov eax, dword ptr [ebp+C] ; 用来存放dll函数地址的指针,执行后eax=75cd2030
75C6BD6A 8918 mov dword ptr [eax], ebx
75C6BD6C A1 6455CD75 mov eax, dword ptr [75CD5564]
75C6BD71 85C0 test eax, eax
75C6BD73 0F85 C2C40100 jnz 75C8823B
75C6BD79 8BC3 mov eax, ebx
75C6BD7B 5F pop edi
75C6BD7C 5E pop esi
75C6BD7D 5B pop ebx
75C6BD7E C9 leave
75C6BD7F C2 0800 retn 8
(图五)
这里的注释都很详细,函数的具体含义就不再解释了,最后的将获得的延迟加载的函数的地址被放入75cd2030和eax寄存器中,函数返回。
回到图四的函数调用75C6BEFF FFE0 jmp eax 去执行InternetReadFile函数,当第二次调用InternetReadFile函数时不在执行以上过程,直接去调用InternetReadFile函数。
75C6BD82 A1 6455CD75 mov eax, dword ptr [75CD5564]
75C6BD87 85C0 test eax, eax
75C6BD89 0F85 9BC30100 jnz 75C8812A
75C6BD8F FF75 C8 push dword ptr [ebp-38]
75C6BD92 FF15 0C12C675 call dword ptr [<&KERNEL32.LoadLibrar>; kernel32.LoadLibraryA
75C6BD98 8BF8 mov edi, eax
75C6BD9A 85FF test edi, edi
75C6BD9C 0F84 95C30100 je 75C88137
75C6BDA2 57 push edi
75C6BDA3 FF75 E8 push dword ptr [ebp-18]
75C6BDA6 FF15 7012C675 call dword ptr [<&KERNEL32.Interlocke>; kernel32.InterlockedExchange
75C6BDAC 3BC7 cmp eax, edi
75C6BDAE 74 21 je short 75C6BDD1
75C6BDB0 837E 18 00 cmp dword ptr [esi+18], 0
75C6BDB4 ^ 74 80 je short 75C6BD36
75C6BDB6 E9 C1C30100 jmp 75C8817C
75C6BDBB 81E2 FFFF0000 and edx, 0FFFF
75C6BDC1 8955 D0 mov dword ptr [ebp-30], edx
75C6BDC4 ^ E9 5AFFFFFF jmp 75C6BD23
75C6BDC9 8BF8 mov edi, eax
75C6BDCB 85FF test edi, edi
75C6BDCD ^ 74 C0 je short 75C6BD8F
75C6BDCF ^ EB D1 jmp short 75C6BDA2
75C6BDD1 57 push edi
75C6BDD2 FF15 3812C675 call dword ptr [<&KERNEL32.FreeLibrar>; kernel32.FreeLibrary
75C6BDD8 ^ E9 59FFFFFF jmp 75C6BD36
图六
当在同一个dll文件中有多个函数需要延迟加载时,当加载该dll中的第一个函数时,在执行(图五)的函数的过程中会调用(图六)中的这段代码,来获得该dll的基地址,即调用kernel32.LoadLibraryA的返回值。
参考来源: http://blog.csdn.net/anzijin/article/details/2008337