参考博客
- https://www.cnblogs.com/night-ride-depart/p/5776107.html
- https://www.cnblogs.com/lanuage/p/7725699.html
1.PE文件结构
PE文件的结构一般来说如下图所示:从起始位置开始依次是DOS头,NT头,节表以及具体的节。DOS头是用来兼容MS-DOS操作系统的,目的是当这个文件在MS-DOS上运行时提示一段文字,大部分情况下是:This program cannot be run in DOS mode.还有一个目的,就是指明NT头在文件中的位置。 NT头包含windows PE文件的主要信息,其中包括一个‘PE’字样的签名,PE文件头(IMAGE_FILE_HEADER)和PE可选头(IMAGE_OPTIONAL_HEADER32),头部的详细结构以及其具体意义在PE文件头文章中详细描述。文件结构定义有两种地址偏移,一种是FOA(文件偏移地址),另外一种是RVA(相对虚拟地址)。
RVA = 虚拟地址-ImageBase
FOA: 文件偏移. 就是文件中所在的地址.
1通过虚拟地址偏移获取导入描述表
hMod = GetModuleHandle(NULL);
pAddr = (PBYTE)hMod; //这里的pAddr就是一个ImageBase的虚拟地址
// pAddr = VA to PE signature (IMAGE_NT_HEADERS)
pAddr += *((long long*)&pAddr[0x3C]); //根据结构体大小计算位0x3c,相对偏移量
// dwRVA = RVA to IMAGE_IMPORT_DESCRIPTOR Table
dwRVA = *((long long*)&pAddr[0x80]);
2通过文件地址偏移获取导入描述表
其中332表达的是0x014c 对应IMAGE_FILE_MACHINE_I386 // Intel 386
NumberOfSections:该PE文件中有多少个节,也就是节表中的项数。
TimeDateStamp:PE文件的创建时间,一般由连接器填写。
PointerToSymbolTable:COFF文件符号表在文件中的偏移。
NumberOfSymbols:符号表的数量。
SizeOfOptionalHeader:紧随其后的可选头的大小。
Characteristics:可执行文件的属性,可以是下面这些值按位相或。
unsigned int RVAToFOA(LPVOID buf, long long rva) {
if (buf == NULL) {
return 0;
}
PIMAGE_DOS_HEADER pdosHeader = (PIMAGE_DOS_HEADER)buf;
PIMAGE_NT_HEADERS pNtheader = (PIMAGE_NT_HEADERS)((long long)buf + (long long)pdosHeader->e_lfanew);
PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((long long)buf + 4 + (long long)pdosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER pOptHeader = (PIMAGE_OPTIONAL_HEADER)((long long)pFileHeader + sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionheader = (PIMAGE_SECTION_HEADER)((long long)pFileHeader + sizeof(IMAGE_FILE_HEADER) + pFileHeader->SizeOfOptionalHeader);
for (int i = 0; i <= pFileHeader->NumberOfSections; i++) {
if (rva >= (pSectionheader->VirtualAddress) && rva < (pSectionheader->VirtualAddress + pSectionheader->Misc.VirtualSize)) {
return (rva - pSectionheader->VirtualAddress) + pSectionheader->PointerToRawData;
}
pSectionheader++;
}
return 0;
}
OriginalFirstThunk 指向的数组通常叫做 hint-name table,即 HNT ,他在 PE 加载到内存中时被保留了下来且永远不会被修改。但是在 Windows 加载过 PE 到内存之后,Windows 会重写 FirstThunk 所指向的数组元素中的内容,使得数组中每个 IMAGE_THUNK_DATA 不再表示指向带有函数描述的 IMAGE_THUNK_DATA 元素,而是直接指向了函数地址。此时,FirstThunk 所指向的数组就称之为输入地址表(Import Address Table ,即经常说的 IAT)。
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
我们可以看出由于是union结构,所以IMAGE_THUNK_DATA 事实上是4个字节大小。
这个共用体是怎么使用的呢:
当 IMAGE_THUNK_DATA 值的最高位为 1时,表示函数以序号方式输入,这时候低 31位被看作一个函数序号。
当 IMAGE_THUNK_DATA 值的最高位为 0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个 RVA,指向一个 IMAGE_IMPORT_BY_NAME 结构。
接下来说明IMAGE_IMPORT_BY_NAME 结构:
PDWORD OriginalFirstThunk = (PDWORD)((long long)FileBuffer + RVAToFOA(FileBuffer, ImageImportDescriptor->OriginalFirstThunk));
PDWORD FirstThunk = (PDWORD)((long long)FileBuffer + RVAToFOA(FileBuffer, ImageImportDescriptor->FirstThunk));
while (*OriginalFirstThunk != 0)
{
long long OriginalFirstThunkBestHighBit = (*OriginalFirstThunk & 0x80000000) >> 31;
if (OriginalFirstThunkBestHighBit == 1)
{
long long OriginalFirstThunkSerialNumber = (*OriginalFirstThunk & 0x6FFFFFFF);
printf("序号为->%x\n", OriginalFirstThunkSerialNumber);
}
else
{
PIMAGE_IMPORT_BY_NAME OriginalFirstThunkFunctionName = (PIMAGE_IMPORT_BY_NAME)((long long)FileBuffer + RVAToFOA(FileBuffer, *OriginalFirstThunk));
printf("HIT为->%x\n", OriginalFirstThunkFunctionName->Hint);
printf("函数名为->%s\n", OriginalFirstThunkFunctionName->Name);
}
OriginalFirstThunk++;
}