在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格式的文件進行修改,理論上講可以寫入任何一個節內,並調整此節的屬性就可以了。
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;
PE文件頭後是節表,在winnt.h下如下定義
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格式得可執行文件信息,可以發現如下描述:
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
由於在PE格式的文件中,所有的地址都使用RVA地址,所以一些函數調用和返回地址都要經過計算才可以得到,以上是我在實踐中的心得,如果你有更好的辦法,真心的希望你能告訴我。