pe文件簡單分析

原文: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工具的編寫
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章