【原创】国庆PE总复习(四)

来源:互联网 发布:windows update不更新 编辑:程序博客网 时间:2024/05/02 01:42
昨天感冒了,今天鼻子也不是很舒服,不过不想失约,承诺了就一定要做下去,七篇文章一定会在国庆结束前全部献上,不管评论是好与坏,至少我懂得做为男人既已承诺,就一定要现!!


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