PE文件格式的一些研究
分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.****.net/jiangjunshow
也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!
作者:朱金灿
来源:http://blog.****.net/clever101/
最近抽空对PE文件格式做了一些研究。众所周知,PE文件格式是Windows平台下可执行文件的格式。为什么要研究PE文件格式?可能有人认为,做这件事就是一件重复造轮子的事,因为之前已经有无数人做过这样的事。但是有些事不是简单地以是不是重复造轮子来衡量的。研究PE文件格式对加深程序本质的认识和理解程序的构成都有很大的好处。美籍匈牙利科学家冯•诺依曼最新提出程序存储的思想,具体到研究PE文件格式,或许可以是运行程序所需的指令和数据是以怎样的组织结构存贮在文件的,在运行时程序是怎样被加载的,数据是怎样初始化的。
一个完整的PE文件结构一般由五大部分组成。如下图:
最开头的是部分是DOS部首,DOS部首由两部分组成:DOS的MZ文件标志和DOS stub(DOS存根程序)。之所以设置DOS部首是微软为了兼容原有的DOS系统下的程序而设立的。
紧接着的是真正的PE文件头。值得注意的是PE文件头中的IMAGE_OPTIONAL_HEADER32是一个非常重要的结构,PE文件中的导入表、导出表、资源、重定位表等数据的位置和长度都保存在这个结构里。
PE文件结构中可研究的内容很多,暂时讲解这么多,有兴趣的朋友可以阅读《Windows图形编程》中的第一章和《程序员的自我修养》的5.6节。
研究PE文件格式,最好的方法我觉得还是自己动手写一个PE文件解释类。看了《Windows图形编程》中的第一章,加深自己对PE文件结构的理解,我决定在袁峰大侠(《Windows图形编程》一书作者)编写PE文件解释类KPEFile的基础上增加一些接口。KPEFile类的构造函数是通过提供模块句柄来获取PE文件信息的,我发现这对运行中的程序是有用的,但对解析一个静态的exe文件并不有效。
编程实现分析一个PE文件,网上一般有两种做法:一是打开exe文件,然后利用Dbghelp库的一个函数ImageRvaToVa来获取你要打开结构的指针;另一种做法也大同小异,也是打开exe文件,自己计算所有获取信息的结构的偏移地址。我采用的是第二种做法。
下面是重要部分代码:
- /*!
- * @brief tString类主要是为了兼容unicode字符集和多字节字符集
- *
- *
- */
- #ifdef UNICODE
- #define tString std::wstring
- #else
- #define tString std::string
- #endif
- /*! @struct stImportDll
- * @brief 导入的DLL的信息结构体
- *
- *
- */
- struct stImportDll
- {
- /* @brief 导入的DLL名
- *
- *
- */
- tString m_strDllName;
- /* @brief 导入的DLL中的函数
- *
- *
- */
- std::vector<tString> m_vecStrFunname; //
- };
- /*!
- * @brief KPEFile类构造函数
- *
- * @param [in]strBinPath exe文件全路径,主要是获取DOS部首和PE文件头的位置
- * /return
- */
- KPEFile::KPEFile( tString strBinPath )
- {
- DWORD dwRead = 0;
- m_pModule = NULL;
- // 打开exe文件
- HANDLE hFile = CreateFile(
- strBinPath.c_str(), //PE文件名
- GENERIC_READ,
- FILE_SHARE_READ,
- NULL,
- OPEN_EXISTING,
- FILE_ATTRIBUTE_NORMAL,
- NULL);
- if(hFile == INVALID_HANDLE_VALUE)
- {
- return;
- }
- // 获取文件大小
- DWORD dwSize = GetFileSize(hFile,NULL);
- if (dwSize == 0xFFFFFFFF){
- return ;
- }
- // 开辟读取缓冲区
- m_pModule = (LPBYTE)VirtualAlloc( NULL,dwSize,MEM_COMMIT,PAGE_READWRITE);
- // 读取文件
- ReadFile(hFile,m_pModule,dwSize,&dwRead,NULL);
- if( dwRead != dwSize)
- return ;
- // 关闭文件
- CloseHandle(hFile);
- // 获取DOS部首和PE文件头的位置
- m_pDosHeader = (PIMAGE_DOS_HEADER)m_pModule;
- m_pNTHeader = (PIMAGE_NT_HEADERS)(m_pModule + m_pDosHeader->e_lfanew);
- }
- /*!
- * @brief 由虚拟地址获取偏移距离
- *
- * @param [in]VirtualAddr 虚拟地址
- * /return
- */
- DWORD KPEFile::VirtualToRaw(DWORD VirtualAddr)
- {
- int i;
- PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((LPBYTE)m_pNTHeader + sizeof(IMAGE_NT_HEADERS));
- for( i = 0; i < m_pNTHeader->FileHeader.NumberOfSections; i++)
- {
- if (VirtualAddr >= pSectionHeader->VirtualAddress
- && VirtualAddr < pSectionHeader->VirtualAddress + pSectionHeader->Misc.VirtualSize)
- return pSectionHeader->PointerToRawData + (DWORD)VirtualAddr - (DWORD)pSectionHeader->VirtualAddress;
- pSectionHeader ++;
- }
- return 0;
- }
- /*!
- * @brief 获取所有的导入表
- *
- * @param [in]vecImportDll 导入的DLL的信息结构体
- * /return
- */
- BOOL KPEFile::GetAllImportSymbol(std::vector<stImportDll> &vecImportDll)
- {
- if((m_pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress==0)
- ||(m_pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size==0))
- return FALSE;
- DWORD ITableRAoffset = VirtualToRaw(m_pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
- PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(m_pModule+ITableRAoffset);
- if (pImport==NULL)
- return FALSE;
- DWORD Nameoffset ;
- DWORD OFToffset ;
- DWORD Funcoffset;
- while((Nameoffset = VirtualToRaw(pImport->Name))!= 0)
- {
- stImportDll ImportDll;
- LPCSTR szDllName = reinterpret_cast<LPCSTR>(m_pModule + Nameoffset);
- #ifdef UNICODE
- TCHAR szwszDllName[MAX_PATH];
- ::MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,szDllName,-1,szwszDllName,MAX_PATH);
- ImportDll.m_strDllName = szwszDllName;
- #else
- ImportDll.m_strDllName = szDllName;
- #endif
- PIMAGE_THUNK_DATA32 pThunk;
- OFToffset = VirtualToRaw(pImport->OriginalFirstThunk);
- pThunk = (PIMAGE_THUNK_DATA)(m_pModule + OFToffset);
- for (int i = 0;pThunk->u1.Function;i++)
- {
- if (pThunk->u1.Ordinal&IMAGE_ORDINAL_FLAG32)
- {
- // 按序号导入不予处理
- break;
- }
- else
- {
- PIMAGE_IMPORT_BY_NAME pFuncName;
- Funcoffset = VirtualToRaw(pThunk->u1.AddressOfData);
- pFuncName = (PIMAGE_IMPORT_BY_NAME)(m_pModule + Funcoffset);
- #ifdef UNICODE
- TCHAR szwFunctionName[MAX_PATH];
- ::MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,reinterpret_cast<LPCSTR>(pFuncName->Name),-1,szwFunctionName,MAX_PATH);
- tString strFuncname = szwFunctionName;
- ImportDll.m_vecStrFunname.push_back(strFuncname);
- #else
- tString strFuncname = reinterpret_cast<LPCSTR>(pFuncName->Name);
- ImportDll.m_vecStrFunname.push_back(strFuncname);
- #endif
- }
- pThunk++;
- }
- vecImportDll.push_back(ImportDll);
- pImport++;
- }
- return TRUE;
- }
值得注意的是PE文件内部字符使用的多字节字符集,而VS 2005默认建的工程是使用unicode字符集,因为这个在解释pe文件时遇到一些挫折。
这个KPEFile现已兼容多字节字符集和unicode字符集。为此我还写了一个获取所有导入表的例子,相关源码已经上传,源码下载链接:KPEFile源码下载。
在编写获取PE文件中的所有导入表的时候,如果你观察一些运行结果再联系书本上的知识,你会加深对原有知识的认识。比如下面是一个运行结果:
你比较一下导入的两个DLL:kernel32.dll和msvcp80d.dll,你可以看到导入的kernel32.dll函数都是很正常的字符,但是你看到msvcp80d.dll导入的函数中夹杂了?和@字符,联系所学的C++的知识,你很快明白kernel32.dll是一个C库,编译器没有对其进行名称修饰,而msvcp80d.dll是一个C++库,由于命名空间和虚函数的影响,编译器对其进行了名称修饰。
参考文献:
1. 《Windows图形编程》,作者:feng yuan
2. 《程序员的自我修养--链接、装载与库》,俞甲子、石凡、潘爱民等
3. 读取PE文件的导入表,作者:hoodlum1980 ( 發發 )