鏈接:
PE文件格式的一些研究 最近抽空對PE文件格式做了一些研究。眾所周知,PE文件格式是Windows平臺下可執(zhí)行文件的格式。為什么要研究PE文件格式?可能有人認(rèn)為,做這件事就是一件重復(fù)造輪子的事,因為之前已經(jīng)有無數(shù)人做過這樣的事。但是有些事不是簡單地以是不是重復(fù)造輪子來衡量的。研究PE文件格式對加深程序本質(zhì)的認(rèn)識和理解程序的構(gòu)成都有很大的好處。美籍匈牙利科學(xué)家馮•諾依曼最新提出程序存儲的思想,具體到研究PE文件格式,或許可以是運(yùn)行程序所需的指令和數(shù)據(jù)是以怎樣的組織結(jié)構(gòu)存貯在文件的,在運(yùn)行時程序是怎樣被加載的,數(shù)據(jù)是怎樣初始化的。
一個完整的PE文件結(jié)構(gòu)一般由五大部分組成。如下圖:
最開頭的是部分是DOS部首,DOS部首由兩部分組成:DOS的MZ文件標(biāo)志和DOS stub(DOS存根程序)。之所以設(shè)置DOS部首是微軟為了兼容原有的DOS系統(tǒng)下的程序而設(shè)立的。
緊接著的是真正的PE文件頭。值得注意的是PE文件頭中的IMAGE_OPTIONAL_HEADER32是一個非常重要的結(jié)構(gòu),PE文件中的導(dǎo)入表、導(dǎo)出表、資源、重定位表等數(shù)據(jù)的位置和長度都保存在這個結(jié)構(gòu)里。
PE文件結(jié)構(gòu)中可研究的內(nèi)容很多,暫時講解這么多,有興趣的朋友可以閱讀《Windows圖形編程》中的第一章和《程序員的自我修養(yǎng)》的5.6節(jié)。
研究PE文件格式,最好的方法我覺得還是自己動手寫一個PE文件解釋類??戳恕禬indows圖形編程》中的第一章,加深自己對PE文件結(jié)構(gòu)的理解,我決定在袁峰大俠(《Windows圖形編程》一書作者)編寫PE文件解釋類KPEFile的基礎(chǔ)上增加一些接口。KPEFile類的構(gòu)造函數(shù)是通過提供模塊句柄來獲取PE文件信息的,我發(fā)現(xiàn)這對運(yùn)行中的程序是有用的,但對解析一個靜態(tài)的exe文件并不有效。
編程實現(xiàn)分析一個PE文件,網(wǎng)上一般有兩種做法:一是打開exe文件,然后利用Dbghelp庫的一個函數(shù)ImageRvaToVa來獲取你要打開結(jié)構(gòu)的指針;另一種做法也大同小異,也是打開exe文件,自己計算所有獲取信息的結(jié)構(gòu)的偏移地址。我采用的是第二種做法。
下面是重要部分代碼:
- C/C++ code
/*!* @brief tString類主要是為了兼容unicode字符集和多字節(jié)字符集***/#ifdef UNICODE#define tString std::wstring#else#define tString std::string #endif/*! @struct stImportDll* @brief 導(dǎo)入的DLL的信息結(jié)構(gòu)體***/struct stImportDll{/* @brief 導(dǎo)入的DLL名***/tString m_strDllName;/* @brief 導(dǎo)入的DLL中的函數(shù)***/std::vector<tString> m_vecStrFunname; //};/*!* @brief KPEFile類構(gòu)造函數(shù)** @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 ;}// 開辟讀取緩沖區(qū) m_pModule = (LPBYTE)VirtualAlloc( NULL,dwSize,MEM_COMMIT,PAGE_READWRITE);// 讀取文件 ReadFile(hFile,m_pModule,dwSize,&dwRead,NULL);if( dwRead != dwSize)return ;// 關(guān)閉文件 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 獲取所有的導(dǎo)入表** @param [in]vecImportDll 導(dǎo)入的DLL的信息結(jié)構(gòu)體* \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 UNICODETCHAR szwszDllName[MAX_PATH];::MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,szDllName,-1,szwszDllName,MAX_PATH);ImportDll.m_strDllName = szwszDllName;#elseImportDll.m_strDllName = szDllName;#endifPIMAGE_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){// 按序號導(dǎo)入不予處理 break;}else{PIMAGE_IMPORT_BY_NAME pFuncName;Funcoffset = VirtualToRaw(pThunk->u1.AddressOfData);pFuncName = (PIMAGE_IMPORT_BY_NAME)(m_pModule + Funcoffset);#ifdef UNICODETCHAR 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);#elsetString strFuncname = reinterpret_cast<LPCSTR>(pFuncName->Name);ImportDll.m_vecStrFunname.push_back(strFuncname);#endif}pThunk++;}vecImportDll.push_back(ImportDll);pImport++;}return TRUE;}
值得注意的是PE文件內(nèi)部字符使用的多字節(jié)字符集,而VS 2005默認(rèn)建的工程是使用unicode字符集,因為這個在解釋pe文件時遇到一些挫折。
這個KPEFile現(xiàn)已兼容多字節(jié)字符集和unicode字符集。為此我還寫了一個獲取所有導(dǎo)入表的例子,相關(guān)源碼已經(jīng)上傳,源碼下載鏈接:
KPEFile源碼下載。
在編寫獲取PE文件中的所有導(dǎo)入表的時候,如果你觀察一些運(yùn)行結(jié)果再聯(lián)系書本上的知識,你會加深對原有知識的認(rèn)識。比如下面是一個運(yùn)行結(jié)果:
你比較一下導(dǎo)入的兩個DLL:kernel32.dll和msvcp80d.dll,你可以看到導(dǎo)入的kernel32.dll函數(shù)都是很正常的字符,但是你看到msvcp80d.dll導(dǎo)入的函數(shù)中夾雜了?和@字符,聯(lián)系所學(xué)的C++的知識,你很快明白kernel32.dll是一個C庫,編譯器沒有對其進(jìn)行名稱修飾,而msvcp80d.dll是一個C++庫,由于命名空間和虛函數(shù)的影響,編譯器對其進(jìn)行了名稱修飾。
參考文獻(xiàn):
1.《Windows圖形編程》,作者:feng yuan
2.《程序員的自我修養(yǎng)——鏈接、裝載與庫》,俞甲子、石凡、潘愛民等
3.
讀取PE文件的導(dǎo)入表,作者:hoodlum1980 ( 發(fā)發(fā) )