原文:http://bbs.pediy.com/showthread.php?p=867885
前面幾篇文章中我已經對PE文件的結構作了一個比較詳細的講解,如果大家還有不清楚的,請參考相關資料,謝謝,下面我開始講解PE文件編程方面的知識~~這樣理論結合實際,我想大家會有一個更深切的理解!
首先我想對《加密與解密》第三版上的PE分析工具實例進行講解,因爲考慮到大多數人還是對C語言比較熟悉,所以先用C語言進行整體講解,在後面的三篇中的我將着重講解PE的Win32彙編的編程,因爲必竟彙編我還是比較熟一點,C語言不是很熟,呵呵!!
其實有了上面的理論講解,基本的算法很簡單了,主要就是對PE格式的各個結構進行定位,這裏我們定義一個MAP_FILE_STRUCT結構來存放有關信息,結構如下:
typedef struct _MAP_FILE_STRUCT
{
HANDLE hFile; ;文件句柄
HANDLE hMapping; ;映射文件句柄
LPVOID ImageBase; ;映像基址
} MAP_FILE_STRUCT;
PE文件格式的檢查
文件格式可能通過PE頭開始的標誌Signature來檢測。檢測DOS Header的Magic Mark不是也可以檢測此PE文件是否合法嗎?但是大家想想如果只檢測一個文件的頭兩個字節是不是MZ,如果一個文件文件的頭兩個字節是MZ,那不是判斷失誤!所以要檢查PE文件的格式有兩個重要的步驟:
判斷文件開始的第一個字段是否爲IMAGE_DOS_SIGNATURE,即5A4Dh
再通過e_lfanew找到IMAGE_NT_HEADERS,判斷Signature字段的值是否爲IMAGE_NT_SIGNATURE,即00004550h,如果是IMAGE_NT_SIGNATURE,就可以認爲該文件是PE格式。
具體代碼實現如下:
BOOL IsPEFile(LPVOID ImageBase)
{
PIMAGE_DOS_HEADER pDH=NULL;
PIMAGE_NT_HEADERS pNtH=NULL;
if(!ImageBase) //判斷映像基址
return FALSE;
pDH=(PIMAGE_DOS_HEADER)ImageBase;
if(pDH->e_magic!=IMAGE_DOS_SIGNATURE) //判斷是否爲MZ
return FALSE;
pNtH=(PIMAGE_NT_HEADERS32)((DWORD)pDH+pDH->e_lfanew); //DOS頭+e_lfanew(03Ch)定位PE文件頭
if (pNtH->Signature != IMAGE_NT_SIGNATURE ) //判斷是否爲PE文件頭PE
return FALSE;
return TRUE;
}
FileHeader和OptionalHeader內容的讀取
IMAGE_NT_HEADERS STRUCT
Signature DWORD ? ;PE文件標識
FileHeader IMAGE_FILE_HEADER <>
OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
IMAGE_NT_HEADERS ENDS
從上面的結構可知,只要得到了IMAGE_NT_HEADERS,根據IMAGE_NT_HEADERS的定義,就可以找到IMAGE_FILE_HEADER和IMAGE_OPTIONAL_HEADER32。
首先我們要得到IMAGE_NT_HEADERS結構指針的函數:
PIMAGE_NT_HEADERS GetNtHeaders(LPVOID ImageBase)
{
if(!IsPEFile(ImageBase)) //通過文件基址來判斷文件是否爲PE文件
return NULL;
PIMAGE_NT_HEADERS pNtH; //定義PE文件頭指針
PIMAGE_DOS_HEADER pDH; //定義DOS頭指針
pDH=(PIMAGE_DOS_HEADER)ImageBase; //得到DOS指針
pNtH=(PIMAGE_NT_HEADERS)((DWORD)pDH+pDH->e_lfanew); //得到PE文件頭指針
return pNtH;
}
上面得到了IMAGE_NT_HEADERS結構的指針,下面我們來得到兩個重要的結構指針:
IMAGE_FILE_HEADER結構的指針,函數如下:
PIMAGE_FILE_HEADER GetFileHeader(LPVOID ImageBase)
{
PIMAGE_NT_HEADERS pNtH = NULL; //定義PE文件頭指針
PIMAGE_NT_HEADERS pFH = NULL; //定義映射文件頭指針
pNtH = GetNtHeaders(ImageBase); //得到PE文件頭指針
if (!pNtH)
return NULL;
pFH=&pNtH->FileHeader; //得到映射文件頭指針
return pFH; //返回IMAGE_FILE_HEADER指針
}
IMAGE_OPTIONAL_HEADER32結構的指針,函數如下:
PIMAGE_OPTIONAL_HEADER GetOptionalHeader(LPVOID ImageBase)
{
PIMAGE_NT_HEADERS pNtH = NULL; //定義PE文件頭指針
PIMAGE__OPTIONAL_HEADER pOH = NULL; //定義可選映射頭指針
pNtH = GetNtHeaders(ImageBase); //得到PE文件頭指針
if (!pNtH)
return NULL;
pOH=&pNtH->OptionalHeader; //得到可選映像頭指針
return pOH; //返回IMAGE_OPTION_HEADER32指針
}
得到了這兩個重要的結構指針之後,其它的事情就變得這樣簡單,我們只需要將FileHeader和OptionalHeader的信息顯示出來,在《加密與解密》第三版中,是把FileHeader和OptionalHeader的信息以十六進制方式顯示在編輯控件上,此時先用函數wsprintf將顯示的值進行格式化,然後調用API函數中的SetDlgItemText即可,代碼如下:
大家先先看看FileHeader的結構如下:
IMAGE_FILE_HEADER STRUCT
Machine WORD ? ;0004h - 運行平臺
NumberOfSections WORD ? ;0006h - 文件的節數目
TimeDateStamp DWORD ? ;0008h - 文件創建日期和時間
PointerToSymbolTable DWORD ? ;000ch - 指向符號表(用於調試)
NumberOfSymbols DWORD ? ;0010h - 符號表中的符號數量(用於調試)
SizeOfOptionalHeader WORD ? ;0014h - IMAGE_OPTIONAL_HEADER32結構的長度
Characteristics WORD ? ;0016h - 文件屬性
IMAGE_FILE_HEADER ENDS
下面編程將上面的各個信息完全顯示出來了!!
void ShowFileHeaderInfo(HWND hWnd)
{
char cBuff[10];
PIMAGE_FILE_HEADER pFH=NULL;
pFH=GetFileHeader(stMapFile.ImageBase); //得到文件頭指針
if(!pFH)
{
MessageBox(hWnd,"Can't get File Header ! :(","PEInfo_Example",MB_OK);
return;
}
wsprintf(cBuff, "%04lX", pFH->Machine); //格式化輸出內容
SetDlgItemText(hWnd,IDC_EDIT_FH_MACHINE,cBuff);
wsprintf(cBuff, "%04lX", pFH->NumberOfSections);
SetDlgItemText(hWnd,IDC_EDIT_FH_NUMOFSECTIONS,cBuff);
wsprintf(cBuff, "%08lX", pFH->TimeDateStamp);
SetDlgItemText(hWnd,IDC_EDIT_FH_TDS,cBuff);
wsprintf(cBuff, "%08lX", pFH->PointerToSymbolTable);
SetDlgItemText(hWnd,IDC_EDIT_FH_PTSYMBOL,cBuff);
wsprintf(cBuff, "%08lX", pFH->NumberOfSymbols);
SetDlgItemText(hWnd,IDC_EDIT_FH_NUMOFSYM,cBuff);
wsprintf(cBuff, "%04lX", pFH->SizeOfOptionalHeader);
SetDlgItemText(hWnd,IDC_EDIT_FH_SIZEOFOH,cBuff);
wsprintf(cBuff, "%04lX", pFH->Characteristics);
SetDlgItemText(hWnd,IDC_EDIT_FH_CHARACTERISTICS,cBuff);
}
再來看看OptionalHeader結構的信息:
IMAGE_OPTIONAL_HEADER32 STRUCT
Magic WORD ? ;0018h 107h=ROM Image,10Bh=exe Image
MajorLinkerVersion BYTE ? ;001ah 鏈接器版本號
MinorLinkerVersion BYTE ? ;001bh
SizeOfCode DWORD ? ;001ch 所有含代碼的節的總大小
SizeOfInitializedData DWORD? ;0020h所有含已初始化數據的節的總大小
SizeOfUninitializedData DWORD ? ;0024h 所有含未初始化數據的節的大小
AddressOfEntryPoint DWORD ? ;0028h 程序執行入口RVA
BaseOfCode DWORD ? ;002ch 代碼的節的起始RVA
BaseOfData DWORD ? ;0030h 數據的節的起始RVA
ImageBase DWORD ? ;0034h 程序的建議裝載地址
SectionAlignment DWORD ? ;0038h 內存中的節的對齊粒度
FileAlignment DWORD ? ;003ch 文件中的節的對齊粒度
MajorOperatingSystemVersion WORD ? ;0040h 操作系統主版本號
MinorOperatingSystemVersion WORD ? ;0042h 操作系統副版本號
MajorImageVersion WORD ? ;0044h可運行於操作系統的最小版本號
MinorImageVersion WORD ? ;0046h
MajorSubsystemVersion WORD ?;0048h 可運行於操作系統的最小子版本號
MinorSubsystemVersion WORD ? ;004ah
Win32VersionValue DWORD ? ;004ch 未用
SizeOfImage DWORD ? ;0050h 內存中整個PE映像尺寸
SizeOfHeaders DWORD ? ;0054h 所有頭+節表的大小
CheckSum DWORD ? ;0058h
Subsystem WORD ? ;005ch 文件的子系統
DllCharacteristics WORD ? ;005eh
SizeOfStackReserve DWORD ? ;0060h 初始化時的堆棧大小
SizeOfStackCommit DWORD ? ;0064h 初始化時實際提交的堆棧大小
SizeOfHeapReserve DWORD ? ;0068h 初始化時保留的堆大小
SizeOfHeapCommit DWORD ? ;006ch 初始化時實際提交的堆大小
LoaderFlags DWORD ? ;0070h 未用
NumberOfRvaAndSizes DWORD ? ;0074h 下面的數據目錄結構的數量
DataDirectory IMAGE_DATA_DIRECTORY 16 dup(<>) ;0078h
IMAGE_OPTIONAL_HEADER32 ENDS
代碼和上面是一樣的,只是未顯示完全,只是顯示了幾個重要的字段內容!!
得到數據目錄表的信息:
數據目錄表(DataDirectory)由一組數組構成,每組項目包括執行文件的重要部分的起媽RVA和長度。因爲數據目錄有16項,書有用了一種簡單的方法,就是定義一個編輯控件ID的結構數組,用一個循環就可以了。
我們先來看一個DataDirectory的結構
IMAGE_DATA_DIRECTORY STRUCT
VirtualAddress DWORD ? ;數據的起始RVA
Size DWORD ? ;數據塊的長度
IMAGE_DATA_DIRECTORY ENDS
很簡單就兩個字段,我們就先定義一個結構體用於存放這兩個字段,然後在定義一個結構體數組就於存放十六個結構體
typedef struct
{
UINT ID_RVA; //用於存放DataDirectory數據塊的起始RVA
UINT ID_SIZE; //用於存放DataDirectory數據塊的大小
} DataDir_EditID;
DataDir_EditID EditID_Array[]=
{
{IDC_EDIT_DD_RVA_EXPORT, IDC_EDIT_DD_SIZE_EXPORT},
{IDC_EDIT_DD_RVA_IMPORT, IDC_EDIT_DD_SIZE_IMPORT},
{IDC_EDIT_DD_RVA_RES, IDC_EDIT_DD_SZIE_RES},
{IDC_EDIT_DD_RVA_EXCEPTION, IDC_EDIT_DD_SZIE_EXCEPTION},
{IDC_EDIT_DD_RVA_SECURITY, IDC_EDIT_DD_SIZE_SECURITY},
{IDC_EDIT_DD_RVA_RELOC, IDC_EDIT_DD_SIZE_RELOC},
{IDC_EDIT_DD_RVA_DEBUG, IDC_EDIT_DD_SIZE_DEBUG},
{IDC_EDIT_DD_RVA_COPYRIGHT, IDC_EDIT_DD_SIZE_COPYRIGHT},
{IDC_EDIT_DD_RVA_GP, IDC_EDIT_DD_SIZE_GP},
{IDC_EDIT_DD_RVA_TLS, IDC_EDIT_DD_SIZE_TLS},
{IDC_EDIT_DD_RVA_LOADCONFIG, IDC_EDIT_DD_SIZE_LOADCONFIG},
{IDC_EDIT_DD_RVA_IAT, IDC_EDIT_DD_SIZE_IAT},
{IDC_EDIT_DD_RVA_BOUND, IDC_EDIT_DD_SIZE_BOUND},
{IDC_EDIT_DD_RVA_COM, IDC_EDIT_DD_SIZE_COM},
{IDC_EDIT_DD_RVA_DELAYIMPORT,IDC_EDIT_DD_SIZE_DELAYIMPORT},
{IDC_EDIT_DD_RVA_NOUSE, IDC_EDIT_DD_SIZE_NOUSE}
};
上面正是定義了十六個DataDirectory,這裏用一個數組表示,主要是爲了方便編程時使用,以避免代碼的冗長!!!
顯示數據目錄表的函數如下:
void ShowDataDirInfo(HWND hDlg)
{
char cBuff[9];
PIMAGE_OPTIONAL_HEADER pOH=NULL;
pOH=GetOptionalHeader(stMapFile.ImageBase); //得到IMAGE_OPTION_HEADER32的結構指針
if(!pOH)
return;
for(int i=0;i<16;i++) //循環顯示數據目錄表的十六個元素
{
wsprintf(cBuff, "%08lX", pOH->DataDirectory[i].VirtualAddress); //格式化DataDirectory中數據塊的RVA
SetDlgItemText(hDlg,EditID_Array[i].ID_RVA,cBuff); //設置DataDirectory中數據塊的RVA
wsprintf(cBuff, "%08lX", pOH->DataDirectory[i].Size); //格式化DataDirectory中數據塊的Size
SetDlgItemText(hDlg,EditID_Array[i].ID_SIZE,cBuff); //設置DataDirectory中數據塊的Size
}
}
得到區塊表信息
緊接IMAGE_NT_HEADERS以後就是區塊表(Section Table)了,Section Table則是由IMAGE_SECTION_HEADER組成的數組。如何得到Section Table的位置呢?換名話說,也就是如何得到第一個IMAGE_SECTION_HEADER的位置。在Visual C++中,可以利用IMAGE_FIRST_SECTION宏來輕鬆得到第一個IMAGE_SECTION_HEADER的位置。(這裏先講在VC中得到區塊表,到後面會具體講在彙編中如何得到)
又因爲區塊的個數已經在文件頭中指明瞭,所以只要得到第一個區塊的位置,然後再利用一個循環語句就可以得到所有區塊的信息了。
請看下面的函數就是利用IMAGE_FIRST_SECTION宏得到區塊表的起始位置。
PIMAGE_SECTION_HEADER GetFirstSectionHeader(PIMAGE_NT_HEADERS pNtH)
{
PIMAGE_SECTION_HEADER pSH; //定義區塊表首地址指針
pSH = IMAGE_FIRST_SECTION(pNtH); //得到區塊表首地址指針
return pSH; //返回區塊首地址指針
}
這裏必須強調一下,在一個PE文件中,OptionHeader的大小是可以變化的,雖然它的大小通常爲E0h,但是總是有例外,原因是可選文件頭的大小是由文件頭的SizeOfOptionalHeader字段指定的,並不是個固定值。這也是IMAGE_FIRST_SECTION宏對於可選文件頭的大小爲什麼不直接用固定值的原因。系統的PE加載器在加載PE文件的時候,也是利用了文件頭中的SizeOfOptionalHeader字段的值來定位區塊表的,而不是用固定值。能否正確定位到區塊表,取決於SizeOfOptionalHeader字段的值的正確性。這是個很容易被忽略的問題,因些導致一些程序的BUG。書中使用ListView控件來顯示PE文件頭的區段信息。具體代碼如下:
void ShowSectionHeaderInfo(HWND hDlg)
{
LVITEM lvItem;
char cBuff[9],cName[9];
WORD i;
PIMAGE_FILE_HEADER pFH=NULL; //定義映射頭指針
PIMAGE_SECTION_HEADER pSH=NULL; //定義區塊表指針
pFH=GetFileHeader(stMapFile.ImageBase); //得到映射頭的指針主要是爲了得到區塊的數目通過NumberOfSections字段
if(!pFH)
return;
pSH=GetFirstSectionHeader(stMapFile.ImageBase); //得到第一個區塊表指針
for( i=0;i<pFH->NumberOfSections;i++) //循環得到各區塊表的指針
{
memset(&lvItem, 0, sizeof(lvItem));
lvItem.mask = LVIF_TEXT;
lvItem.iItem = i;
memset(cName,0,sizeof(cName)); //設置區塊表中的各個字段的值
memcpy(cName, pSH->Name, 8);
lvItem.pszText = cName;
SendDlgItemMessage(hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,(LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", pSH->VirtualAddress);
lvItem.iSubItem = 1;
SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
wsprintf(cBuff, "%08lX", pSH->Misc.VirtualSize);
lvItem.iSubItem = 2;
SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
wsprintf(cBuff, "%08lX", pSH->PointerToRawData);
lvItem.iSubItem = 3;
SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
wsprintf(cBuff, "%08lX", pSH->SizeOfRawData);
lvItem.iSubItem = 4;
SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
wsprintf(cBuff, "%08lX", pSH->Characteristics);
lvItem.iSubItem = 5;
SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
++pSH; //指向下一個區塊位置
}
}
得到輸出表信息
輸出表(Export Table)中的主要成分是一個表格,內含函數名稱,輸出序數等。輸出表是數據目錄表的第一個成員,其指向IMAGE_EXPORT_DIRECTORY結構。輸出函數的個數是由結構IMAGE_EXPORT_DIRECTORY的字段NumberOfFunctions來說明的。實際上,也有例外,例如在寫一個DLL的時候,可以用DEF文件來制定輸出函數的名稱,序號等。請看下面這個DEF文件內容:
LIBRARY TEST
EXPORTS
Func1 @1
Func2 @12
Func3 @18
Func4 @23
Func5 @31
在這個文件中,共輸出了五個函數(Func1到Func5),而輸出函數的序號卻是1到31,如果沒有考慮到這一點的話,很有可能會在這裏出錯,因爲這時IMAGE_EXPORT_DIRECTORY的字段NumberOfFunctions的值爲0x1F,即31。如果認爲NumberOfFunctions值就爲輸出函數個數的話,就錯了。
首先通過下面的兩個函數來得到輸出表的指針
LPVOID GetDirectoryEntryToData(LPVOID ImageBase,USHORT DirectoryEntry)
{
DWORD dwDataStartRVA;
LPVOID pDirData=NULL;
PIMAGE_NT_HEADERS pNtH=NULL;
PIMAGE_OPTIONAL_HEADER pOH=NULL;
pNtH=GetNtHeaders(ImageBase);
if(!pNtH)
return NULL;
pOH=GetOptionalHeader(ImageBase);
if(!pOH)
return NULL;
dwDataStartRVA=pOH->DataDirectory[DirectoryEntry].VirtualAddress;
if(!dwDataStartRVA)
return NULL;
pDirData=RvaToPtr(pNtH,ImageBase,dwDataStartRVA);
if(!pDirData)
return NULL;
return pDirData;
}
PIMAGE_EXPORT_DIRECTORY GetExportDirectory(LPVOID ImageBase)
{
PIMAGE_EXPORT_DIRECTORY pExportDir=NULL;
pExportDir=(PIMAGE_EXPORT_DIRECTORY)GetDirectoryEntryToData(ImageBase,IMAGE_DIRECTORY_ENTRY_EXPORT);
if(!pExportDir)
return NULL;
return pExportDir;
}
PIMAGE_IMPORT_DESCRIPTOR GetFirstImportDesc(LPVOID ImageBase)
{
PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
pImportDesc=(PIMAGE_IMPORT_DESCRIPTOR)GetDirectoryEntryToData(ImageBase,IMAGE_DIRECTORY_ENTRY_IMPORT);
if(!pImportDesc)
return NULL;
return pImportDesc;
}
顯示輸出表信息的函數如下:
void ShowExportFuncsInfo(HWND hDlg)
{
HWND hList;
LVITEM lvItem;
char cBuff[10], *szFuncName;
UINT iNumOfName=0;
PDWORD pdwRvas, pdwNames;
PWORD pwOrds;
UINT i=0,j=0,k=0;
BOOL bIsByName=FALSE;;
PIMAGE_NT_HEADERS pNtH=NULL;
PIMAGE_EXPORT_DIRECTORY pExportDir=NULL;
pNtH=GetNtHeaders(stMapFile.ImageBase);
if(!pNtH)
return ;
pExportDir= (PIMAGE_EXPORT_DIRECTORY)GetExportDirectory(stMapFile.ImageBase); //調用GetExprotDirectory來得到輸出表的首地址指針
if (!pExportDir)
return ;
pwOrds=(PWORD)RvaToPtr(pNtH,stMapFile.ImageBase,pExportDir->AddressOfNameOrdinals); //指向輸出序列號數組
pdwRvas=(PDWORD)RvaToPtr(pNtH,stMapFile.ImageBase,pExportDir->AddressOfFunctions); //指向函數地址數組
pdwNames=(PDWORD)RvaToPtr(pNtH,stMapFile.ImageBase,pExportDir->AddressOfNames); //函數名字的指針地址
if(!pdwRvas) //如果函數地址數組爲NULL,則直接返回
return;
hList=GetDlgItem(hDlg,IDC_EXPORT_LIST);
SendMessage(hList,LVM_SETEXTENDEDLISTVIEWSTYLE,0,(LPARAM)LVS_EX_FULLROWSELECT);
iNumOfName=pExportDir->NumberOfNames; //得到函數名字的指針地址陣列中的元素個數
for( i=0;i<pExportDir->NumberOfFunctions;i++) //得到函數地址數組陣列中的元素個數
{
if(*pdwRvas) //如果函數地址數組中的值不爲NULL,則繼續顯示,否則指向函數地址數組中下一個
{
for( j=0;j<iNumOfName;j++) //以函數名字指針地址陣列中的元素個數爲循環
{
if(i==pwOrds[j]) //如果函數地址數組的值等於函數名字的指針地址中元素j的值
{
bIsByName=TRUE;
szFuncName=(char*)RvaToPtr(pNtH,stMapFile.ImageBase,pdwNames[j]);
break;
}
bIsByName=FALSE;
}
//show funcs to listctrl
memset(&lvItem, 0, sizeof(lvItem));
lvItem.mask = LVIF_TEXT;
lvItem.iItem = k;
lvItem.pszText = cBuff;
wsprintf(cBuff, "%04lX", (UINT)(pExportDir->Base+i));
SendDlgItemMessage(hDlg,IDC_EXPORT_LIST,LVM_INSERTITEM,0,(LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", (*pdwRvas));
lvItem.iSubItem = 1;
SendDlgItemMessage(hDlg,IDC_EXPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
if(bIsByName)
lvItem.pszText=szFuncName;
else
lvItem.pszText = "-";
lvItem.iSubItem = 2;
SendDlgItemMessage(hDlg,IDC_EXPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
//
++k;
}
++pdwRvas;
}
}
得到輸入表的信息
數據目錄表第二個成員指向輸入表。輸入表以一個IMAGE_IMPORT_DESCRIPTOR結構開始,以一個空的IMAGE_IMPORT_DESCRIPTOR結構結束。在這裏可以通過GetFirstImportDesc函數得到ImportTable在文件中的位置。
GetFirstImportDesc函數的定義如下:
PIMAGE_IMPORT_DESCRIPTOR GetFirstImportDesc(LPVOID ImageBase)
{
PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
pImportDesc=(PIMAGE_IMPORT_DESCRIPTOR)GetDirectoryEntryToData(ImageBase,IMAGE_DIRECTORY_ENTRY_IMPORT);
if(!pImportDesc)
return NULL;
return pImportDesc;
}
這個函數同樣用到了上面所使用的GetDirectoryEntryToData,這個函數是用於專門得到區塊表的各個數據塊的位置而準備的,我們找到了輸入表的位置,可以通過一個循環來得到整個輸入表,循環終止的條件是IMAGE_IMPORT_DESCRIPTOR結構爲空。
void ShowImportDescInfo(HWND hDlg)
{
HWND hList;
LVITEM lvItem;
char cBuff[10], * szDllName;
PIMAGE_NT_HEADERS pNtH=NULL;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc=NULL;
memset(&lvItem, 0, sizeof(lvItem));
hList=GetDlgItem(hDlg,IDC_IMPORT_LIST);
SendMessage(hList,LVM_SETEXTENDEDLISTVIEWSTYLE,0,(LPARAM)LVS_EX_FULLROWSELECT);
pNtH=GetNtHeaders(stMapFile.ImageBase);
pImportDesc=GetFirstImportDesc(stMapFile.ImageBase);
if(!pImportDesc)
{
MessageBox(hDlg,"Can't get ImportDesc:(","PEInfo_Example",MB_OK);
return;
}
int i=0;
while(pImportDesc->FirstThunk)
{
memset(&lvItem, 0, sizeof(lvItem));
lvItem.mask = LVIF_TEXT;
lvItem.iItem = i;
szDllName=(char*)RvaToPtr(pNtH,stMapFile.ImageBase,pImportDesc->Name);
lvItem.pszText = szDllName;
SendDlgItemMessage(hDlg,IDC_IMPORT_LIST,LVM_INSERTITEM,0,(LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", pImportDesc->OriginalFirstThunk);
lvItem.iSubItem = 1;
SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", pImportDesc->TimeDateStamp);
lvItem.iSubItem = 2;
SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", pImportDesc->ForwarderChain);
lvItem.iSubItem = 3;
SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", pImportDesc->Name);
lvItem.iSubItem = 4;
SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", pImportDesc->FirstThunk);
lvItem.iSubItem = 5;
SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
++i;
++pImportDesc;
}
}
在ShowImportDescInfo函數中,首先用GetFirstImportDesc函數得到指向第一個IMAGE_IMPORT_DESCRIPTOR結構和指針pImportDesc,以pImportDesc-->FirstThunk爲真來作爲循環的條件,循環得到ImportTable的各項信息。
通過上面的ShowImportDescInfo函數,可以得到PE文件所引入的DLL的信息,接下來的任務就是如何分析得到通過DLL所輸入的函數的信息,這裏必須通過IMAGE_IMPORT_DESCRIPTOR所提供的信息來得到輸入的函數的信息。可以通過名字和序號來引入所用的函數,怎麼來區分一個函數是如何引入的呢?在於IMAGE_THUNK_DATA值的高位,如果被置位了,低31位被看作是一個序數值。如果高位沒有被置位,IMAGE_THUNK_DATA值是一個指向IMAGE_IMPORT_BY_NAME的RVA。如果兩者都不是,則可以認爲IMAGE_THUNK_DATA值爲函數的內存地址。具體參考下面的ShowImportFuncsByDllIndex(HWND hDlg,int index)函數:
void ShowImportFuncsByDllIndex(HWND hDlg,int index)
{
HWND hFuncList;
LVITEM lvItem;
char cBuff[30],cOrd[30],cMemAddr[30], * szFuncName;
DWORD dwThunk, *pdwThunk=NULL, *pdwRVA=NULL;
int i=0;
PIMAGE_NT_HEADERS pNtH=NULL;
PIMAGE_IMPORT_DESCRIPTOR pFistImportDesc=NULL,pCurrentImportDesc=NULL;
PIMAGE_IMPORT_BY_NAME pByName=NULL;
memset(&lvItem, 0, sizeof(lvItem));
hFuncList=GetDlgItem(hDlg,IDC_IMPORTFUNCTIONS_LIST);
SendMessage(hFuncList,LVM_SETEXTENDEDLISTVIEWSTYLE,0,(LPARAM)LVS_EX_FULLROWSELECT);
SendMessage(hFuncList,LVM_DELETEALLITEMS ,0,0);
pNtH=GetNtHeaders(stMapFile.ImageBase);
pFistImportDesc=GetFirstImportDesc(stMapFile.ImageBase);
pCurrentImportDesc=&pFistImportDesc[index];
dwThunk=GETTHUNK(pCurrentImportDesc);
pdwRVA=(DWORD *)dwThunk;
pdwThunk=(DWORD*)RvaToPtr(pNtH,stMapFile.ImageBase,dwThunk);
if(!pdwThunk)
return;
while(*pdwThunk)
{
memset(&lvItem, 0, sizeof(lvItem));
lvItem.mask = LVIF_TEXT;
lvItem.iItem = i;
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX",(DWORD)pdwRVA);
SendDlgItemMessage(hDlg,IDC_IMPORTFUNCTIONS_LIST,LVM_INSERTITEM,0,(LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", (DWORD)(*pdwThunk));
lvItem.iSubItem = 1;
SendDlgItemMessage(hDlg,IDC_IMPORTFUNCTIONS_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
if (HIWORD(*pdwThunk)==0x8000) //如果最高位被置位了,那麼低31位是一個序數值
{
strcpy(cBuff,"-");
wsprintf(cOrd, "Ord:%08lX",IMAGE_ORDINAL32(*pdwThunk));
szFuncName=cOrd;
}
else //如果最高位沒有被置位IMAGE_THUNK_DATA值是指向IMAGE_IMPORT_BY_NAME的RVA
{
pByName =(PIMAGE_IMPORT_BY_NAME)RvaToPtr(pNtH,stMapFile.ImageBase,(DWORD)(*pdwThunk));
if(pByName)
{
wsprintf(cBuff,"%04lX",pByName->Hint);
szFuncName=(char *)pByName->Name;
}
else //如果兩者都不是,則可以認爲IMAGE_THUNK_DATA值爲函數的內存地址
{
strcpy(cBuff,"-");
wsprintf(cMemAddr, "MemAddr:%08lX",(DWORD)(*pdwThunk));
szFuncName=cMemAddr;
}
}
lvItem.pszText = cBuff;
lvItem.iSubItem = 2;
SendDlgItemMessage(hDlg,IDC_IMPORTFUNCTIONS_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
lvItem.pszText = szFuncName;
lvItem.iSubItem = 3;
SendDlgItemMessage(hDlg,IDC_IMPORTFUNCTIONS_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
++i;
++pdwRVA;
++pdwThunk;
}
}
到此,一個PE的簡單工具的核心代碼就基本上分析完畢了,其實當你真正瞭解了前面三章所講的內容,再去看這些代碼,你會覺得很簡單是不是,只要你花時間,你也可以寫一人簡單的PE分析工具,呵呵,如果還沒有弄明白的,我向大家推薦《加密與解密》第三版第十章的PE工具編寫的源代碼,大家可以再仔細研究研究,由於我的C不是很好,這時我就先說到這裏吧,明天我會繼續給大家講解PE編程方面的問題,我相信一定會讓大家對PE編程更加清楚明白!!
上篇文章有位朋友說要寫上參考文獻-----《加密與解密》第三版第十章PE工具的編寫