上一篇文章只是大体介绍了一下有关引入表的内容,收到部份网友的来信说不太详细,将以前整理的一篇也拿了出来,希望这一篇能给大家更大的帮助。
-------------------------------------------------------------------------------
我们平时做好程序后准备发布时,经常会对软件做一些相关的保护措施,对执行文件进行加密处理(即所谓的加壳)是我们常用的方法之一。
我们在Win32系统生成的EXE文件一般是PE文件,看过“PE文件格式“内容的朋友应该知道,在我们的程序中调用的API函数,程序执行时通过PE引入表(Import Table)可以得到API函数的地址,从而使程序能够正常运行。那么当我们对程序加壳后,壳程序中所调用的API函数在执行时又是如何取得其在系统中的地址的呢?
通常加壳程序都是通过自建引入表、壳程序本身再将原引入表导入的方法使壳程序能够正常的运行。本节将详细的讲解壳程序是如何自建引入表、导入原引入表的方法,使你能够对引入表有更深入的了解。
一、自建引入表
首先我们先回顾一下关于引入表的部分内容,详细介绍参见有关PE资料。
引入表的RVA和长度,都是存放在PE文件头结构中最后一项的目录表项部份中的第二项,其结构如下:
--------------------------------------------------
IMAGE_DATA_DIRECTORY STRUCT
VirtualAddress dword ? ;1
isize dword ? ;2
IMAGE_DATA_DIRECTORY ENDS
1、指向IMAGE_IMPORT_DESCRIPTOR数组的RVA
2、长度
--------------------------------------------------
引入表其实就是一个IMAGE_IMPORT_DESCRIPTOR 结构数组,每个被程序引用的DLL都有一个IMAGE_IMPORT_DESCRIPTOR。例如:我们的程序中所调用的函数来自于3个DLL文件,那么就有3个这样的数组。该数组的每一项如果全为0表示结尾。
IMAGE_IMPORT_DESCRIPTOR的结构如下:
--------------------------------------------------
IMAGE_IMPORT_DESCRIPTOR STRUCT
OriginalFirstThunk dd ? ;1
TimeDateStamp dd ? ;2
ForwarderChain dd ? ;3
Name1 dd ? ;4
FirstThunk dd ? ;5
IMAGE_IMPORT_DESCRIPTOR ENDS
1、指向IMAGE_THUNK_DATA数组的RVA
2、时间日期标志
3、正向链接索引
4、指向DLL文件名的RVA
5、指向引入函数真实地址单元处的RVA
--------------------------------------------------
IMAGE_THUNK_DATA其实就是一个指向IMAGE_IMPORT_BY_NAME结构的指针,来看一下
IMAGE_IMPORT_BY_NAME 的结构:
--------------------------------------------------
IMAGE_IMPORT_BY_NAME STRUCT
Hint dw ? ;1
Name1 db ? ;2
IMAGE_IMPORT_BY_NAME ENDS
1、索引号
2、引入函数的函数名的ASCII字符串
--------------------------------------------------
说了这么多结构,可能大家感觉有点晕,不要紧,下面给出自建引入表的代码,看着代码再对照着上面的相关结构,你会更明白一些。 假设我们要建的壳程序只会显示一个对话框,那么他将会调用5个API函数,这5个API函数都是最基本的,其中4个用于导入原引入表,余下的一个用于显示对框框,因此缺一不可。这5个函数来源于KERNEL32.DLL和USER32.DLL两个DLL文件,根据上面讲的,构建他们的IMAGE_IMPORT_DESCRIPTOR结构:
--------------------------------------------------
align 4
v_ImportA dd Ker_API-v_ImportA ;1
v_TimeDateA dd 0 ;2
v_ForChainA dd 0 ;3
v_DllNameA dd KerName-v_ImportA ;4
v_FThunkA dd vGetProcAddress-v_ImportA ;5
v_ImportB dd Use_API-v_ImportA ;1
v_TimeDateB dd 0 ;2
v_ForChainB dd 0 ;3
v_DllNameB dd UserName-v_ImportA ;4
v_FThunkB dd vMessageBoxA-v_ImportA ;5
dd 20 dup (0) ;6
1、指向IMAGE_THUNK_DATA数组的地址。具体信息见下方
2、时间日期标志。此项可以忽略
3、正向链接索引。此项可以忽略
4、指向我们所要调用的DLL文件名的地址
5、指向引入函数真实地址单元处的地址。具体信息见下方
6、结束符。由于我们总共就引用2个DLL,因此将
IMAGE_IMPORT_DESCRIPTOR中的各项全部设为0,表示结尾
--------------------------------------------------
余下的引入表部份见下方:
--------------------------------------------------
KerName db 'KERNEL32.DLL',0
Ker_API dd KAPI_A-v_ImportA
dd KAPI_B-v_ImportA
dd KAPI_C-v_ImportA
dd KAPI_D-v_ImportA
dd 0
UserName db 'USER32.DLL',0
Use_API dd UAPI_A-v_ImportA
dd 0
vGetProcAddress dd 0
vGetModuleHandleA dd 0
vLoadLibraryA dd 0
vExitProcess dd 0
vMessageBoxA dd 0
dd 0
KAPI_A db 0,0,'GetProcAddress',0
KAPI_B db 0,0,'GetModuleHandleA',0
KAPI_C db 0,0,'LoadLibraryA',0
KAPI_D db 0,0,'ExitProcess',0
UAPI_A db 0,0,'MessageBoxA',0
vImport_End:
可以看出,以上部份也是按照引入表结构中的各项构造的。
像KAPI_A到KAPI_D和UAPI_A即IMAGE_IMPORT_BY_NAME结构。
--------------------------------------------------
至此引入表部份已经完成一大半啦。从上面的代码可以看出,我们在某些项中所填写的地址,是相对于v_ImportA来说的偏移地址,因此还需要再得到我们自建的引入表所在位置的RVA地址,并将此值加上上面的各地址项,再将引入表所在位置的RVA地址,保存到PE文件头结构中最后一项的目录表项部份中的第二项的VirtualAddress中,再将我们所建的引入表的大小(即vImport_End-v_ImportA)保存到isize中,至此大功告成。
二、导入原引入表
为了更好的理解这部份的代码,首先讲解一下导入引入表的步骤:
1、首先获取第一个DLL模块的模块句柄;
2、利用第一步得到的DLL模块句柄通过调用GetProcAddress函数,循环得到引用此DLL 文件中的所有函数的地址并将得到的地址保存到地址表中;
3、再获取下一个DLL模块句柄,执行第2步,一直到操作完最后一个模块为止。
按照代码输入的顺序看一下代码,更容易理解:
--------------------------------------------------
; 前提
; edx保存此PE文件的基址
; esi保存当前引入表的RVA地址
Next_DLL:
mov eax,[esi+0ch] ;1
or eax,eax ;
jz Dll_END ;2
add eax,edx ;
mov ebx,eax ;3
push eax ;
call [vGetModuleHandleA] ;4
or eax,eax ;
jnz Dll_LOADED ;
push ebx ;
call [vLoadLibraryA] ;5
or eax,eax ;
jnz Dll_LOADED ;6
到1、获得当前IMAGE_IMPORT_DESCRIPTOR结构所指的DLL名称;
到2、如果存在继续往下执行,如果不存在(已到结尾)跳到最后,结束导入工作;
到3、将DLL名称所在的RVA地址加上基址,并保存在ebx中;
到4、调用GetModuleHandleA函数,得到此DLL模块句柄;
到5、如果此DLL未载入到内存中,通过调用LoadLibraryA函数载入此DLL;
到6、得到DLL句柄跳到Dll_LOADED,否则往下运行到Exit_LOADER处
--------------------------------------------------
Exit_LOADER:
lea eax,[MI_ERR_TITLE] ;
push 64 ;
push eax ;
lea eax,[MI_ERR_TEXTS] ;
push eax ;
push 0 ;
call [vMessageBoxA] ;1
push 0 ;
call [vExitProcess] ;2
Dll_LOADED :
mov [hDll],eax ;3
mov [FunTable_Count],0 ;4
当我们在载入引入表某个步骤失败时,都会跳到此处。
到1、显示出错信息;
到2、退出程序
到3、将得到的模块句柄进行保存
到4、FunTable_Count表示当前我们正在操作函数的索引值
--------------------------------------------------
Next_FUNCTION:
mov edx,[BASE_RVA] ;1
mov eax,[esi] ;
or eax,eax ;
jnz Hint_OK ;
mov eax,[esi+10h] ;2
到1、得到基址
到2、得到IMAGE_THUNK_DATA结构数组的RVA
--------------------------------------------------
Hint_OK:
add eax,edx ;1
add eax,[FunTable_Count] ;2
mov ebx,[eax] ;3
mov edi,[esi+10h] ;
add edi,edx ;
add edi,[FunTable_Count] ;4
test ebx,ebx ;
jz Function_END ;5
test ebx,80000000h ;
jnz Function_ORDINAL ;6
add ebx,edx ;
add ebx,2 ;
jmp Function_GOON ;7
到1、加上基址
到2、加上所操作函数的索引值
到3、保存当前所操作函数信息的所在地址
到4、得到当前所操作函数的地址表的地址
到5、当前函数不存在跳到Function_END
到6、当前函数信息如果是以序数表示的,跳到Function_ORDINAL
到7、得到函数的名称,并跳到Function_GOON
--------------------------------------------------
Function_ORDINAL:
and ebx,0FFFFFFFh ;1
Function_GOON: ;
push ebx ;
push dword ptr [hDll] ;
call [vGetProcAddress] ;2
or eax,eax ;
jz Exit_LOADER ;3
mov [edi],eax ;4
add [FunTable_Count],4 ;
jmp Next_FUNCTION ;5
Function_END:
add esi,14h ;
mov edx,[BASE_RVA] ;
jmp Next_DLL ;6
Dll_END:
到1、屏序数的高4位
到2、调用GetProcAddress得到所操作函数的地址
到3、不成功,退出;
到4、将函数的地址保存到地址表中
到5、将所操作函数的索引值加上4,继续操作下一个函数
到6、当当前DLL的所有函数全部操作完毕,继续操作下一个DLL
--------------------------------------------------
至此利用我们自己的程序导入引入表讲解完啦。大家可以下载代码再研究一下。