导入表的位置和大小可以从PE文件头中IMAGE_OPTIONAL_HEADER32结构的数据目录字段中获取,对应的项目是DataDirectory字段的第2个IMAGE_DATA_DIRECTORY结构,从结构的VirtualAddress字段得到导入表的RVA值。
导入表由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成,结构的数量取决于程序要使用的DLL文件的数量,每个结构对应一个DLL文件,在所有这些结构的最后,由一个内容全为0的IMAGE_IMPORT_DESCRIPTOR结构作为结束。
IMAGE_IMPORT_DESCRIPTOR结构的定义如下:
IMAGE_IMPORT_DESCRIPTOR = packed record
case integer of
0: Characteristics: DWORD;
1: OriginalFirstThunk:DWORD;
end;
TimeDateStamp: DWORD;
ForwarderChain: DWORD;
Name: DWORD;
FirstThunk: DWORD;
end;
结构中的Name字段是一个RVA,它指向此结构所对应的DLL文件的名称,这个文件名是一个以NULL结尾的字符串。
OriginalFirstThunk字段和FirstThunk字段,它们都指向一个包含一系列IMAGE_THUNK_DATA结构的数组,数组中的每个IMAGE_THUNK_DATA结构定义了一个导入函数的信息,数组的最后以一个内容为0的IMAGE_THUNK_DATA结构作为结束。
一个IMAGE_THUNK_DATA结构实际上就是一个双字,之所以把它定义成结构,是因为它在不同的时刻有不同的含义,结构的定义如下:
IMAGE_THUNK_DATA = record
case integer of
0: ForwarderString: DWORD;
1: Function: DWORD;
2: Ordinal: DWORD;
3: AddressOfData: DWORD;
end;
end;
一个IMAGE_THUNK_DATA结构如何用来指定一个导入函数呢?当双字(就是指结构)的最高位为1时,表示函数是以序号的方式导入的,这时双字的低位就是函数的序号。当双字的最高位为0时,表示函数以字符串类型的函数名方式导入,这时双字的值是一个RVA,指向一个用来定义导入函数名称的IMAGE_IMPORT_BY_NAME结构,此结构的定义如下:
IMAGE_IMPORT_BY_NAME = record
Hint: Word;
Name: Array[0..0] of Char;
end;
结构中的Hint字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为0,Name字段定义了导入函数的名称字符串,这是一个以0为结尾的字符串。
为什么需要两个一模一样的IMAGE_THUNK_DATA数组呢?答案是当PE文件被装入内存的时候,其中一个数组的值将被改作他用,Windows装载器会将指令Jmp dword ptr [xxxxxxxx]指定的xxxxxxxx处的RVA替换成真正的函数地址,其实xxxxxxxx地址正是由FirstThunk字段指向的那个数组中的一员。
实际上,当PE文件被装入内存后,其中由FirstThunk字段指向的那个数组中的每个双字都被替换成了真正的函数入口地址,之所以在PE文件中使用两份IMAGE_THUNK_DATA数组的拷贝并修改其中的一份,是为了最后还可以留下一份拷贝用来反过来查询地址所对应的导入函数名。
导入表内注入代码(一)
下载源代码(包括:itview.zip (87.1 KB) pemaker6.zip (96.6 KB) pemaker7.zip (193 KB)zimport.zip (130 KB)。译注:本文代码可在VS2003及WINDOWSXP+sp2下正常运行,Windows2000下ITview功能有异常。)
本文介绍注入代码到PE(Portable Executable可移植的执行体)文件格式的Import Table(导入表,也有译为“引入表”)技术,其也被称为API重定向技术(API redirection technique)。
让我们想像一下:如果我们可以通过操作导入表thunks将导入函数的入口点(thoroughfare)重定向到我们的指定的例程,用我们的例程过滤导入(消息)就成为可能。此外,我们可以通过这个功能实现安排给我们适当的例程,专业的Portable Executable (PE) Protectors正是这么来做的,另外一些种类的rootkits使用这个方法通过一个特洛伊木马嵌入其恶意代码到受害者。在反向工程世界里,我们称之为:API重定向技术,然而我不准备通过源代码描述这个领域的所有观点,本文只是通过一个简单代码介绍一下这个技术的概况。我将描述这个源代码中没有的其他一些问题;我不能公开这些代码,原因是其关系到一些商业项目或可能会被怀有恶意者利用,然而我想本文可以被用来作为一个关于该主题的入门。
1.进入导入表
PE文件格式包括:MS-DOS header、NT headers、Sections headers和Section images(译注:正如很多技术读物上一样,其实header可以译为“头”,image可译为“映像”,但本文正文在不影响理解的前提下尽量保留原文术语,以免误解。)正如你在图 1中所看到的。MS-DOS header是自DOS时代到Windows时代在所有微软可执行文件格式(executable file format)公有的。NT headers的思想来源于UNIX系统的Executable and Linkable Format (ELF),当然Portable Executable (PE)格式是Linux Executable and Linkable Format (ELF)的姐妹。PE 格式包括"PE" Signature、Common Object File Format (COFF) header、Portable Executable Optimal header和Section headers。
图1 - Portable Executable 文件格式结构
NT headers的定义可以在Virtual C++ included 目录下 <winnt.h>头文件中找到。该信息可以非常容易地通过使用DbgHelp.dll的ImageNtHeader()函数获得。你也可以使用DOS header来获取NT headers,因为DOS header的末尾位置:e_lfanew,代表NT headers的偏移。(译注:将这个偏移加到内存映射文件的基址上就得到了PE header 的地址:pNTHeader=dosHeader+ dosHeader->e_lfanew;)
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
在Portable Executable Optional header,有一些数据目录(data directories)描述了当前进程在虚拟内存中主信息表(the principal information tables)的相对位置和大小。这些表可以有关于资源的信息、import(导入)、export(导出)、relocation(重定位)、Debug(调试)、thread local storage(线程本地存储)和COM运行时。没有导入表想要找到一个PE可执行文件是不可能的;该表包含DLL的名称和Functions(函数)名称,这些是当程序意图通过它们的虚地址来请求(调用)它们时所必需的。在Console executable files(控制台可执行文件)中没有发现资源表(resource table);然而它是拥有Graphic User Interface (GUI)的Windows可执行文件的至关重要的部分。导出表(export table)在一个动态链接库想要导出它的函数到外界时是必需的,并且它也在OLE Active-X容器中。Dot NET虚拟机在没有COM+ runtime header下时不能被执行。正如你看到的,在PE格式中每个表都有特定的委派任务,图 2。
图2 - Data Directories(数据目录)
Data | 0 Export Table |
1Import Table | |
2 Resource Table | |
3 Exception Table | |
4 Certificate File | |
5 Relocation Table | |
6 Debug Data | |
7 Architecture Data | |
8 Global Ptr | |
9 Thread Local Storage Table | |
10 Load Config Table | |
11 Bound Import Table | |
12 Import Address Table | |
13 Delay Import Descriptor | |
14 COM+ Runtime Header | |
15 Reserved |
// <winnt.h>
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES16
// Optional header format.
typedef struct _IMAGE_OPTIONAL_HEADER {
...
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
// Directory Entries
#define IMAGE_DIRECTORY_ENTRY_EXPORT0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG6 // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_TLS9 // TLS Directory
我们只用三两行代码就可以获得导入表的位置和大小。通过知道导入表的位置,我们转入下一步获得DLL名称和Function名称,它将在下一节里讨论。
PIMAGE_NT_HEADERS pimage_nt_headers = ImageNtHeader(pImageBase);
DWORD it_voffset = pimage_nt_headers->OptionalHeader.
DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
PIMAGE_DOS_HEADER pimage_dos_header = PIMAGE_DOS_HEADER(pImageBase);
PIMAGE_NT_HEADERS pimage_nt_headers = (PIMAGE_NT_HEADERS)
(pImageBase + pimage_dos_header->e_lfanew);
DWORD it_voffset = pimage_nt_headers->OptionalHeader.
DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
2. 导入描述符(Import Descriptor)一瞥
导入表的导入目录入口(import directory entry)带我们来到文件image内部的导入表位置。
对于每个被导入的DLL,导入描述符是个容器,它包含了first thunk的地址和original first thunk的地址,DLL名称的指针。First Thunk引用first thunk的位置,thunks在运行该程序时将会被Windows的PE loader(装载器)初始化,图 5。Original First Thunk指向thunks第一个存储处,该存储处提供Hint 数据的地址和每个函数的Function Name数据,图 4。在此情况下,First Original Thunk没有出现;First Thunks引用Hint 数据和Function Name 数据被定位的位置,图 3。用IMAGE_IMPORT_DESCRIPTOR来表示导入描述符结构如下定义:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
DWORDOriginalFirstThunk;
DWORDTimeDateStamp;
DWORDForwarderChain;
DWORDName;
DWORDFirstThunk;
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
成员:
OriginalFirstThunk
它指向first thunk,IMAGE_THUNK_DATA,该thunk拥有Hint和Function name的地址。
TimeDateStamp
如果那里有绑定的话它包含时间/数据戳(time/data stamp)。如果它是0,就没有绑定在被导入的DLL中发生。在最近,它被设置为0xFFFFFFFF以表示绑定发生。
ForwarderChain
在老版的绑定中,它引用API的第一个forwarder chain(传递器链表)。它可被设置为0xFFFFFFFF以代表没有forwarder。
Name
它表示DLL 名称的相对虚地址(译注:相对一个用null作为结束符的ASCII字符串的一个RVA,该字符串是该导入DLL文件的名称,如:KERNEL32.DLL)。
FirstThunk
它包含由IMAGE_THUNK_DATA定义的 first thunk数组的虚地址,通过loader用函数虚地址初始化thunk。在Orignal First Thunk缺席下,它指向first thunk:Hints和The Function names的thunks。
typedef struct _IMAGE_IMPORT_BY_NAME {
WORDHint;
BYTEName[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
typedef struct _IMAGE_THUNK_DATA {
union {
PDWORDFunction;
PIMAGE_IMPORT_BY_NAMEAddressOfData;
} u1;
} IMAGE_THUNK_DATA, *PIMAGE_THUNK_DATA;
图3 - Import Table View
图4 - Import Table View (有 Orignal First Thunk)
这里有两个导入表(图3和图4)表示有和没有original first thunk下导入表的不同。
图5-用PE loader重写后的导入表
我们可以用Dependency Walker,图 6,来察看导入表的所有信息。顺便说一下,我已提供另一个工具,Import Table viewer,图 7,它简单且与前面工具的操作相似。通过这类工具我肯定它的资源将帮助你更好地理解它做的主要(功能)表现。
Figure 6 - Dependency Walker, Steve P. Miller
这里我们看到一个简单的可以用一个控制台模式程序来显示导入DLLs和导入Functions的资源。然而,我想我的Import Table viewer,图 7,因为它的图形用户界面更适于来理解主题。
PCHARpThunk;
PCHARpHintName;
DWORDdwAPIaddress;
PCHARpDllName;
PCHARpAPIName;
//----------------------------------------
DWORD dwImportDirectory= RVA2Offset(pImageBase, pimage_nt_headers->OptionalHeader.
DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
//----------------------------------------
PIMAGE_IMPORT_DESCRIPTOR pimage_import_descriptor= (PIMAGE_IMPORT_DESCRIPTOR)
(pImageBase+dwImportDirectory);
//----------------------------------------
while(pimage_import_descriptor->Name!=0)
{
pThunk= pImageBase+pimage_import_descriptor->FirstThunk;
pHintName= pImageBase;
if(pimage_import_descriptor->OriginalFirstThunk!=0)
{
pHintName+= RVA2Offset(pImageBase, pimage_import_descriptor->OriginalFirstThunk);
}
else
{
pHintName+= RVA2Offset(pImageBase, pimage_import_descriptor->FirstThunk);
}
pDllName= pImageBase + RVA2Offset(pImageBase, pimage_import_descriptor->Name);
printf(" DLL Name: %s First Thunk: 0x%x", pDllName,
pimage_import_descriptor->FirstThunk);
PIMAGE_THUNK_DATA pimage_thunk_data= (PIMAGE_THUNK_DATA) pHintName;
while(pimage_thunk_data->u1.AddressOfData!=0)
{
dwAPIaddress= pimage_thunk_data->u1.AddressOfData;
if((dwAPIaddress&0x80000000)==0x80000000)
{
dwAPIaddress&= 0x7FFFFFFF;
printf("Proccess: 0x%x", dwAPIaddress);
}
else
{
pAPIName= pImageBase+RVA2Offset(pImageBase, dwAPIaddress)+2;
printf("Proccess: %s", pAPIName);
}
pThunk+= 4;
pHintName+= 4;
pimage_thunk_data++;
}
pimage_import_descriptor++;
}
对在Image Import Descriptor里, 每个DLL的入口格式的简单小结
在image import descriptor 区域的每个入口符合一个DLL
每个DLL占用5个Dword (双词)
对我们来说,最重要的Dword是:
Microsoft 类型: Dword 1, Dword 4, Dword 5
Borland 类型: Dword 4, Dword 5
图02.
(*) Entrée pour la DLL n°1 (例子 : KERNEL32.dll)
(**) Entrée pour la DLL n°2 (例子 : SHELL32.dll)
Dword1 :
它指向表格的第一个dword.
这个表包含Dword 4指向的DLL的所有的有关函数的指针.
这个表是‘ Hint Name Array ’内存块段的一部分.
Dword 5 :
它指向表格的第一个dword.
这个表,包含在‘ Hint Name Array ‘里的函数的所有调用地址. 它与Hint Name Array并行.
这个表格是‘ Import Address Table ’内存块段的一部分.
在这,最有用的是 Dword 4, 能帮助我们在.rdata 节里找到我们的小朋友们 :
Dword 4 包含DLL明文名字的地址(Raw Virtual Address).
比如:
符合KERNEL32.dll的入口.
假设dword 4 含有‘8A 65 01 00’的值, 就是表示它指向’00 01 65 8A’的地址.
要是Raw Offset 与Virtual Offset相等 (也就是说在硬盘上的地址与内存里的地址相符),我们将在offset ’00 01 65 8A’(在文件)找到‘KERNEL32.dll’的字串.
在.rdata节内,输入函数组织的例子
注意,这只是一个例子,只是我有幸观察过几个包含.rdata节的程序后的结果.
因此,我不敢担保所有的.rdata节都与此相同.
图03.
您看到了吗? 除了import address table 的块段现在处于.rdata 节的开端,跟在一段数据块后,其它基本上是一样的.
唯一的盲点对我们来说是,Image Import Descriptor的块不处在rdata节的开端.
因此,当我们遇到象被ASPack 或 ASProtect加壳的程序,并且我们成功的DUMP出脱壳程序,我们还要用可靠快捷的方法成功辨认出这个块段(地址的开端及其大小),为了将这些信息交给PE header.
由此,我们可以得到一个可运行的程序,并且具有输入函数表的反编代码.
怎样找回内存里包含Image Import Descriptor信息的区域,当在PE header没有具备这些信息时