本节书摘来自异步社区出版社《c++ 黑客编程揭秘与防范(第2版)》一书中的第6章,第6.4节,作者:冀云,更多章节内容可以访问云栖社区“异步社区”公众号查看。
c++ 黑客编程揭秘与防范(第2版)
前面讲的都是概念性的知识,本节主要编写一些关于pe文件结构的程序代码,以帮助读者加强对pe结构的了解。
写pe查看器并不是件复杂的事情,只要按照pe结构一步一步地解析就可以了。下面简单地解析其中几个字段内容,显示一下节表的信息,其余的内容只要稍作修改即可。pe查看器的界面如图6-26所示。
pe查看器的界面按照图6-26所示的设置,不过这个可以按照个人的偏好进行布局设置。编写该pe查看器的步骤为打开文件并创建文件内存映像,判断文件是否为pe文件并获得pe格式相关结构体的指针,解析基本的pe字段,枚举节表,最后关闭文件。需要在类中添加几个成员变量及成员函数,添加的内容如图6-27所示。

按照前面所说的顺序,依次实现添加的各个成员函数。
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->optionalheader.addressofentrypoint);
setdlgitemtext(idc_edit_ep, strtmp);
// 映像基地址
strtmp.format("%08x", 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("%02x", m_pnthdr->fileheader.numberofsections);
setdlgitemtext(idc_edit_sectionnum, strtmp);
// 文件对齐值大小
strtmp.format("%08x", m_pnthdr->optionalheader.filealignment);
setdlgitemtext(idc_edit_filealign, strtmp);
// 内存对齐值大小
strtmp.format("%08x", m_pnthdr->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所示。
图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, &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->optionalheader.imagebase;
dwfileoffset = m_psechdr[ninnum].pointertorawdata
+ (dwrva - m_psechdr[ninnum].virtualaddress);
dwva = dwaddr + m_pnthdr->optionalheader.imagebase;
dwrva = dwaddr;
dwfileoffset = dwaddr;
dwrva = m_psechdr[ninnum].virtualaddress
+ (dwfileoffset - m_psechdr[ninnum].pointertorawdata);
dwva = dwrva + m_pnthdr->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所示。
图6-35 节表位置信息
从图6-35中可以看到,该pe文件有3个节表。直接看十六进制信息可能很不方便(看多了就习惯了),为了直观方便地查看节表中image_section_header的信息,那么使用lordpe进行查看,如图6-36所示。
图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宏注释掉。因为要修改内存文件映射,有这个值会使添加节区失败,因此要将其注释掉或者直接删除掉。
图6-46 添加节区界面
程序的界面如图6-46所示。
首先编写“添加”按钮响应事件,代码如下:
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()。前者是用来进行对齐的,后者是用来在文件中添加实际的数据内容的。这两个函数非常简单,代码如下: