尝试编写代码获取PE文件的信息。
首先使用 CreateFile
打开一个PE文件并返回一个用于访问该对象的handle
。
HANDLE CreateFile(
LPCTSTR lpFileName, // pointer to name of the file
DWORD dwDesiredAccess, // access (read-write) mode
DWORD dwShareMode, // share mode
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // pointer to security attributes
DWORD dwCreationDistribution, // how to create
DWORD dwFlagsAndAttributes, // file attributes
HANDLE hTemplateFile // handle to file with attributes to copy
);
然后调用CreateFileMapping
函数为该文件创建一个文件映射对象(file-mapping object)
,此时为文件创建了一个视图,而并未将该视图映射进进程的地址空间,也就是尚未装载进入内存之中的意思吧。
Creating a file-mapping object creates the potential for mapping a view of the file but does not map the view. The MapViewOfFile and MapViewOfFileEx functions map a view of a file into a process's address space.
HANDLE CreateFileMapping(
HANDLE hFile, // handle to file to map
LPSECURITY_ATTRIBUTES lpFileMappingAttributes, // optional security attributes
DWORD flProtect, // protection for mapping object
DWORD dwMaximumSizeHigh, // high-order 32 bits of object size
DWORD dwMaximumSizeLow, // low-order 32 bits of object size
LPCTSTR lpName // name of file-mapping object
);
接着MapViewOfFile
函数会将之前创建的视图对象映射进入当前调用父进程的内存地址空间中了,那也就是说此时该PE文件基址将不再会是默认的,与DLL文件被程序调用的载入方式一致。
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, // file-mapping object to map into address space
DWORD dwDesiredAccess, // access mode
DWORD dwFileOffsetHigh, // high-order 32 bits of file offset
DWORD dwFileOffsetLow, // low-order 32 bits of file offset
DWORD dwNumberOfBytesToMap // number of bytes to map
);
MapViewOfFile返回值为LPVOID
,为该映射视图对象在内存中的基址,指向PE文件中最开始的IMAGE_DOS_HEADER
结构体,做完这些就可以开始对文件的结构进行分析了。
在写代码的时候要注意类型转换,不然运算结果可能出乎意料:)
大多数结构体中的地址成员为RVA
,需要转化,我这里主要记录一下遇到的关于RVAToVA的问题。
- 转化RVA,可以使用
ImageRvaToVa()
函数,该函数在"Imagehlp.h"或"Dbghelp.h"中声明:
PVOID IMAGEAPI ImageRvaToVa(
\_In\_ PIMAGE_NT_HEADERS NtHeaders,
\_In\_ PVOID Base,
\_In\_ ULONG Rva,
\_In\_opt\_ OUT PIMAGE_SECTION_HEADER *LastRvaSection
);
在vs中碰到一个问题,编译时可能会出现引用错误之类的问题,这个在#include "imagehlp.h"
下面添加#pragma comment(lib, "imagehlp.lib")
即可;Dbghelp.h
同理。
这里加入#pragma comment 解决问题的原因:出错的地方主要在于链接程序无法解析别引用的外部函数(此时已经编译完成),使用comment使linker根据指定的库名查找该库,从而可以识别调用的函数。
那为什么出现这种情况?谷歌一下,原因在于vs的链接器默认写入了我们常用的一些库,当是一些不常用的库则没有,这个时候链接器只找到声明,倒是没有函数的定义部分,也因此需要我们自己添加。
- 由于函数并不复杂,可以尝试自己手动实现一个ImageRvaToVa函数:
INT32 ImageRvaToVa(PIMAGE_NT_HEADERS pNtH, LPVOID ImageBase, DWORD Rva) {
INT sectionTbNum = pNtH->FileHeader.NumberOfSections; // 记录节区头的个数,便于遍历
PIMAGE_SECTION_HEADER pCurrentSection = (PIMAGE_SECTION_HEADER)((DWORD)pNtH + (pNtH->FileHeader).SizeOfOptionalHeader + 0x18); // 当前节区头VA
// printf("%x
", pCurrentSection->VirtualAddress);
// printf("sectionTbNum : %x
pCurrentSection : %x", (DWORD)sectionTbNum, (DWORD)pCurrentSection);///////////////////
for (int i = 0; i < sectionTbNum; i++, pCurrentSection ++) {
// 判定待转化的RVA是否在当前的节区范围内
if (Rva > pCurrentSection->VirtualAddress && Rva <= pCurrentSection->VirtualAddress+pCurrentSection->SizeOfRawData) {
// return: RVA - CurrentSectionRVA + CurrentSectionRAW + ImageBase
return Rva - pCurrentSection->VirtualAddress + pCurrentSection->PointerToRawData + (DWORD)ImageBase;
}
}
return 0;
}
自己尝试写的原因在于刚开始尝试以RVA+ImageBase
作为VA
的时候一直出错,后面上网找写文章看看,后面又看了一下OD中该可执行文件的导入表的地址计算方式,发现时VA = RVA+ImageBase
,然后又看见一个关于ImageRvaToVa函数的汇编代码,之后自己尝试调试之后根据其不复杂的逻辑可以写出代码。
根据这个情况,是不是说明通过函数将PE文件映射进入进程的地址空间的做法并没有如同通常的可执行文件一样根据VA来确定位置,而是带有基址的文件偏移作为地址。
当不使用该文件影响时,要完全关闭这个文件映射对象,需要调用CreateFileMapping
和CloseHandle
函数,调用顺序随意。