本节书摘来自异步社区《C++ 黑客编程揭秘与防范(第2版)》一书中的第6章6.4节PE相关编程实例,作者冀云,更多章节内容可以访问云栖社区“异步社区”公众号查看。
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("%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);
}
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("%08X", m_pSecHdr[i].VirtualAddress);
m_SectionLIst.SetItemText(i, 1, StrTmp);
StrTmp.Format("%08X", m_pSecHdr[i].Misc.VirtualSize);
m_SectionLIst.SetItemText(i, 2, StrTmp);
StrTmp.Format("%08X", m_pSecHdr[i].PointerToRawData);
m_SectionLIst.SetItemText(i, 3, StrTmp);
StrTmp.Format("%08X", m_pSecHdr[i].SizeOfRawData);
m_SectionLIst.SetItemText(i, 4, StrTmp);
StrTmp.Format("%08X", m_pSecHdr[i].Characteristics);
m_SectionLIst.SetItemText(i, 5, StrTmp);
}
}
最后的动作是释放动作,因为很简单,这里就不给出代码了。将这些自定义函数通过界面上的“查看”按钮联系起来,整个PE查看器就算是写完了。
6.4.2 简单的查壳工具
前面介绍了通过编程解析PE文件格式的基础数据,对于PE文件格式的解析其实并不难,难点在于兼容性。从前面的内容中可以看到,PE文件结构中大多用的是偏移地址,因此,只要偏移地址和实际的数据相符,那么PE文件格式有可能是嵌套的。也就是说,PE文件是可以变形的,只要保证其偏移地址和PE文件格式的结构基本就没多大问题。
对于PE可执行文件来说,为了保护可执行文件或者是压缩可执行文件,通常会对该文件进行加壳。接触过软件破解的人应该都清楚壳的概念。关于壳的概念,这里就不多说了。下面来写一个查壳的工具。
首先,用ASPack给前面写的程序加个壳。打开ASPack加壳工具,如图6-28所示。
对测试用的软件进行一次加壳,不过在加壳前先用PEiD查看一下,如图6-29所示。
从图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”。打开这个文件,看大概内容就能知道里边保存了壳的特征码。程序员的任务就是自己实现一个这个壳的识别工具。
壳的识别是通过特征码进行的,特征码的提取通常是选择文件的入口处。壳会修改程序的入口处,因此对于壳的特征码来说,选择入口处比较合适。这里的工具主要是用来学习和演示用的,因此写的查壳工具要能识别两种类型,第一种类型是可以识别用Visual C++ 6.0编译出来的文件,第二种类型是可以识别ASPack加壳后的程序。当然,ASPack加壳工具的版本众多,这里只要能识别上面所演示版本的ASPack就可以了。
如何提取特征码呢?程序无论是在磁盘上还是在内存中,都是以二进制的形式存在的。前面也提到,特征码是从程序的入口处进行提取的,那么可以使用C32Asm以十六进制的形式打开这些文件,在入口处提取特征码,也可以用OD将程序载入内存后提取特征码。这里选择使用OD提取特征码。用OD载入未加壳的程序,如图6-31所示。
可以看到,这就是未加壳程序的入口处代码。在图6-31中,“HEX数据”列中就是代码对应的十六进制编码,这里要做的就是提取这些十六进制编码。提取结果如下:
"\x55\x8B\xEC\x6A\xFF\x68\x00\x65\x41\x00" \
"\x68\xE8\x2D\x40\x00\x64\xA1\x00\x00\x00" \
"\x00\x50\x64\x89\x25\x00\x00\x00\x00\x83" \
"\xC4\x94"
根据这个步骤,把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",
"\x55\x8B\xEC\x6A\xFF\x68\x00\x65\x41\x00" \
"\x68\xE8\x2D\x40\x00\x64\xA1\x00\x00\x00" \
"\x00\x50\x64\x89\x25\x00\x00\x00\x00\x83" \
"\xC4\x94"
},
{
// ASPACK
"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"
}};
程序界面是在PE查看器的基础上完成的,如图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所示。
这个工具是在前两个工具的基础上完成的。因此,在进行计算的时候,应该先要进行“查看”,再进行“计算”。否则,该获取的指针还没有获取到。
在界面上,左边的3个按钮是“单选框”,单选框的设置方法如图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("%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种地址的方法,这里就不进行介绍了。
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中可以看到,该PE文件有3个节表。直接看十六进制信息可能很不方便(看多了就习惯了),为了直观方便地查看节表中IMAGE_SECTION_HEADER的信息,那么使用LordPE进行查看,如图6-36所示。
用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所示。
接下来在00000240位置处添加节的大小,该大小直接是对齐后的大小即可。由于文件对齐是0x1000字节,也就是4096字节,那么采用最小值即可,使该值为0x1000。不知道读者是否还记得前面提到的字节顺序的问题,在C32Asm中添加时,正确的添加应当是“00 10 00 00”,以后添加时也要注意字节顺序。在添加后面几个成员时,不再提示注意字节顺序,读者应时刻清楚这点。在添加该值时,应当将光标定位在十六进制编辑处,而不是刚才所在的ASCII字符处。顺便要把VirutalAddress也添加上,VirtualAddress的值是前一个节区的起始位置加上上一个节对齐后的长度的值,上一个节区的起始位置为0x6000,上一个节区对齐后的长度为0x3000,因此新节区的起始位置为0x9000。添加VirtualSize和VirtualAddress后如图6-38所示。
接下来的两个字段分别是SizeOfRawData和PointerToRawData,其添加方法类似前面两个字段的添加方法,这里就不细说了。分别添加“0x9000”和“0x1000”两个值,如图6-39所示。
PointerToRawData后面的12字节都可以为0,只要修改最后4字节的内容,也就是Characteristics的值即可。这个值直接使用上一个节区的值即可,实际添加时应根据所要节的属性给值。这里为了省事而直接使用上一个节区的属性,如图6-40所示。
整个节表需要添加的地方就添加完成了,接下来需要修改该PE文件的节区数量。当前节区数量是3,这里要修改为4。虽然可以通过LordPE等修改工具完成,但是这里仍然使用手动修改。对于修改的位置,请读者自行定位找到,修改如图6-41所示。
除了节区数量以外,还要修改文件映像的大小,也就是前面提到的SizeOfImage的值。由于新添加了节区,那么应该把该节区的大小加上SizeOfImage的大小,即为新的SizeOfImage的大小。现在的SizeOfImage的大小为0x9000,加上新添加节区的大小为0xa000。SizeOfImage的位置请读者自行查找,修改如图6-42所示。
修改PE结构字段的内容都已经做完了,最后一步就是添加真实的数据。由于这个节区不使用,因此填充0值就可以了,文件的起始位置为0x9000,长度为0x1000。把光标移到文件的末尾,单击“编辑”->“插入数据”命令,在“插入数据大小”文本框中输入十进制的4096,也就是十六进制的0x1000,如图6-43所示。
单击“确定”按钮,可以看到在刚才的光标处插入了很多0值,这样工作也完成了。单击“保存”按钮进行保存,提示是否备份,选择“是”。然后用LordPE查看添加节区的情况,如图6-44所示。
对比前后两个文件的大小,如图6-45所示。
从图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所示。
首先编写“添加”按钮响应事件,代码如下:
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所示。
从图6-47中可以看出,添加节区是成功的。试着运行一下添加节区后的文件,可以正常运行,而且添加节区的文件比原文件大了4KB,和前面手动添加的效果是一样的。
至此,对PE文件结构的介绍就结束了。其实,PE文件结构还有很多比较重要的内容,但是这里只介绍了一些基础的知识。至于其他的内容,请读者自行学习。PE结构查看器最关
键的是兼容性,PE结构是可以进行各种变形的。常规的PE结构也许比较好解析,但是经过变形的PE结构解析起来就可能会出错,因此要不断地尝试去解析不同的PE文件结构,PE查看器兼容性才会不断地完善。前面介绍了通过C32Asm手动分析PE文件结构,这种方法有助于完善PE查看器。这就好比数据恢复一样,数据恢复高手绝对不是简单地通过数据恢复工具来进行。虽然高手也在使用工具,但是如果遇到较为复杂的情况,数据恢复工具可能就会显得无力,那么手动分析文件系统格式将是唯一的方法。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。