天天看点

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

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

c++ 黑客编程揭秘与防范(第2版)

前面讲的都是概念性的知识,本节主要编写一些关于pe文件结构的程序代码,以帮助读者加强对pe结构的了解。

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

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

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

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

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

);<code>`</code>

参数说明如下。

hfile:该参数是createfile()函数返回的句柄。

lpattributes:是安全属性,该值通常是null。

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

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

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

bool unmapviewoffile(

 lpcvoid lpbaseaddress  // starting address

该函数的参数就是mapviewoffile()函数的返回值。

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

void cpeparsedlg::parsebasepe()

{

  cstring strtmp;

  // 入口地址

  strtmp.format("%08x", m_pnthdr-&gt;optionalheader.addressofentrypoint);

  setdlgitemtext(idc_edit_ep, strtmp);

  // 映像基地址

  strtmp.format("%08x", m_pnthdr-&gt;optionalheader.imagebase);

  setdlgitemtext(idc_edit_imagebase, strtmp);

  // 连接器版本号

  strtmp.format("%d.%d",

    m_pnthdr-&gt;optionalheader.majorlinkerversion,

    m_pnthdr-&gt;optionalheader.minorlinkerversion);

  setdlgitemtext(idc_edit_linkversion, strtmp);

  // 节表数量

  strtmp.format("%02x", m_pnthdr-&gt;fileheader.numberofsections);

  setdlgitemtext(idc_edit_sectionnum, strtmp);

  // 文件对齐值大小

  strtmp.format("%08x", m_pnthdr-&gt;optionalheader.filealignment);

  setdlgitemtext(idc_edit_filealign, strtmp);

  // 内存对齐值大小

  strtmp.format("%08x", m_pnthdr-&gt;optionalheader.sectionalignment);

  setdlgitemtext(idc_edit_secalign, strtmp);

}<code>`</code>

pe格式的基础信息,就是简单地获取结构体的成员变量,没有过多复杂的内容。获取导入表、导出表比获取基础信息复杂。关于导入表、导出表的内容将在后面介绍。接下来进行节表的枚举,具体代码如下:

"x55x8bxecx6axffx68x00x65x41x00" \

"x68xe8x2dx40x00x64xa1x00x00x00" \

"x00x50x64x89x25x00x00x00x00x83" \

"xc4x94"<code>`</code>

根据这个步骤,把aspack的特征码也提取出来,提取结果如下:

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"

  }};<code>`</code>

程序界面是在pe查看器的基础上完成的,如图6-32所示。

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

图6-32 查壳程序结果

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

dword cpeparsedlg::getaddr()

  char szaddr[10] = { 0 };

  dword dwaddr = 0;

  switch ( m_nselect )

  case 1:

    {

      getdlgitemtext(idc_edit_va, szaddr, 10);

      hexstrtoint(szaddr, &amp;dwaddr);

      break;

    }

  case 2:

      getdlgitemtext(idc_edit_rva, szaddr, 10);

  case 3:

      getdlgitemtext(idc_edit_fileoffset, szaddr, 10);

  }

  return dwaddr;

获取该地址所属的第几个节的代码如下:

void cpeparsedlg::calcaddr(int ninnum, dword dwaddr)

  dword dwva = 0;

  dword dwrva = 0;

  dword dwfileoffset = 0;

      dwva = dwaddr;

      dwrva = dwva - m_pnthdr-&gt;optionalheader.imagebase;

      dwfileoffset = m_psechdr[ninnum].pointertorawdata

              + (dwrva - m_psechdr[ninnum].virtualaddress);

      dwva = dwaddr + m_pnthdr-&gt;optionalheader.imagebase;

      dwrva = dwaddr;

      dwfileoffset = dwaddr;

      dwrva = m_psechdr[ninnum].virtualaddress

          + (dwfileoffset - m_psechdr[ninnum].pointertorawdata);

      dwva = dwrva + m_pnthdr-&gt;optionalheader.imagebase;

  setdlgitemtext(idc_edit_section, (const char *)m_psechdr[ninnum].name);

  cstring str;

  str.format("%08x", dwva);

  setdlgitemtext(idc_edit_va, str);

  str.format("%08x", dwrva);

  setdlgitemtext(idc_edit_rva, str);

  str.format("%08x", dwfileoffset);

  setdlgitemtext(idc_edit_fileoffset, str);

代码都不复杂,关键就是calcaddr()中3种地址的转换。如果读者没能理解代码,请参考前面手动转换3种地址的方法,这里就不进行介绍了。

添加节区在很多场合都会用到,比如在加壳中、在免杀中都会经常用到对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所示。

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

图6-35 节表位置信息

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

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

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

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

  m_hmap = createfilemapping(m_hfile, null,

                page_readwrite /| sec_image/,

                0, 0, 0);

  if ( m_hmap == null )

    closehandle(m_hfile);

    return bret;

  }<code>`</code>

这里要把sec_image宏注释掉。因为要修改内存文件映射,有这个值会使添加节区失败,因此要将其注释掉或者直接删除掉。

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

图6-46 添加节区界面

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

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

void cpeparsedlg::addsec(char *szsecname, int nsecsize)

  int nsecnum = m_pnthdr-&gt;fileheader.numberofsections;

  dword dwfilealignment = m_pnthdr-&gt;optionalheader.filealignment;

  dword dwsecalignment = m_pnthdr-&gt;optionalheader.sectionalignment;

  pimage_section_header ptmpsec = m_psechdr + nsecnum;

  // 拷贝节名

  strncpy((char *)ptmpsec-&gt;name, szsecname, 7);

  // 节的内存大小

  ptmpsec-&gt;misc.virtualsize = alignsize(nsecsize, dwsecalignment);

  // 节的内存起始位置

  ptmpsec-&gt;virtualaddress=m_psechdr[nsecnum-1].virtualaddress+alignsize(m_psechdr

[nsecnum - 1].misc.virtualsize, dwsecalignment);

  // 节的文件大小

  ptmpsec-&gt;sizeofrawdata = alignsize(nsecsize, dwfilealignment);

  // 节的文件起始位置

  ptmpsec-&gt;pointertorawdata=m_psechdr[nsecnum-1].pointertorawdata+alignsize(m_pse

chdr[nsecnum - 1].sizeofrawdata, dwsecalignment);

  // 修正节数量

  m_pnthdr-&gt;fileheader.numberofsections ++;

  // 修正映像大小

  m_pnthdr-&gt;optionalheader.sizeofimage += ptmpsec-&gt;misc.virtualsize;

  flushviewoffile(m_lpbase, 0);

  // 添加节数据

  addsecdata(ptmpsec-&gt;sizeofrawdata);

  enumsections();

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

继续阅读