《C++ 黑客编程揭秘与防范(第2版)》——6.4 PE相关编程实例

    xiaoxiao2024-05-09  6

    本节书摘来自异步社区出版社《C++ 黑客编程揭秘与防范(第2版)》一书中的第6章,第6.4节,作者:冀云,更多章节内容可以访问云栖社区“异步社区”公众号查看。

    6.4 PE相关编程实例

    C++ 黑客编程揭秘与防范(第2版)前面讲的都是概念性的知识,本节主要编写一些关于PE文件结构的程序代码,以帮助读者加强对PE结构的了解。

    6.4.1 PE查看器

    写PE查看器并不是件复杂的事情,只要按照PE结构一步一步地解析就可以了。下面简单地解析其中几个字段内容,显示一下节表的信息,其余的内容只要稍作修改即可。PE查看器的界面如图6-26所示。

    PE查看器的界面按照图6-26所示的设置,不过这个可以按照个人的偏好进行布局设置。编写该PE查看器的步骤为打开文件并创建文件内存映像,判断文件是否为PE文件并获得PE格式相关结构体的指针,解析基本的PE字段,枚举节表,最后关闭文件。需要在类中添加几个成员变量及成员函数,添加的内容如图6-27所示。

    按照前面所说的顺序,依次实现添加的各个成员函数。

    BOOL CPeParseDlg::FileCreate(char *szFileName) {   BOOL bRet = FALSE;   m_hFile = CreateFile(szFileName,              GENERIC_READ | GENERIC_WRITE,              FILE_SHARE_READ,              NULL,              OPEN_EXISTING,              FILE_ATTRIBUTE_NORMAL,              NULL);   if ( m_hFile == INVALID_HANDLE_VALUE )   {     return bRet;   }   m_hMap = CreateFileMapping(m_hFile, NULL,                 PAGE_READWRITE | SEC_IMAGE,                 0, 0, 0);   if ( m_hMap == NULL )   {     CloseHandle(m_hFile);     return bRet;   }   m_lpBase = MapViewOfFile(m_hMap,                FILE_MAP_READ | FILE_SHARE_WRITE,                0, 0, 0);   if ( m_lpBase == NULL )   {     CloseHandle(m_hMap);     CloseHandle(m_hFile);     return bRet;   }   bRet = TRUE;   return bRet; }``` 这个函数的主要功能是打开文件并创建内存文件映像。通常对文件进行连续读写时直接使用ReadFile()和WriteFile()两个函数。当不连续操作文件时,每次在ReadFile()或者WriteFile()后就要使用SetFilePointer()来调整文件指针的位置,这样的操作较为繁琐。内存文件映像的作用是把整个文件映射入进程的虚拟空间中,这样操作文件就像操作内存变量或内存数据一样方便。 创建内存文件映像所使用的函数有两个,分别是CreateFileMapping()和MapViewOfFile()。CreateFileMapping()函数的定义如下:

    HANDLE CreateFileMapping( HANDLE hFile,               // handle to file LPSECURITY_ATTRIBUTES lpAttributes,    // security DWORD flProtect,             // protection DWORD dwMaximumSizeHigh,          // high-order DWORD of size DWORD dwMaximumSizeLow,           // low-order DWORD of size LPCTSTR lpName               // object name);`参数说明如下。

    hFile:该参数是CreateFile()函数返回的句柄。

    lpAttributes:是安全属性,该值通常是NULL。

    flProtect:创建文件映射后的属性,通常设置为可读可写PAGE_READWRITE。如果需要像装载可执行文件那样把文件映射入内存的话,那么需要使用SEC_IMAGE。

    最后3个参数在这里为0。如果创建的映射需要在多进程中共享数据的话,那么最后一个参数设定为一个字符串,以便通过该名称找到该块共享内存。

    该函数的返回值为一个内存映射的句柄。

    MapViewOfFile()函数的定义如下: LPVOID MapViewOfFile(  HANDLE hFileMappingObject,      // handle to file-mapping object  DWORD dwDesiredAccess,        // access mode  DWORD dwFileOffsetHigh,       // high-order DWORD of offset  DWORD dwFileOffsetLow,       // low-order DWORD of offset  SIZE_T dwNumberOfBytesToMap     // number of bytes to map );``` 参数说明如下。 hFileMappingObject:该参数为CreateFileMapping()返回的句柄。 dwDesiredAccess:想获得的访问权限,通常情况下也是可读可写FILE_MAP_READ、FILE_MAP_ WRITE。 最后3个参数一般给0值就可以了。 按照编程的规矩,打开要关闭,申请要释放。CreateFileMapping()的关闭需要使用CloseHandle()函数。MapViewOfFile()的关闭,要使用UnmapViewOfFile()函数,该函数的定义如下:

    BOOL UnmapViewOfFile( LPCVOID lpBaseAddress  // starting address);`该函数的参数就是MapViewOfFile()函数的返回值。

    接着说PE查看器,文件已经打开,就要判断文件是否为有效的PE文件了。如果是有效的PE文件,就把解析PE格式的相关结构体的指针也得到。代码如下:

    BOOL CPeParseDlg::IsPeFileAndGetPEPointer() {   BOOL bRet = FALSE;   // 判断是否为MZ头   m_pDosHdr = (PIMAGE_DOS_HEADER)m_lpBase;   if ( m_pDosHdr->e_magic != IMAGE_DOS_SIGNATURE )   {     return bRet;   }   // 根据IMAGE_DOS_HEADER的e_lfanew的值得到PE头的位置   m_pNtHdr = (PIMAGE_NT_HEADERS)((DWORD)m_lpBase + m_pDosHdr->e_lfanew);   // 判断是否为PE\0\0   if ( m_pNtHdr->Signature != IMAGE_NT_SIGNATURE )   {     return bRet;   }   // 获得节表的位置   m_pSecHdr = (PIMAGE_SECTION_HEADER)((DWORD)&(m_pNtHdr->OptionalHeader)         + m_pNtHdr->FileHeader.SizeOfOptionalHeader);   bRet = TRUE;   return bRet; }``` 这段代码应该非常容易理解,继续看解析PE格式的部分。

    VOID CPeParseDlg::ParseBasePe(){  CString StrTmp;

      // 入口地址  StrTmp.Format("X", m_pNtHdr->OptionalHeader.AddressOfEntryPoint);  SetDlgItemText(IDC_EDIT_EP, StrTmp);

      // 映像基地址  StrTmp.Format("X", m_pNtHdr->OptionalHeader.ImageBase);  SetDlgItemText(IDC_EDIT_IMAGEBASE, StrTmp);

      // 连接器版本号  StrTmp.Format("%d.%d",     m_pNtHdr->OptionalHeader.MajorLinkerVersion,    m_pNtHdr->OptionalHeader.MinorLinkerVersion);  SetDlgItemText(IDC_EDIT_LINKVERSION, StrTmp);

      // 节表数量  StrTmp.Format("X", m_pNtHdr->FileHeader.NumberOfSections);  SetDlgItemText(IDC_EDIT_SECTIONNUM, StrTmp);

      // 文件对齐值大小  StrTmp.Format("X", m_pNtHdr->OptionalHeader.FileAlignment);  SetDlgItemText(IDC_EDIT_FILEALIGN, StrTmp);

      // 内存对齐值大小  StrTmp.Format("X", m_pNtHdr->OptionalHeader.SectionAlignment);  SetDlgItemText(IDC_EDIT_SECALIGN, StrTmp);}`PE格式的基础信息,就是简单地获取结构体的成员变量,没有过多复杂的内容。获取导入表、导出表比获取基础信息复杂。关于导入表、导出表的内容将在后面介绍。接下来进行节表的枚举,具体代码如下:

    VOID CPeParseDlg::EnumSections() {   int nSecNum = m_pNtHdr->FileHeader.NumberOfSections;   int i = 0;   CString StrTmp;   for ( i = 0; i < nSecNum; i ++ )   {     m_SectionLIst.InsertItem(i, (const char *)m_pSecHdr[i].Name);     StrTmp.Format("X", m_pSecHdr[i].VirtualAddress);     m_SectionLIst.SetItemText(i, 1, StrTmp);     StrTmp.Format("X", m_pSecHdr[i].Misc.VirtualSize);     m_SectionLIst.SetItemText(i, 2, StrTmp);     StrTmp.Format("X", m_pSecHdr[i].PointerToRawData);     m_SectionLIst.SetItemText(i, 3, StrTmp);     StrTmp.Format("X", m_pSecHdr[i].SizeOfRawData);     m_SectionLIst.SetItemText(i, 4, StrTmp);     StrTmp.Format("X", m_pSecHdr[i].Characteristics);     m_SectionLIst.SetItemText(i, 5, StrTmp);   } }``` 最后的动作是释放动作,因为很简单,这里就不给出代码了。将这些自定义函数通过界面上的“查看”按钮联系起来,整个PE查看器就算是写完了。 ###![image](https://yqfile.alicdn.com/b8d2348adaca4fbeab4769e5c7344664ff56b911.png) 6.4.2 简单的查壳工具 前面介绍了通过编程解析PE文件格式的基础数据,对于PE文件格式的解析其实并不难,难点在于兼容性。从前面的内容中可以看到,PE文件结构中大多用的是偏移地址,因此,只要偏移地址和实际的数据相符,那么PE文件格式有可能是嵌套的。也就是说,PE文件是可以变形的,只要保证其偏移地址和PE文件格式的结构基本就没多大问题。 对于PE可执行文件来说,为了保护可执行文件或者是压缩可执行文件,通常会对该文件进行加壳。接触过软件破解的人应该都清楚壳的概念。关于壳的概念,这里就不多说了。下面来写一个查壳的工具。 首先,用ASPack给前面写的程序加个壳。打开ASPack加壳工具,如图6-28所示。 图6-28 ASPack加壳工具界面 对测试用的软件进行一次加壳,不过在加壳前先用PEiD查看一下,如图6-29所示。 ![image](https://yqfile.alicdn.com/4efd9c49c812bd4db0951a870384a4cfc7abd8b9.png) 图6-29 PEiD查壳 从图6-29可以看出,该程序是Visual C++ 5.0 Debug版的程序。其实该程序是用Visual C++ 6.0写的,这里是PEiD识别有误。不过只要用Visual C++ 6.0进行编译选择Release版时,PEiD是可以正确进行识别的。使用ASPack对该程序进行加壳,然后用PEiD查壳,如图6-30所示。 从图6-30中可以看出,PEiD识别出文件被加过壳,且是用ASPack进行加壳的。PEiD如何识别程序被加壳,以及加了哪种壳呢?在PEiD的目录下有一个特征码文件,名为“userdb.txt”。打开这个文件,看大概内容就能知道里边保存了壳的特征码。程序员的任务就是自己实现一个这个壳的识别工具。 ![image](https://yqfile.alicdn.com/2f72f9a8b6fdbdbaaf5e226841c19a4928639c18.png) 图6-30 用PEiD查看加壳后的文件 壳的识别是通过特征码进行的,特征码的提取通常是选择文件的入口处。壳会修改程序的入口处,因此对于壳的特征码来说,选择入口处比较合适。这里的工具主要是用来学习和演示用的,因此写的查壳工具要能识别两种类型,第一种类型是可以识别用Visual C++ 6.0编译出来的文件,第二种类型是可以识别ASPack加壳后的程序。当然,ASPack加壳工具的版本众多,这里只要能识别上面所演示版本的ASPack就可以了。 如何提取特征码呢?程序无论是在磁盘上还是在内存中,都是以二进制的形式存在的。前面也提到,特征码是从程序的入口处进行提取的,那么可以使用C32Asm以十六进制的形式打开这些文件,在入口处提取特征码,也可以用OD将程序载入内存后提取特征码。这里选择使用OD提取特征码。用OD载入未加壳的程序,如图6-31所示。 ![image](https://yqfile.alicdn.com/21574a3911771ce192acece18e472772dcaacb8a.png) 图6-31 OD载入为加壳文件的入口处 可以看到,这就是未加壳程序的入口处代码。在图6-31中,“HEX数据”列中就是代码对应的十六进制编码,这里要做的就是提取这些十六进制编码。提取结果如下:

    "x55x8BxECx6AxFFx68x00x65x41x00" \"x68xE8x2Dx40x00x64xA1x00x00x00" \"x00x50x64x89x25x00x00x00x00x83" \"xC4x94"`根据这个步骤,把ASPack的特征码也提取出来,提取结果如下:

    "\x60\xE8\x03\x00\x00\x00\xE9\xEB\x04\x5D" \ "\x45\x55\xC3\xE8\x01\x00\x00\x00\xEB\x5D" \ "\xBB\xED\xFF\xFF\xFF\x03\xDD\x81\xEB\x00" "\xC0\x01"``` 有了这些特征码,就可以开始编程了。先来定义一个数据结构,用来保存特征码,该结构如下:

    define NAMELEN 20

    define SIGNLEN 32

    typedef struct _SIGN{  char szName[NAMELEN];  BYTE bSign[SIGNLEN + 1];}SIGN, *PSIGN;利用该数据结构定义2个保存特征码的全局变量,如下:

    SIGN Sign[2] = {  {    // VC6    "VC6",    "x55x8BxECx6AxFFx68x00x65x41x00" \    "x68xE8x2Dx40x00x64xA1x00x00x00" \    "x00x50x64x89x25x00x00x00x00x83" \    "xC4x94"  },  {    // ASPACK    "ASPACK",    "x60xE8x03x00x00x00xE9xEBx04x5D" \    "x45x55xC3xE8x01x00x00x00xEBx5D" \    "xBBxEDxFFxFFxFFx03xDDx81xEBx00"    "xC0x01"  }};`程序界面是在PE查看器的基础上完成的,如图6-32所示。

    图6-32 查壳程序结果

    提取特征码后,查壳工作只剩特征码匹配了。这非常简单,只要用文件的入口处代码和特征码进行匹配,匹配相同就会给出相应的信息。查壳的代码如下:

    VOID CPeParseDlg::GetPeInfo() {   PBYTE pSign = NULL;   // 定位文件入口位置   pSign = (PBYTE)((DWORD)m_lpBase       + m_pNtHdr->OptionalHeader.AddressOfEntryPoint);   // 比较入口特征码   if ( memcmp(Sign[0].bSign, pSign, SIGNLEN) == 0 )   {     SetDlgItemText(IDC_EDIT_PEINFO, Sign[0].szName);   }   else if ( memcmp(Sign[1].bSign, pSign, SIGNLEN) == 0 )   {     SetDlgItemText(IDC_EDIT_PEINFO, Sign[1].szName);   }   else   {     SetDlgItemText(IDC_EDIT_PEINFO, "未知");   } }``` 这样,查壳程序的功能就完成了。在程序中提取的特征码的长度为32字节,由于这里只是一个简单的例子,读者在提取特征码的时候,为了提高准确率,需要多进行一些测试。 6.4.3 地址转换器 前面介绍了关于PE文件的3种地址,分别是VA(虚拟地址)、RVA(相对虚拟地址)和FileOffset(文件偏移地址)。这3种地址的转换如果始终使用手动来计算会非常累,因此通常的做法是借助工具来完成。前面介绍了使用LordPE来计算这3种地址的转换,现在来编写一个对这3种地址进行转换的工具。该工具如图6-33所示。 ![image](https://yqfile.alicdn.com/7e8a46d66bd98d4b8a6d608b9eca622b5fadc47f.png) 图6-33 地址转换器 这个工具是在前两个工具的基础上完成的。因此,在进行计算的时候,应该先要进行“查看”,再进行“计算”。否则,该获取的指针还没有获取到。 在界面上,左边的3个按钮是“单选框”,单选框的设置方法如图6-34所示。 ![image](https://yqfile.alicdn.com/a4792aba5d2f41c4376336429320a540f4fedd97.png) 图6-34 对单选框的设置 3个单选框中只能有一个是选中状态,为了记录哪个单选框是选中状态,在类中定义一个成员变量m_nSelect。对3个单选框,分别使m_nSelect值为1、2和3。关于界面的编程,请读者参考源代码,这里就不进行过多的介绍了。下面来看主要的代码。 在单击“计算”按钮后,响应该按钮的代码如下: `` void CPeParseDlg::OnBtnCalc() {   // TODO: Add your control notification handler code here   DWORD dwAddr = 0;   // 获取的地址   dwAddr = GetAddr();   // 地址所在的节   int nInNum = GetAddrInSecNum(dwAddr);    // 计算其他地址   CalcAddr(nInNum, dwAddr); }``` 分别看一下GetAddr()、GetAddrInSecNum()和CalcAddr()的实现。 获取在编辑框中输入的地址内容的代码如下:

    DWORD CPeParseDlg::GetAddr(){  char szAddr[10] = { 0 };  DWORD dwAddr = 0;  switch ( m_nSelect )  {  case 1:    {      GetDlgItemText(IDC_EDIT_VA, szAddr, 10);      HexStrToInt(szAddr, &dwAddr);      break;    }  case 2:    {      GetDlgItemText(IDC_EDIT_RVA, szAddr, 10);      HexStrToInt(szAddr, &dwAddr);      break;    }  case 3:    {      GetDlgItemText(IDC_EDIT_FILEOFFSET, szAddr, 10);      HexStrToInt(szAddr, &dwAddr);      break;    }  }

      return dwAddr;}`获取该地址所属的第几个节的代码如下:

    int CPeParseDlg::GetAddrInSecNum(DWORD dwAddr) {   int nInNum = 0;   int nSecNum = m_pNtHdr->FileHeader.NumberOfSections;   switch ( m_nSelect )   {   case 1:     {       DWORD dwImageBase = m_pNtHdr->OptionalHeader.ImageBase;       for ( nInNum = 0; nInNum < nSecNum; nInNum ++ )       {         if ( dwAddr >= dwImageBase + m_pSecHdr[nInNum].VirtualAddress           && dwAddr <= dwImageBase + m_pSecHdr[nInNum].VirtualAddress             + m_pSecHdr[nInNum].Misc.VirtualSize)         {           return nInNum;         }       }       break;     }   case 2:     {       for ( nInNum = 0; nInNum < nSecNum; nInNum ++ )       {         if ( dwAddr >= m_pSecHdr[nInNum].VirtualAddress           && dwAddr <= m_pSecHdr[nInNum].VirtualAddress             + m_pSecHdr[nInNum].Misc.VirtualSize)         {           return nInNum;         }       }       break;     }   case 3:     {       for ( nInNum = 0; nInNum < nSecNum; nInNum ++ )       {         if ( dwAddr >= m_pSecHdr[nInNum].PointerToRawData           && dwAddr <= m_pSecHdr[nInNum].PointerToRawData             + m_pSecHdr[nInNum].SizeOfRawData)         {           return nInNum;         }       }       break;     }   }   return -1; }``` 计算其他地址的代码如下:

    VOID CPeParseDlg::CalcAddr(int nInNum, DWORD dwAddr){  DWORD dwVa = 0;  DWORD dwRva = 0;  DWORD dwFileOffset = 0;

      switch ( m_nSelect )  {  case 1:    {      dwVa = dwAddr;      dwRva = dwVa - m_pNtHdr->OptionalHeader.ImageBase;      dwFileOffset = m_pSecHdr[nInNum].PointerToRawData               + (dwRva - m_pSecHdr[nInNum].VirtualAddress);      break;    }  case 2:    {      dwVa = dwAddr + m_pNtHdr->OptionalHeader.ImageBase;      dwRva = dwAddr;      dwFileOffset = m_pSecHdr[nInNum].PointerToRawData               + (dwRva - m_pSecHdr[nInNum].VirtualAddress);      break;    }  case 3:    {      dwFileOffset = dwAddr;      dwRva = m_pSecHdr[nInNum].VirtualAddress           + (dwFileOffset - m_pSecHdr[nInNum].PointerToRawData);      dwVa = dwRva + m_pNtHdr->OptionalHeader.ImageBase;      break;    }  }

      SetDlgItemText(IDC_EDIT_SECTION, (const char *)m_pSecHdr[nInNum].Name);

      CString str;  str.Format("X", dwVa);  SetDlgItemText(IDC_EDIT_VA, str);

      str.Format("X", dwRva);  SetDlgItemText(IDC_EDIT_RVA, str);

      str.Format("X", dwFileOffset);  SetDlgItemText(IDC_EDIT_FILEOFFSET, str);}`代码都不复杂,关键就是CalcAddr()中3种地址的转换。如果读者没能理解代码,请参考前面手动转换3种地址的方法,这里就不进行介绍了。

    6.4.4 添加节区

    添加节区在很多场合都会用到,比如在加壳中、在免杀中都会经常用到对PE文件添加一个节区。添加一个节区的方法有4步,第1步是在节表的最后面添加一个IMAGE_SECTI ON_HEADER,第2步是更新IMAGE_FILE_HEADER中的NumberOfSections字段,第3步是更新IMAGE_OPTIONAL_HEADER中的SizeOfImage字段,最后一步则是添加文件的数据。当然,前3步是没有先后顺序的,但是最后一步一定要明确如何改变。

    注:某些情况下,在添加新的节区项以后会向新节区项的数据部分添加一些代码,而这些代码可能要求在程序执行之前就被执行,那么这时还需要更新IMAGE_OPTIONAL _HEADER中的AddressOfEntryPoint字段。

    1.手动添加一个节区先来进行一次手动添加节区的操作,这个过程是个熟悉上述步骤的过程。网上有很多现成的添加节区的工具。这里自己编写工具的目的是掌握和了解其实现方法,锻炼编程能力;手动添加节区是为了巩固前面的知识,熟悉添加节区的步骤。

    接下来还是使用前面的测试程序。使用C32Asm用十六进制编辑方式打开这个程序,并定位到其节表处,如图6-35所示。

    图6-35 节表位置信息

    从图6-35中可以看到,该PE文件有3个节表。直接看十六进制信息可能很不方便(看多了就习惯了),为了直观方便地查看节表中IMAGE_SECTION_HEADER的信息,那么使用LordPE进行查看,如图6-36所示。

    图6-36 使用LordPE查看该节表信息

    用LordPE工具查看的确直观多了。对照LordPE显示的节表信息来添加一个节区。回顾一下IMAGE_SECTION_HEADER结构体的定义,如下:

    typedef struct _IMAGE_SECTION_HEADER {   BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];   union {       DWORD  PhysicalAddress;       DWORD  VirtualSize;   } Misc;   DWORD  VirtualAddress;   DWORD  SizeOfRawData;   DWORD  PointerToRawData;   DWORD  PointerToRelocations;   DWORD  PointerToLinenumbers;   WORD  NumberOfRelocations;   WORD  NumberOfLinenumbers;   DWORD  Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;``` IMAGE_SECTION_HEADER结构体的成员很多,但是真正要使用的只有6个,分别是Name、VirtualSize、VritualAddress、SizeOfRawData、PointerToRawData和Characteristics。这6项刚好与LordPE显示的6项相同。其实IMAGE_SECTION_HEADER结构体中其余的成员几乎不被使用。下面介绍如何添加这些内容。 IMAGE_SECTION_HEADER的长度为40字节,是十六进制的0x28,在C32Asm中占用2行半的内容,这里一次把这两行半的内容手动添加进去。回到C32Asm中,在最后一个节表的位置处开始添加内容,首先把光标放到右边的ASCII字符中,输入“.test”,如图6-37所示。 ![image](https://yqfile.alicdn.com/22dd5f49efa18ae40655a7c2f0569b7aa3e67fd3.png) 图6-37 添加“.test”节名 接下来在00000240位置处添加节的大小,该大小直接是对齐后的大小即可。由于文件对齐是0x1000字节,也就是4096字节,那么采用最小值即可,使该值为0x1000。不知道读者是否还记得前面提到的字节顺序的问题,在C32Asm中添加时,正确的添加应当是“00 10 00 00”,以后添加时也要注意字节顺序。在添加后面几个成员时,不再提示注意字节顺序,读者应时刻清楚这点。在添加该值时,应当将光标定位在十六进制编辑处,而不是刚才所在的ASCII字符处。顺便要把VirutalAddress也添加上,VirtualAddress的值是前一个节区的起始位置加上上一个节对齐后的长度的值,上一个节区的起始位置为0x6000,上一个节区对齐后的长度为0x3000,因此新节区的起始位置为0x9000。添加VirtualSize和VirtualAddress后如图6-38所示。 ![image](https://yqfile.alicdn.com/7a6bf9f3905dfff194b089bb4e4672797456f9f1.png) 图6-38 添加VirtualSize和VirtualAddress的值 接下来的两个字段分别是SizeOfRawData和PointerToRawData,其添加方法类似前面两个字段的添加方法,这里就不细说了。分别添加“0x9000”和“0x1000”两个值,如图6-39所示。 ![image](https://yqfile.alicdn.com/1ee8aa728d50af8f30e48c0c32b50ed98c25dbca.png) 图6-39 添加SizeOfRawData和PointerToRawData PointerToRawData后面的12字节都可以为0,只要修改最后4字节的内容,也就是Characteristics的值即可。这个值直接使用上一个节区的值即可,实际添加时应根据所要节的属性给值。这里为了省事而直接使用上一个节区的属性,如图6-40所示。 ![image](https://yqfile.alicdn.com/d062637b480538b28ced02f4611ab8c887f4e049.png) 图6-40 添加Characteristics属性 整个节表需要添加的地方就添加完成了,接下来需要修改该PE文件的节区数量。当前节区数量是3,这里要修改为4。虽然可以通过LordPE等修改工具完成,但是这里仍然使用手动修改。对于修改的位置,请读者自行定位找到,修改如图6-41所示。 ![image](https://yqfile.alicdn.com/8cb67397e7a6644c9b342f43805c9f35a2a4ab05.png) 图6-41 修改节区个数为4 除了节区数量以外,还要修改文件映像的大小,也就是前面提到的SizeOfImage的值。由于新添加了节区,那么应该把该节区的大小加上SizeOfImage的大小,即为新的SizeOfImage的大小。现在的SizeOfImage的大小为0x9000,加上新添加节区的大小为0xa000。SizeOfImage的位置请读者自行查找,修改如图6-42所示。 ![image](https://yqfile.alicdn.com/b1fc23f4a23f81c21ebbd13c995441cd0aef3579.png) ![image](https://yqfile.alicdn.com/4f9220b0df2acd6b67bf88326567be7b3b6ae2d3.png) 图6-42 修改SizeOfImage的值为0xa000 修改PE结构字段的内容都已经做完了,最后一步就是添加真实的数据。由于这个节区不使用,因此填充0值就可以了,文件的起始位置为0x9000,长度为0x1000。把光标移到文件的末尾,单击“编辑”->“插入数据”命令,在“插入数据大小”文本框中输入十进制的4096,也就是十六进制的0x1000,如图6-43所示。 图6-43 “插入数据”对话框的设置 单击“确定”按钮,可以看到在刚才的光标处插入了很多0值,这样工作也完成了。单击“保存”按钮进行保存,提示是否备份,选择“是”。然后用LordPE查看添加节区的情况,如图6-44所示。 ![image](https://yqfile.alicdn.com/d8a53c97b6b20f60cb1b543a8aacff9b4aad09d4.png) 从图6-45中可以看出,添加节区后的文件比原来的文件大了4KB,这是由于添加了4096字节的0值。也许读者最关心的不是大小问题,而是软件添加了大小后是否真的可以运行。其实试运行一下,是可以运行的。 上面的整个过程就是手动添加一个新节区的全部过程,除了特有的几个步骤以外,要注意新节区的内存起始位置和文件起始位置的值。相信通过上面手动添加节区,读者对此已经非常熟悉了。下面就开始通过编程来完成添加节区的任务。 补充:在C32Asm软件中可以快速定位PE结构的各个结构体和字段的位置,在菜单栏单击“查看(V)”->“PE信息(P)”即可在C32Asm工作区的左侧打开一个PE结构字段的解析面板,在面板上双击PE结构的每个字段则可在C32Asm工作区中定位到十六进制形式的PE结构字段的数据。 2.通过编程添加节区 通过编程添加一个新的节区无非就是文件相关的操作,只是多了一个对PE文件的解析和操作而已。添加节区的步骤和手动添加节区的步骤是一样的,只要一步一步按照上面的步骤写代码就可以了。在开始写代码前,首先修改FileCreate()函数中的部分代码,如下:

      m_hMap = CreateFileMapping(m_hFile, NULL,                 PAGE_READWRITE /| SEC_IMAGE/,                 0, 0, 0);  if ( m_hMap == NULL )  {    CloseHandle(m_hFile);    return bRet;  }`这里要把SEC_IMAGE宏注释掉。因为要修改内存文件映射,有这个值会使添加节区失败,因此要将其注释掉或者直接删除掉。

    图6-46 添加节区界面

    程序的界面如图6-46所示。

    首先编写“添加”按钮响应事件,代码如下:

    void CPeParseDlg::OnBtnAddSection() {    // TODO: Add your control notification handler code here   // 节名   char szSecName[8] = { 0 };   // 节大小   int nSecSize = 0;   GetDlgItemText(IDC_EDIT_SECNAME, szSecName, 8);   nSecSize = GetDlgItemInt(IDC_EDIT_SEC_SIZE, FALSE, TRUE);   AddSec(szSecName, nSecSize); }``` 按钮事件中最关键的地方是AddSec()函数。该函数有2个参数,分别是添加节的名称与添加节的大小。这个大小无论输入多大,最后都会按照对齐方式进行向上对齐。看一下AddSec()函数的代码,如下:

    VOID CPeParseDlg::AddSec(char *szSecName, int nSecSize){  int nSecNum = m_pNtHdr->FileHeader.NumberOfSections;  DWORD dwFileAlignment = m_pNtHdr->OptionalHeader.FileAlignment;  DWORD dwSecAlignment = m_pNtHdr->OptionalHeader.SectionAlignment;

      PIMAGE_SECTION_HEADER pTmpSec = m_pSecHdr + nSecNum;

      // 拷贝节名  strncpy((char *)pTmpSec->Name, szSecName, 7);  // 节的内存大小  pTmpSec->Misc.VirtualSize = AlignSize(nSecSize, dwSecAlignment);  // 节的内存起始位置  pTmpSec->VirtualAddress=m_pSecHdr[nSecNum-1].VirtualAddress+AlignSize(m_pSecHdr [nSecNum - 1].Misc.VirtualSize, dwSecAlignment);  // 节的文件大小  pTmpSec->SizeOfRawData = AlignSize(nSecSize, dwFileAlignment);  // 节的文件起始位置  pTmpSec->PointerToRawData=m_pSecHdr[nSecNum-1].PointerToRawData+AlignSize(m_pSe cHdr[nSecNum - 1].SizeOfRawData, dwSecAlignment);

      // 修正节数量  m_pNtHdr->FileHeader.NumberOfSections ++;  // 修正映像大小  m_pNtHdr->OptionalHeader.SizeOfImage += pTmpSec->Misc.VirtualSize;

      FlushViewOfFile(m_lpBase, 0);

      // 添加节数据  AddSecData(pTmpSec->SizeOfRawData);

      EnumSections();}`代码中每一步都按照相应的步骤来完成,其中用到的2个函数分别是AlignSize()和AddSecData()。前者是用来进行对齐的,后者是用来在文件中添加实际的数据内容的。这两个函数非常简单,代码如下:

    DWORD CPeParseDlg::AlignSize(int nSecSize, DWORD Alignment) {   int nSize = nSecSize;   if ( nSize % Alignment != 0 )   {     nSecSize = (nSize / Alignment + 1) * Alignment;   }   return nSecSize; } VOID CPeParseDlg::AddSecData(int nSecSize) {   PBYTE pByte = NULL;   pByte = (PBYTE)malloc(nSecSize);   ZeroMemory(pByte, nSecSize);   DWORD dwNum = 0;   SetFilePointer(m_hFile, 0, 0, FILE_END);   WriteFile(m_hFile, pByte, nSecSize, &dwNum, NULL);   FlushFileBuffers(m_hFile);   free(pByte); }``` 整个添加节区的代码就完成了,仍然使用最开始的那个简单程序进行测试,看是否可以添加一个节区,如图6-47所示。 ![image](https://yqfile.alicdn.com/881bf2f482c52998d2f6974ca687bc2f36e0b95d.png) 图6-47 添加节区 从图6-47中可以看出,添加节区是成功的。试着运行一下添加节区后的文件,可以正常运行,而且添加节区的文件比原文件大了4KB,和前面手动添加的效果是一样的。 至此,对PE文件结构的介绍就结束了。其实,PE文件结构还有很多比较重要的内容,但是这里只介绍了一些基础的知识。至于其他的内容,请读者自行学习。PE结构查看器最关 相关资源:Visual C 黑客编程 揭秘与防范(高清PDF 源代码)
    最新回复(0)