前面几篇文章中我已经对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工具的编写