2.6.2 把MessageBoxA()代码写入PE文件的完整实例
根据以上的对MessageBoxA()的分析,可以直接把以上代码注入到一个PE可执行 文件中。为了使程序有通用性,这里编写了一个产生显示任意长度字符的对话框的函数WriteMessageBox()。
下面是用于注入MessageBoxA()代码的头文件,取名为Pe.h,其中用 #include包含了相关的文件头,定义了peHeader结构,且定义了CPe类,其源代码如下:
l
// Pe.h: 定义CPe类
//
#ifndef _PE_H__INCLUDED
#define _PE_H__INCLUDED
#include <io.h>
#include <fcntl.h>
#include <sys\stat.h>
typedef struct PE_HEADER_MAP
{
DWORD signature;
IMAGE_FILE_HEADER _head;
IMAGE_OPTIONAL_HEADER opt_head;
IMAGE_SECTION_HEADER section_header[6];
} peHeader;
class CPe
{
public:
CPe();
virtual ~CPe();
public:
void CalcAddress(const void *base);
void ModifyPe(CString strFileName,CString strMsg);
void WriteFile(CString strFileName,CString strMsg);
BOOL WriteNewEntry(int ret,long offset,DWORD dwAddress);
BOOL WriteMessageBox(int ret,long offset,CString strCap,CString
strTxt);
CString StrOfDWord(DWORD dwAddress);
public:
DWORD dwSpace;
DWORD dwEntryAddress;
DWORD dwEntryWrite;
DWORD dwProgRAV;
DWORD dwOldEntryAddress;
DWORD dwNewEntryAddress;
DWORD dwCodeOffset;
DWORD dwPeAddress;
DWORD dwFlagAddress;
DWORD dwVirtSize;
DWORD dwPhysAddress;
DWORD dwPhysSize;
DWORD dwMessageBoxAadaddress;
};
#endif
l
其中peHeader结构是前面所讲的PE Header结构与节表(Section Table)头结构(6个表头成员)的总结构。因为它们在PE文件中是紧凑排列的,所以可以这样写。其实只用一个节表头就可以。
下面分别介绍CPe类成员函数的定义,它们包含在Pe.cpp文件中。在这个文件开始用#include包含了stdafx.h和Pe.h文件。用MFC VC++编译器编译时,必须包括stdafx.h文件,即使这个文件是空的,也需要包括它,这是编译器设置所致,除非修改MFC的编译器的默认设置。CPe类的构造和析构函数这里没有用上,对系统内存的访问和其他操作主要是通过主成员函数ModifyPe()来进行。它们的源代码如下:
l
// Pe.cpp: 实现 CPe类
//
#include "stdafx.h"
#include "Pe.h"
CPe::CPe()
{
}
CPe::~CPe()
{
}
void CPe::ModifyPe(CString strFileName,CString strMsg)
{
CString strErrMsg;
HANDLE hFile, hMapping;
void *basepointer;
// 打开要修改的文件
if ((hFile = CreateFile(strFileName, GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE, 0,
OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0)) == INVALID_HANDLE_ VALUE)
{
AfxMessageBox("Could not open file.");
return;
}
// 创建一个映射文件
if (!(hMapping = CreateFileMapping(hFile, 0, PAGE_READONLY | SEC_ COMMIT, 0, 0, 0)))
{
AfxMessageBox("Mapping failed.");
CloseHandle(hFile);
return;
}
// 把文件头映象存入baseointer
if (!(basepointer = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0)))
{
AfxMessageBox("View failed.");
CloseHandle(hMapping);
CloseHandle(hFile);
return;
}
CloseHandle(hMapping);
CloseHandle(hFile);
CalcAddress(basepointer); // 得到相关地址
UnmapViewOfFile(basepointer);
if(dwSpace<50)
{
AfxMessageBox("No room to write the data!");
}
else
{
WriteFile(strFileName,strMsg); // 写文件
}
if ((hFile = CreateFile(strFileName, GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE, 0,
OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0)) == INVALID_HANDLE_ VALUE)
{
AfxMessageBox("Could not open file.");
return;
}
CloseHandle(hFile);
}
其中对一个PE文件进行MessageBoxA()代码的注入是通过ModifyPe()函数进行,它的入口参数是要被修改的PE可执行文件名。在这个函数中,首先创建所修改文件的句柄,然后创建映射文件,再通过映射文件的句柄获得这个PE文件的文件头指针,最后把这个指针传给函数CalcAddress()。通过CalcAddress()函数来计算PE Header的开始偏移、保存旧的程序入口地址、计算新的程序入口地址和计算PE文件的空隙空间等。
CalcAddress()函数的源代码如下:
l
void CPe::CalcAddress(const void *base)
{
IMAGE_DOS_HEADER * dos_head =(IMAGE_DOS_HEADER *)base;
if (dos_head->e_magic != IMAGE_DOS_SIGNATURE)
{
AfxMessageBox("Unknown type of file.");
return;
}
peHeader * header;
// 得到PE文件头
header = (peHeader *)((char *)dos_head + dos_head->e_lfanew);
if(IsBadReadPtr(header, sizeof(*header)))
{
AfxMessageBox("No PE header, probably DOS executable.");
return;
}
DWORD mods;
char tmpstr[4]={0};
if(strstr((const char *)header->section_header[0].Name,".text")!=
NULL)
{
// 此段的真实长度
dwVirtSize=header->section_header[0].Misc.VirtualSize;
// 此段的物理偏移
dwPhysAddress=header->section_header[0].PointerToRawData;
// 此段的物理长度
dwPhysSize=header->section_header[0].SizeOfRawData;
// 得到PE文件头的开始偏移
dwPeAddress=dos_head->e_lfanew;
// 得到代码段的可用空间,用以判断可不可以写入我们的代码
// 用此段的物理长度减去此段的真实长度就可以得到
dwSpace=dwPhysSize-dwVirtSize;
// 得到程序的装载地址,一般为0x400000
dwProgRAV=header->opt_head.ImageBase;
// 得到代码偏移,用代码段起始RVA减去此段的物理偏移
// 应为程序的入口计算公式是一个相对的偏移地址,计算公式为:
// 代码的写入地址+dwCodeOffset
dwCodeOffset=header->opt_head.BaseOfCode-dwPhysAddress;
// 代码写入的物理偏移
dwEntryWrite=header->section_header[0].PointerToRawData+header->
section_header[0].Misc.VirtualSize;
//对齐边界
mods=dwEntryWrite%16;
if(mods!=0)
{
dwEntryWrite+=(16-mods);
}
// 保存旧的程序入口地址
dwOldEntryAddress=header->opt_head.AddressOfEntryPoint;
// 计算新的程序入口地址
dwNewEntryAddress=dwEntryWrite+dwCodeOffset;
return;
}
}
l
下面的StrOfDWord()函数是把一个DWORD值转换成一个字符串,因为一个DWORD值占有4个字节,因此把一个DWORD值变成一个字符串,若保持数值不变,就变成了一个4个字节的字符串。同时把这个值的位置顺序颠倒,这是为了把一个实际的值变成按Little-endian的方式写入PE文件中,其转换方法如下:
l
CString CPe::StrOfDWord(DWORD dwAddress)
{
unsigned char waddress[4]={0};
waddress[3]=(char)(dwAddress>>24)&0xFF;
waddress[2]=(char)(dwAddress>>16)&0xFF;
waddress[1]=(char)(dwAddress>>8)&0xFF;
waddress[0]=(char)(dwAddress)&0xFF;
return waddress;
}
l
下面的WriteNewEntry()函数把新的入口点写入PE程序原来的入口点处,使PE装载器在载入程序时,直接跳入到MessageBoxA()的入口处,该函数的源代码如下:
l
BOOL CPe::WriteNewEntry(int ret,long offset, DWORD dwAddress)
{
CString strErrMsg;
long retf;
unsigned char waddress[4]={0};
retf=_lseek(ret,offset,SEEK_SET);
if(retf==-1)
{
AfxMessageBox("Error seek.");
return FALSE;
}
memcpy(waddress,StrOfDWord(dwAddress),4);
retf=_write(ret,waddress,4);
if(retf==-1)
{
strErrMsg.Format("Error write: %d",GetLastError());
AfxMessageBox(strErrMsg);
return FALSE;
}
return TRUE;
}
l
下面的WriteMessageBox()函数是把MessageBoxA()的机器代码写入到PE文件中。这个函数显示的对话框标题和显示的字符串内容和长度不是固定的。在这个函数中,首先就计算MessageBoxA()函数的地址和函数的返回地址,然后把重新生成的对话框代码写入到程序中。WriteMessageBox()函数的源代码如下:
l
BOOL CPe::WriteMessageBox(int ret,long offset,CString strCap,CString
strTxt)
{
CString strAddress1,strAddress2;
unsigned char waddress[4]={0};
DWORD dwAddress;
// 获取MessageBox在内存中的地址
HINSTANCE gLibMsg=LoadLibrary("user32.dll");
dwMessageBoxAadaddress=(DWORD)GetProcAddress(gLibMsg,"MessageBoxA");
// 计算校验位
int nLenCap1 =strCap.GetLength()+1; // 加上字符串后面的结束位
int nLenTxt1 =strTxt.GetLength()+1; // 加上字符串后面的结束位
int nTotLen=nLenCap1+nLenTxt1+24;
// 重新计算MessageBox函数的地址
dwAddress=dwMessageBoxAadaddress-(dwProgRAV+dwNewEntryAddress+nTot
Len-5);
strAddress1=StrOfDWord(dwAddress);
// 计算返回地址
dwAddress=0-(dwNewEntryAddress-dwOldEntryAddress+nTotLen);
strAddress2=StrOfDWord(dwAddress);
// 对话框头代码(固定)
unsigned char cHeader[2]={0x6a,0x40};
// 标题定义
unsigned char cDesCap[5]={0xe8,nLenCap1,0x00,0x00,0x00};
// 内容定义
unsigned char cDesTxt[5]={0xe8,nLenTxt1,0x00,0x00,0x00};
// 对话框后部分的代码段
unsigned char cFix[12]
={0x6a,0x00,0xe8,0x00,0x00,0x00,0x00,0xe9,0x00,0x00,0x00,0x00};
// 修改对话框后部分的代码段
for(int i=0;i<4;i++)
cFix[3+i]=strAddress1.GetAt(i);
for(i=0;i<4;i++)
cFix[8+i]=strAddress2.GetAt(i);
char* cMessageBox=new char[nTotLen];
char* cMsg;
// 生成对话框命令字符串
memcpy((cMsg = cMessageBox),(char*)cHeader,2);
memcpy((cMsg += 2),cDesCap,5);
memcpy((cMsg += 5),strCap,nLenCap1);
memcpy((cMsg += nLenCap1),cDesTxt,5);
memcpy((cMsg += 5),strTxt,nLenTxt1);
memcpy((cMsg += nLenTxt1),cFix,12);
// 向应用程序写入对话框代码
CString strErrMsg;
long retf;
retf=_lseek(ret,(long)dwEntryWrite,SEEK_SET);
if(retf==-1)
{
delete[] cMessageBox;
AfxMessageBox("Error seek.");
return FALSE;
}
retf=_write(ret,cMessageBox,nTotLen);
if(retf==-1)
{
delete[] cMessageBox;
strErrMsg.Format("Error write: %d",GetLastError());
AfxMessageBox(strErrMsg);
return FALSE;
}
delete[] cMessageBox;
return TRUE;
}
l
下面的WriteFile()函数是总的写入函数。在这个函数中,先打开被修改的PE文件,然后调用WriteNewEntry()和WriteMessageBox()函数。WriteFile()函数的源代码如下:
l
void CPe::WriteFile(CString strFileName)
{
CString strAddress1,strAddress2;
int ret;
unsigned char waddress[4]={0};
ret=_open(strFileName,_O_RDWR | _O_CREAT | _O_BINARY,_S_IREAD | _S_
IWRITE);
if(!ret)
{
AfxMessageBox("Error open");
return;
}
// 把新的入口地址写入文件,程序的入口地址在偏移PE文件头开始第40位
if(!WriteNewEntry(ret,(long)(dwPeAddress+40),dwNewEntryAddress))
return;
// 把对话框代码写入到应用程序中
if(!WriteMessageBox(ret,(long)dwEntryWrite,"Test","We
are the world!"))
return;
_close(ret);
}
l
仅仅利用以上CPe类还是不能对一个PE文件进行注入MessageBoxA()代码的修改,还必须要一个“载体程序”。例如:
l
// Pefile.cpp:修改PE文件实例
//
#include "stdafx.h"
#include "Pe.h"
void main()
{
CopyFile("..\\calc.exe","..\\calc_shell.exe",FALSE);
CPe a;
a.ModifyPe("..\\calc_shell.exe","We are the world!");
}
l
这个修改后的PE文件运行时,就会先显示对话框,单击“确定”按钮后又继续执行。总之,在了解了PE文件格式后,就可以对某一个PE文件进行修改。本实例只是对PE文件处理的一种应用,在实际中还有更多的其他方面的应用。






