首页 新闻资讯 技术资料 论坛 网站地图
SOPC 安防产品 存储器 接口电路 应用产品 通信产品 嵌入式开发新闻
电子元器件搜索:
IC库存(8958万) PDF资料(329万) IC价格 IC求购 资讯 技术资料
维库电子市场网是知名的电子元器件交易网站, 为电子生产企业提供IC库存和技术资料查询服务。
位置: 首页 > 详细信息
  关于PE可执行文件的修改
出处:嵌入式技术网 时间: 2007-11-15

在windows&nbsp9x、NT、2000下,所有的可执行文件都是基于Microsoft设计的一种新的文件格式Portable&nbspExecutable&nbspFile&nbspFormat(可移植的执行体),即PE格式。有一些时候,我们需要对这些可执行文件进行修改,下面文字试图详细的描述PE文件的格式及对PE格式文件的修改。
1、PE文件框架构成
DOS&nbspMZ&nbspheader
DOS&nbspstub
PE&nbspheader
Section&nbsptable
Section&nbsp1
Section&nbsp2
Section ...
Section&nbspn
上表是PE文件结构的总体层次分布。所有&nbspPE文件(甚至32位的&nbspDLLs) 必须以一个简单的&nbspDOS&nbspMZ&nbspheader 开始,在偏移0处有DOS下可执行文件的“MZ标志”,有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随&nbspMZ&nbspheader 之后的&nbspDOS&nbspstub。DOS&nbspstub实际上是个有效的EXE,在不支持&nbspPE文件格式的操作系统中,它将简单显示一个错误提示,类似于字符串 "&nbspThis&nbspprogram&nbspcannot&nbsprun&nbspin&nbspDOS&nbspmode " 或者程序员可根据自己的意图实现完整的&nbspDOS代码。通常DOS&nbspstub由汇编器/编译器自动生成,对我们的用处不是很大,它简单调用中断21h服务9来显示字符串"This&nbspprogram&nbspcannot&nbsprun&nbspin&nbspDOS&nbspmode"。
紧接着&nbspDOS&nbspstub 的是&nbspPE&nbspheader。&nbspPE&nbspheader 是PE相关结构&nbspIMAGE_NT_HEADERS 的简称,其中包含了许多PE装载器用到的重要域。可执行文件在支持PE文件结构的操作系统中执行时,PE装载器将从&nbspDOS&nbspMZ&nbspheader的偏移3CH处找到&nbspPE&nbspheader 的起始偏移量。因而跳过了&nbspDOS&nbspstub 直接定位到真正的文件头&nbspPE&nbspheader。
PE文件的真正内容划分成块,称之为sections(节)。每节是一块拥有共同属性的数据,比如“.text”节等,那么,每一节的内容都是什么呢?实际上PE格式的文件把具有相同属性的内容放入同一个节中,而不必关心类似“.text”、“.data”的命名,其命名只是为了便于识别,所有,我们如果对PE格式的文件进行修改,理论上讲可以写入任何一个节内,并调整此节的属性就可以了。
PE&nbspheader 接下来的数组结构&nbspsection&nbsptable(节表)。 每个结构包含对应节的属性、文件偏移量、虚拟偏移量等。如果PE文件里有5个节,那么此结构数组内就有5个成员。
以上就是PE文件格式的物理分布,下面将总结一下装载一PE文件的主要步骤:
1、   &nbspPE文件被执行,PE装载器检查&nbspDOS&nbspMZ&nbspheader 里的&nbspPE&nbspheader 偏移量。如果找到,则跳转到&nbspPE&nbspheader。
2、PE装载器检查&nbspPE&nbspheader 的有效性。如果有效,就跳转到PE&nbspheader的尾部。
3、紧跟&nbspPE&nbspheader 的是节表。PE装载器读取其中的节信息,并采用文件映射方法将这些节映射到内存,同时付上节表里指定的节属性。
4、PE文件映射入内存后,PE装载器将处理PE文件中类似&nbspimport&nbsptable(引入表)逻辑部分。
上述步骤是一些前辈分析的结果简述。
2、PE文件头概述
我们可以在winnt.h这个文件中找到关于PE文件头的定义:
typedef&nbspstruct _IMAGE_NT_HEADERS {
DWORD&nbspSignature;               
file://PE文件头标志 :“PE\0\0”。在开始DOS&nbspheader的偏移3CH处所指向的地址开始
IMAGE_FILE_HEADER&nbspFileHeader;        file://PE文件物理分布的信息
IMAGE_OPTIONAL_HEADER32&nbspOptionalHeader;    file://PE文件逻辑分布的信息
}&nbspIMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

typedef&nbspstruct _IMAGE_FILE_HEADER {
WORD   &nbspMachine;            file://该文件运行所需要的CPU,对于Intel平台是14Ch
WORD   &nbspNumberOfSections;        file://文件的节数目
DWORD  &nbspTimeDateStamp;        file://文件创建日期和时间
DWORD  &nbspPointerToSymbolTable;    file://用于调试
DWORD  &nbspNumberOfSymbols;        file://符号表中符号个数
WORD   &nbspSizeOfOptionalHeader;    file://OptionalHeader 结构大小
WORD   &nbspCharacteristics;        file://文件信息标记,区分文件是exe还是dll
}&nbspIMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

typedef&nbspstruct _IMAGE_OPTIONAL_HEADER {
WORD   &nbspMagic;            file://标志字(总是010bh)
BYTE   &nbspMajorLinkerVersion;        file://连接器版本号
BYTE   &nbspMinorLinkerVersion;        //
DWORD  &nbspSizeOfCode;            file://代码段大小
DWORD  &nbspSizeOfInitializedData;    file://已初始化数据块大小
DWORD  &nbspSizeOfUninitializedData;    file://未初始化数据块大小
DWORD  &nbspAddressOfEntryPoint;    file://PE装载器准备运行的PE文件的第一个指令的RVA,若要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。(许多文章都有介绍RVA,请去了解)
DWORD  &nbspBaseOfCode;            file://代码段起始RVA
DWORD  &nbspBaseOfData;            file://数据段起始RVA
DWORD  &nbspImageBase;            file://PE文件的装载地址
DWORD  &nbspSectionAlignment;        file://块对齐
DWORD  &nbspFileAlignment;        file://文件块对齐
WORD   &nbspMajorOperatingSystemVersion;//所需操作系统版本号
WORD   &nbspMinorOperatingSystemVersion;//
WORD   &nbspMajorImageVersion;        file://用户自定义版本号
WORD   &nbspMinorImageVersion;        //
WORD   &nbspMajorSubsystemVersion;    file://win32子系统版本。若PE文件是专门为Win32设计的
WORD   &nbspMinorSubsystemVersion;    file://该子系统版本必定是4.0否则对话框不会有3维立体感
DWORD  &nbspWin32Versionvalue;        file://保留
DWORD  &nbspSizeOfImage;            file://内存中整个PE映像体的尺寸
DWORD  &nbspSizeOfHeaders;        file://所有头+节表的大小
DWORD  &nbspCheckSum;            file://校验和
WORD   &nbspSubsystem;            file://NT用来识别PE文件属于哪个子系统
WORD   &nbspDllCharacteristics;        //
DWORD  &nbspSizeOfStackReserve;        //
DWORD  &nbspSizeOfStackCommit;        //
DWORD  &nbspSizeOfHeapReserve;        //
DWORD  &nbspSizeOfHeapCommit;        //
DWORD  &nbspLoaderFlags;            //
DWORD  &nbspNumberOfRvaAndSizes;    //
IMAGE_DATA_DIRECTORY&nbspDataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
file://IMAGE_DATA_DIRECTORY 结构数组。每个结构给出一个重要数据结构的RVA,比如引入地址表等
}&nbspIMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

typedef&nbspstruct _IMAGE_DATA_DIRECTORY {
DWORD  &nbspVirtualAddress;        file://表的RVA地址
DWORD  &nbspSize;                file://大小
}&nbspIMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

PE文件头后是节表,在winnt.h下如下定义
typedef&nbspstruct _IMAGE_SECTION_HEADER {
BYTE   &nbspName[IMAGE_SIZEOF_SHORT_NAME];//节表名称,如“.text”
union {
   &nbspDWORD  &nbspPhysicalAddress;    file://物理地址           
   &nbspDWORD  &nbspVirtualSize;        file://真实长度
}&nbspMisc;
DWORD  &nbspVirtualAddress;        file://RVA
DWORD  &nbspSizeOfRawData;        file://物理长度
DWORD  &nbspPointerToRawData;        file://节基于文件的偏移量
DWORD  &nbspPointerToRelocations;    file://重定位的偏移
DWORD  &nbspPointerToLinenumbers;    file://行号表的偏移
WORD   &nbspNumberOfRelocations;    file://重定位项数目
WORD   &nbspNumberOfLinenumbers;    file://行号表的数目
DWORD  &nbspCharacteristics;        file://节属性
}&nbspIMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

以上结构就是在winnt.h中关于PE文件头的定义,如何我们用C/C++来进行PE可执行文件操作,就要用到上面的所有结构,它详细的描述了PE文件头的结构。

3、修改PE可执行文件
现在让我们把一段代码写入任何一个PE格式的可执行文件,代码如下:
--&nbsptest.asm --
.386p
.model&nbspflat,&nbspstdcall
option&nbspcasemap:none

include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.code

start:
   &nbspINVOKE&nbspMessageBoxA,0,0,0,MB_ICONINFORMATION&nbspor&nbspMB_OK
   &nbspret
end&nbspstart

以上代码只显示一个MessageBox框,编译后得到二进制代码如下:
unsigned&nbspchar&nbspwriteline[18]={
0x6a,0x40,0x6a,0x0,0x6a,0x0,0x6a,0x0,0xe8,0x01,0x0,0x0,0x0,0xe9,0x0,0x0,0x0,0x0
};

好,现在让我们看看该把这些代码写到那。现在用Tdump.exe显示一个PE格式得可执行文件信息,可以发现如下描述:
Object&nbsptable:
#  &nbspName     &nbspVirtSize   &nbspRVA    &nbspPhysSize &nbspPhys&nbspoff &nbspFlags  
--  --------  --------  --------  --------  --------  --------
01  .text    &nbsp0000CCC0 &nbsp00001000 &nbsp0000CE00 &nbsp00000600 &nbsp60000020 [CER]
02  .data    &nbsp00004628 &nbsp0000E000 &nbsp00002C00 &nbsp0000D400 &nbspC0000040 [IRW]
03  .rsrc    &nbsp000003C8 &nbsp00013000 &nbsp00000400 &nbsp00010000 &nbsp40000040 [IR]

Key&nbspto&nbspsection&nbspflags:
 &nbspC -&nbspcontains&nbspcode
 &nbspE -&nbspexecutable
 &nbspI -&nbspcontains&nbspinitialized&nbspdata
 &nbspR -&nbspreadable
 &nbspW -&nbspwriteable

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

用如下代码得到一个PE格式可执行文件的头信息:

file://writePE.cpp

#include <windows.h>
#include <stdio.h>
#include <io.h>
#include <fcntl.h>
#include <time.h>
#include <SYS\STAT.H>

unsigned&nbspchar&nbspwriteline[18]={
0x6a,0x40,0x6a,0x0,0x6a,0x0,0x6a,0x0,0xe8,0x01,0x0,0x0,0x0,0xe9,0x0,0x0,0x0,0x0
};

DWORD&nbspspace;
DWORD&nbspentryaddress;
DWORD&nbspentrywrite;
DWORD&nbspprogRAV;
DWORD&nbspoldentryaddress;
DWORD&nbspnewentryaddress;
DWORD&nbspcodeoffset;
DWORD&nbsppeaddress;
DWORD&nbspflagaddress;
DWORD&nbspflags;

DWORD&nbspvirtsize;
DWORD&nbspphysaddress;
DWORD&nbspphyssize;
DWORD&nbspMessageBoxAadaddress;

int&nbspmain(int&nbspargc,char * *&nbspargv)
{
HANDLE&nbsphFile,&nbsphMapping;
void *basepointer;
FILETIME *&nbspCreatetime;
FILETIME *&nbspAccesstime;
FILETIME *&nbspWritetime;
Createtime =&nbspnew&nbspFILETIME;
Accesstime =&nbspnew&nbspFILETIME;
Writetime =&nbspnew&nbspFILETIME;

if ((hFile =&nbspCreateFile(argv[1],&nbspGENERIC_READ|GENERIC_WRITE,&nbspFILE_SHARE_READ|FILE_SHARE_WRITE,&nbsp0,&nbspOPEN_EXISTING,&nbspFILE_FLAG_SEQUENTIAL_SCAN,&nbsp0)) ==&nbspINVALID_HANDLE_value)//打开要修改的文件
{
puts("(could&nbspnot&nbspopen)");
return&nbspEXIT_FAILURE;
}
if(!GetFileTime(hFile,Createtime,Accesstime,Writetime))
{
printf("\nerror&nbspgetfiletime: %d\n",GetLastError());
}
file://得到要修改文件的创建、修改等时间
if (!(hMapping =&nbspCreateFileMapping(hFile,&nbsp0,&nbspPAGE_READONLY |&nbspSEC_COMMIT,&nbsp0,&nbsp0,&nbsp0)))
{
puts("(mapping&nbspfailed)");
CloseHandle(hFile);
return&nbspEXIT_FAILURE;
}
if (!(basepointer =&nbspMapViewOfFile(hMapping,&nbspFILE_MAP_READ,&nbsp0,&nbsp0,&nbsp0)))
{
puts("(view&nbspfailed)");
CloseHandle(hMapping);
CloseHandle(hFile);
return&nbspEXIT_FAILURE;
}
file://把文件头映象存入baseointer
CloseHandle(hMapping);
CloseHandle(hFile);
map_exe(basepointer);//得到相关地址
UnmapViewOfFile(basepointer);
printaddress();
printf("\n\n");
if(space<50)
{
printf("\n空隙太小,数据不能写入.\n");
}
else
{
writefile();//写文件
}

if ((hFile =&nbspCreateFile(argv[1],&nbspGENERIC_READ|GENERIC_WRITE,&nbspFILE_SHARE_READ|FILE_SHARE_WRITE,&nbsp0,&nbspOPEN_EXISTING,&nbspFILE_FLAG_SEQUENTIAL_SCAN,&nbsp0)) ==&nbspINVALID_HANDLE_value)
{
puts("(could&nbspnot&nbspopen)");
return&nbspEXIT_FAILURE;
}

if(!SetFileTime(hFile,Createtime,Accesstime,Writetime))
{
printf("error&nbspsettime : %d\n",GetLastError());
}
file://恢复修改后文件的建立时间等
delete&nbspCreatetime;
delete&nbspAccesstime;
delete&nbspWritetime;
CloseHandle(hFile);
return&nbsp0;
}

void&nbspmap_exe(const&nbspvoid *base)
{
IMAGE_DOS_HEADER *&nbspdos_head;
dos_head =(IMAGE_DOS_HEADER *)base;
#include <pshpack1.h>
typedef&nbspstruct&nbspPE_HEADER_MAP
{
DWORD&nbspsignature;
IMAGE_FILE_HEADER _head;
IMAGE_OPTIONAL_HEADER&nbspopt_head;
IMAGE_SECTION_HEADER&nbspsection_header[];
}&nbsppeHeader;
#include <poppack.h>

if (dos_head->e_magic !=&nbspIMAGE_DOS_SIGNATURE)
{
puts("unknown&nbsptype&nbspof&nbspfile");
return;
}

peHeader *&nbspheader;
header = (peHeader *)((char *)dos_head +&nbspdos_head->e_lfanew);//得到PE文件头
if (IsBadReadPtr(header,&nbspsizeof(*header))
{
puts("(no&nbspPE&nbspheader,&nbspprobably&nbspDOS&nbspexecutable)");
return;
}

DWORD&nbspmods;
char&nbsptmpstr[4]={0};
DWORD &nbsptmpaddress;
DWORD &nbsptmpaddress1;

if(strstr((const&nbspchar *)header->section_header[0].Name,".text")!=NULL)
{
virtsize=header->section_header[0].Misc.VirtualSize;
file://此段的真实长度
physaddress=header->section_header[0].PointerToRawData;
file://此段的物理偏移
physsize=header->section_header[0].SizeOfRawData;
file://此段的物理长度
peaddress=dos_head->e_lfanew;
file://得到PE文件头的开始偏移

peHeader&nbsppeH;
tmpaddress=(unsigned&nbsplong )&peH;
file://得到结构的偏移
tmpaddress1=(unsigned&nbsplong )&(peH.section_header[0].Characteristics);
file://得到变量的偏移
flagaddress=tmpaddress1-tmpaddress+2;
file://得到属性的相对偏移
flags=0x8000;
file://一般情况下,“.text”段是不可读写的,如果我们要把数据写入这个段需要改变其属性,实际上这个程序并没有把数据写入“.text”段,所以并不需要更改,但如果你实现复杂的功能,肯定需要数据,肯定需要更改这个值,

space=physsize-virtsize;
file://得到代码段的可用空间,用以判断可不可以写入我们的代码
file://用此段的物理长度减去此段的真实长度就可以得到
progRAV=header->opt_head.ImageBase;
file://得到程序的装载地址,一般为400000
codeoffset=header->opt_head.BaseOfCode-physaddress;
file://得到代码偏移,用代码段起始RVA减去此段的物理偏移
file://应为程序的入口计算公式是一个相对的偏移地址,计算公式为:
file://代码的写入地址+codeoffset

entrywrite=header->section_header[0].PointerToRawData+header->section_header[0].Misc.VirtualSize;
file://代码写入的物理偏移
mods=entrywrite%16;
file://对齐边界
if(mods!=0)
{
entrywrite+=(16-mods);
}
oldentryaddress=header->opt_head.AddressOfEntryPoint;
file://保存旧的程序入口地址
newentryaddress=entrywrite+codeoffset;
file://计算新的程序入口地址       
return;
}

void&nbspprintaddress()
{
HINSTANCE&nbspgLibMsg=NULL;
DWORD&nbspfunaddress;
gLibMsg=LoadLibrary("user32.dll");
funaddress=(DWORD)GetProcAddress(gLibMsg,"MessageBoxA");
MessageBoxAadaddress=funaddress;
gLibAMsg=LoadLibrary("kernel32.dll");
file://得到MessageBox在内存中的地址,以便我们使用
}

void&nbspwritefile()
{
int&nbspret;
long&nbspretf;
DWORD&nbspaddress;
int&nbsptmp;
unsigned&nbspchar&nbspwaddress[4]={0};

ret=_open(filename,_O_RDWR | _O_CREAT | _O_BINARY,_S_IREAD | _S_IWRITE);
if(!ret)
{
printf("error&nbspopen\n");
return;
}
   
retf=_lseek(ret,(long)peaddress+40,SEEK_SET);
file://程序的入口地址在PE文件头开始的40处
if(retf==-1)
{
printf("error&nbspseek\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);
file://把新的入口地址写入文件
if(retf==-1)
{
printf("error&nbspwrite: %d\n",GetLastError());
return;
}
   
retf=_lseek(ret,(long)entrywrite,SEEK_SET);
if(retf==-1)
{
printf("error&nbspseek\n");
return;
}
retf=_write(ret,writeline,18);
if(retf==-1)
{
printf("error&nbspwrite: %d\n",GetLastError());
return;
}
file://把writeline写入我们计算出的空间

retf=_lseek(ret,(long)entrywrite+9,SEEK_SET);
file://更改MessageBox函数地址,它的二进制代码在writeline[10]处
if(retf==-1)
{
printf("error&nbspseek\n");
return;
}

address=MessageBoxAadaddress-(progRAV+newentryaddress+9+4);
file://重新计算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);
file://写入重新计算的MessageBox地址
if(retf==-1)
{
printf("error&nbspwrite: %d\n",GetLastError());
return;
}

retf=_lseek(ret,(long)entrywrite+14,SEEK_SET);
file://更改返回地址,用jpm返回原程序入口地址,其它的二进制代码在writeline[15]处
if(retf==-1)
{
printf("error&nbspseek\n");
return;
}

address=0-(newentryaddress-oldentryaddress+4+15);
file://返回地址计算的方法是新的入口地址减去老的入口地址加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);
file://写入返回地址
if(retf==-1)
{
printf("error&nbspwrite: %d\n",GetLastError());
return;
}

_close(ret);
printf("\nall&nbspdone...\n");
return;
}

file://end
由于在PE格式的文件中,所有的地址都使用RVA地址,所以一些函数调用和返回地址都要经过计算才可以得到,以上是我在实践中的心得,如果你有更好的办法,真心的希望你能告诉我。

如果存在错误,请告诉我,以免误导看这篇文章的人。
写的较乱,请原谅。


关闭】 【打印
相关专题  
嵌入式开发新闻
存储器
测试测量
AD/DA
综合电子技术
应用产品
工业控制
CPLD/FPGA
军工/航空航天
嵌入式系统
医疗电子
数控系统
安防产品
接口电路
通信产品
计算机外设
IC设计
嵌入式硬件
消费电子
电源系列
汽车电子
SOPC
开发工具
嵌入式软件
软件开发

© 2007 百斯嵌入式开发网 网站地图