关于PE可执行文件的修改
当前位置:以往代写 > C/C++ 教程 >关于PE可执行文件的修改
2019-06-13

关于PE可执行文件的修改

关于PE可执行文件的修改

副标题#e#

在windows 9x、NT、2000下,所有的可执行文件都是基于Microsoft设计的一种新的文件名目Portable Executable File Format(可移植的执行体),即PE名目。有一些时候,我们需要对这些可执行文件举办修改,下面文字试图具体的描写PE文件的名目及对PE名目文件的修改。

1、PE文件框架组成

DOS MZ header
DOS stub
PE header
Section table
Section 1
Section 2
Section ...
Section n

上表是PE文件布局的总体条理漫衍。所有 PE文件(甚至32位的 DLLs) 必需以一个简朴的 DOS MZ header 开始,在偏移0处有DOS下可执行文件的“MZ符号”,有了它,一旦措施在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随 MZ header 之后的 DOS stub。DOS stub实际上是个有效的EXE,在不支持 PE文件名目标操纵系统中,它将简朴显示一个错误提示,雷同于字符串 " This program cannot run in DOS mode " 可能措施员可按照本身的意图实现完整的 DOS代码。凡是DOS stub由汇编器/编译器自动生成,对我们的用处不是很大,它简朴挪用间断21h处事9来显示字符串"This program cannot run in DOS mode"。

紧接着 DOS stub 的是 PE header。 PE header 是PE相关布局 IMAGE_NT_HEADERS 的简称,个中包括了很多PE装载器用到的重要域。可执行文件在支持PE文件布局的操纵系统中执行时,PE装载器将从 DOS MZ header的偏移3CH处找到 PE header 的起始偏移量。因而跳过了 DOS stub 直接定位到真正的文件头 PE header。

PE文件的真正内容分别成块,称之为sections(节)。每节是一块拥有配合属性的数据,好比“.text”节等,那么,每一节的内容都是什么呢?实际上PE名目标文件把具有沟通属性的内容放入同一个节中,而不必体贴雷同“.text”、“.data”的定名,其定名只是为了便于识别,所有,我们假如对PE名目标文件举办修改,理论上讲可以写入任何一个节内,并调解此节的属性就可以了。


#p#副标题#e#

PE header 接下来的数组布局 section table(节表)。 每个布局包括对应节的属性、文件偏移量、虚拟偏移量等。假如PE文件里有5个节,那么此布局数组内就有5个成员。

以上就是PE文件名目标物理漫衍,下面将总结一下装载一PE文件的主要步调:

1、 PE文件被执行,PE装载器查抄 DOS MZ header 里的 PE header 偏移量。假如找到,则跳转到 PE header。

2、PE装载器查抄 PE header 的有效性。假如有效,就跳转到PE header的尾部。

3、紧跟 PE header 的是节表。PE装载器读取个中的节信息,并回收文件映射要领将这些节映射到内存,同时付上节内外指定的节属性。

4、PE文件映射入内存后,PE装载器将处理惩罚PE文件中雷同 import table(引入表)逻辑部门。

上述步调是一些前辈阐明的功效简述。

2、PE文件头概述

我们可以在winnt.h这个文件中找到关于PE文件头的界说:

typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
//PE文件头符号 :“PE\0\0”。在开始DOS header的偏移3CH地方指向的地点开始
IMAGE_FILE_HEADER FileHeader; //PE文件物理漫衍的信息
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //PE文件逻辑漫衍的信息
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //该文件运行所需要的CPU,对付Intel平台是14Ch
WORD NumberOfSections; //文件的节数目
DWORD TimeDateStamp; //文件建设日期和时间
DWORD PointerToSymbolTable; //用于调试
DWORD NumberOfSymbols; //标记表中标记个数
WORD SizeOfOptionalHeader; //OptionalHeader 布局巨细
WORD Characteristics; //文件信息标志,区分文件是exe照旧dll
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; //符号字(老是010bh)
BYTE MajorLinkerVersion; //毗连器版本号
BYTE MinorLinkerVersion; //
DWORD SizeOfCode; //代码段巨细
DWORD SizeOfInitializedData; //已初始化数据块巨细
DWORD SizeOfUninitializedData; //未初始化数据块巨细
DWORD AddressOfEntryPoint; //PE装载器筹备运行的PE文件的第一个指令的RVA,若要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。(很多文章都有先容RVA,请去相识)
DWORD BaseOfCode; //代码段起始RVA
DWORD BaseOfData; //数据段起始RVA
DWORD ImageBase; //PE文件的装载地点
DWORD SectionAlignment; //块对齐
DWORD FileAlignment; //文件块对齐
WORD MajorOperatingSystemVersion;//所需操纵系统版本号
WORD MinorOperatingSystemVersion;//
WORD MajorImageVersion; //用户自界说版本号
WORD MinorImageVersion; //
WORD MajorSubsystemVersion; //win32子系统版本。若PE文件是专门为Win32设计的
WORD MinorSubsystemVersion; //该子系统版本肯定是4.0不然对话框不会有3维立体感
DWORD Win32VersionValue; //保存
DWORD SizeOfImage; //内存中整个PE映像体的尺寸
DWORD SizeOfHeaders; //所有头+节表的巨细
DWORD CheckSum; //校验和
WORD Subsystem; //NT用来识别PE文件属于哪个子系统
WORD DllCharacteristics; //
DWORD SizeOfStackReserve; //
DWORD SizeOfStackCommit; //
DWORD SizeOfHeapReserve; //
DWORD SizeOfHeapCommit; //
DWORD LoaderFlags; //
DWORD NumberOfRvaAndSizes; //
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
//IMAGE_DATA_DIRECTORY 布局数组。每个布局给出一个重要数据布局的RVA,好比引入地点表等
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //表的RVA地点
DWORD Size; //巨细
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

#p#副标题#e#

PE文件头后是节表,在winnt.h下如下界说

#p#分页标题#e#

typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//节表名称,如“.text”
union {
DWORD PhysicalAddress; //物理地点
DWORD VirtualSize; //真实长度
} Misc;
DWORD VirtualAddress; //RVA
DWORD SizeOfRawData; //物理长度
DWORD PointerToRawData; //节基于文件的偏移量
DWORD PointerToRelocations; //重定位的偏移
DWORD PointerToLinenumbers; //行号表的偏移
WORD NumberOfRelocations; //重定位项数目
WORD NumberOfLinenumbers; //行号表的数目
DWORD Characteristics; //节属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

以上布局就是在winnt.h中关于PE文件头的界说,如何我们用C/C++来举办PE可执行文件操纵,就要用到上面的所有布局,它具体的描写了PE文件头的布局。

3、修改PE可执行文件

此刻让我们把一段代码写入任何一个PE名目标可执行文件,代码如下:

-- test.asm --
.386p
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
.code
start:
INVOKE MessageBoxA,0,0,0,MB_ICONINFORMATION or MB_OK
ret
end start
以上代码只显示一个MessageBox框,编译后获得二进制代码如下:
unsigned char writeline[18]={
0x6a,0x40,0x6a,0x0,0x6a,0x0,0x6a,0x0,0xe8,0x01,0x0,0x0,0x0,0xe9,0x0,0x0,0x0,0x0
};

好,此刻让我们看看该把这些代码写到那。此刻用Tdump.exe显示一个PE名目得可执行文件信息,可以发明如下描写:

#p#副标题#e#

Object table:
# Name VirtSize RVA PhysSize Phys off Flags
-- -------- -------- -------- -------- -------- --------
01 .text 0000CCC0 00001000 0000CE00 00000600 60000020 [CER]
02 .data 00004628 0000E000 00002C00 0000D400 C0000040 [IRW]
03 .rsrc 000003C8 00013000 00000400 00010000 40000040 [IR]
Key to section flags:
C - contains code
E - executable
I - contains initialized data
R - readable
W - writeable

上面描写此文件中存在3个段及每个段得信息,实际上我们的代码可以写入任何一个段,这里我选择“.text”段。

用如下代码获得一个PE名目可执行文件的头信息:

//writePE.cpp
#include <windows.h>
#include <stdio.h>
#include <io.h>
#include <fcntl.h>
#include <time.h>
#include <SYS\STAT.H>
unsigned char writeline[18]={
0x6a,0x40,0x6a,0x0,0x6a,0x0,0x6a,0x0,0xe8,0x01,0x0,0x0,0x0,0xe9,0x0,0x0,0x0,0x0
};
DWORD space;
DWORD entryaddress;
DWORD entrywrite;
DWORD progRAV;
DWORD oldentryaddress;
DWORD newentryaddress;
DWORD codeoffset;
DWORD peaddress;
DWORD flagaddress;
DWORD flags;
DWORD virtsize;
DWORD physaddress;
DWORD physsize;
DWORD MessageBoxAadaddress;
int main(int argc,char * * argv)
{
HANDLE hFile, hMapping;
void *basepointer;
FILETIME * Createtime;
FILETIME * Accesstime;
FILETIME * Writetime;
Createtime = new FILETIME;
Accesstime = new FILETIME;
Writetime = new FILETIME;
if ((hFile = CreateFile(argv[1], GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0)) == INVALID_HANDLE_VALUE)//打开要修改的文件
{
puts("(could not open)");
return EXIT_FAILURE;
}
if(!GetFileTime(hFile,Createtime,Accesstime,Writetime))
{
printf("\nerror getfiletime: %d\n",GetLastError());
}
//获得要修改文件的建设、修改等时间
if (!(hMapping = CreateFileMapping(hFile, 0, PAGE_READONLY | SEC_COMMIT, 0, 0, 0)))
{
puts("(mapping failed)");
CloseHandle(hFile);
return EXIT_FAILURE;
}
if (!(basepointer = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0)))
{
puts("(view failed)");
CloseHandle(hMapping);
CloseHandle(hFile);
return EXIT_FAILURE;
}
//把文件头映象存入baseointer
CloseHandle(hMapping);
CloseHandle(hFile);
map_exe(basepointer);//获得相关地点
UnmapViewOfFile(basepointer);
printaddress();
printf("\n\n");
if(space<50)
{
printf("\n旷地太小,数据不能写入.\n");
}
else
{
writefile();//写文件
}
if ((hFile = CreateFile(argv[1], GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0)) == INVALID_HANDLE_VALUE)
{
puts("(could not open)");
return EXIT_FAILURE;
}
if(!SetFileTime(hFile,Createtime,Accesstime,Writetime))
{
printf("error settime : %d\n",GetLastError());
}
//规复修改后文件的成立时间等
delete Createtime;
delete Accesstime;
delete Writetime;
CloseHandle(hFile);
return 0;
}
void map_exe(const void *base)
{
IMAGE_DOS_HEADER * dos_head;
dos_head =(IMAGE_DOS_HEADER *)base;
#include <pshpack1.h>
typedef struct PE_HEADER_MAP
{
DWORD signature;
IMAGE_FILE_HEADER _head;
IMAGE_OPTIONAL_HEADER opt_head;
IMAGE_SECTION_HEADER section_header[];
} peHeader;
#include <poppack.h>
if (dos_head->e_magic != IMAGE_DOS_SIGNATURE)
{
puts("unknown type of file");
return;
}
peHeader * header;
header = (peHeader *)((char *)dos_head + dos_head->e_lfanew);//获得PE文件头
if (IsBadReadPtr(header, sizeof(*header))
{
puts("(no PE header, probably DOS executable)");
return;
}
DWORD mods;
char tmpstr[4]={0};
DWORD tmpaddress;
DWORD tmpaddress1;
if(strstr((const char *)header->section_header[0].Name,".text")!=NULL)
{
virtsize=header->section_header[0].Misc.VirtualSize;
//此段的真实长度
physaddress=header->section_header[0].PointerToRawData;
//此段的物理偏移
physsize=header->section_header[0].SizeOfRawData;
//此段的物理长度
peaddress=dos_head->e_lfanew;
//获得PE文件头的开始偏移
peHeader peH;
tmpaddress=(unsigned long )&peH;
//获得布局的偏移
tmpaddress1=(unsigned long )&(peH.section_header[0].Characteristics);
//获得变量的偏移
flagaddress=tmpaddress1-tmpaddress+2;
//获得属性的相对偏移
flags=0x8000;
//一般环境下,“.text”段是不行读写的,假如我们要把数据写入这个段需要改变其属性,实际上这个措施并没有把数据写入“.text”段,所以并不需要变动,但假如你实现巨大的成果,必定需要数据,必定需要变动这个值,
space=physsize-virtsize;
//获得代码段的可用空间,用以判定可不行以写入我们的代码
//用此段的物理长度减去此段的真实长度就可以获得
progRAV=header->opt_head.ImageBase;
//获得措施的装载地点,一般为400000
codeoffset=header->opt_head.BaseOfCode-physaddress;
//获得代码偏移,用代码段起始RVA减去此段的物理偏移
//应为措施的进口计较公式是一个相对的偏移地点,计较公式为:
//代码的写入地点+codeoffset
entrywrite=header->section_header[0].PointerToRawData+header->section_header[0].Misc.VirtualSize;
//代码写入的物理偏移
mods=entrywrite%16;
//对齐界线
if(mods!=0)
{
entrywrite+=(16-mods);
}
oldentryaddress=header->opt_head.AddressOfEntryPoint;
//生存旧的措施进口地点
newentryaddress=entrywrite+codeoffset;
//计较新的措施进口地点
return;
}
void printaddress()
{
HINSTANCE gLibMsg=NULL;
DWORD funaddress;
gLibMsg=LoadLibrary("user32.dll");
funaddress=(DWORD)GetProcAddress(gLibMsg,"MessageBoxA");
MessageBoxAadaddress=funaddress;
gLibAMsg=LoadLibrary("kernel32.dll");
//获得MessageBox在内存中的地点,以便我们利用
}
void writefile()
{
int ret;
long retf;
DWORD address;
int tmp;
unsigned char waddress[4]={0};
ret=_open(filename,_O_RDWR | _O_CREAT | _O_BINARY,_S_IREAD | _S_IWRITE);
if(!ret)
{
printf("error open\n");
return;
}
retf=_lseek(ret,(long)peaddress+40,SEEK_SET);
//措施的进口地点在PE文件头开始的40处
if(retf==-1)
{
printf("error seek\n");
return;
}
address=newentryaddress;
tmp=address>>24;
waddress[3]=tmp;
tmp=address<<8;
tmp=tmp>>24;
waddress[2]=tmp;
tmp=address<<16;
tmp=tmp>>24;
waddress[1]=tmp;
tmp=address<<24;
tmp=tmp>>24;
waddress[0]=tmp;
retf=_write(ret,waddress,4);
//把新的进口地点写入文件
if(retf==-1)
{
printf("error write: %d\n",GetLastError());
return;
}
retf=_lseek(ret,(long)entrywrite,SEEK_SET);
if(retf==-1)
{
printf("error seek\n");
return;
}
retf=_write(ret,writeline,18);
if(retf==-1)
{
printf("error write: %d\n",GetLastError());
return;
}
//把writeline写入我们计较出的空间
retf=_lseek(ret,(long)entrywrite+9,SEEK_SET);
//变动MessageBox函数地点,它的二进制代码在writeline[10]处
if(retf==-1)
{
printf("error seek\n");
return;
}
address=MessageBoxAadaddress-(progRAV+newentryaddress+9+4);
//从头计较MessageBox函数的地点,MessageBox函数的原地点减去措施的装载地点加上新的进口地点加9(它的二进制代码相对偏移)加上4(地点长度)
tmp=address>>24;
waddress[3]=tmp;
tmp=address<<8;
tmp=tmp>>24;
waddress[2]=tmp;
tmp=address<<16;
tmp=tmp>>24;
waddress[1]=tmp;
tmp=address<<24;
tmp=tmp>>24;
waddress[0]=tmp;
retf=_write(ret,waddress,4);
//写入从头计较的MessageBox地点
if(retf==-1)
{
printf("error write: %d\n",GetLastError());
return;
}
retf=_lseek(ret,(long)entrywrite+14,SEEK_SET);
//变动返回地点,用jpm返回原措施进口地点,其它的二进制代码在writeline[15]处
if(retf==-1)
{
printf("error seek\n");
return;
}
address=0-(newentryaddress-oldentryaddress+4+15);
//返回地点计较的要领是新的进口地点减去老的进口地点加4(地点长度)加15(二进制代码相对偏移)后取反
tmp=address>>24;
waddress[3]=tmp;
tmp=address<<8;
tmp=tmp>>24;
waddress[2]=tmp;
tmp=address<<16;
tmp=tmp>>24;
waddress[1]=tmp;
tmp=address<<24;
tmp=tmp>>24;
waddress[0]=tmp;
retf=_write(ret,waddress,4);
//写入返回地点
if(retf==-1)
{
printf("error write: %d\n",GetLastError());
return;
}
_close(ret);
printf("\nall done...\n");
return;
}
//end

#p#分页标题#e#

由于在PE名目标文件中,所有的地点都利用RVA地点,所以一些函数挪用和返回地点都要颠末计较才可以获得,以上是我在实践中的心得,假如你有更好的步伐,真心的但愿你能汇报我。

    关键字:

在线提交作业