PE文件整个结构如图:
+-------------------+
| DOS头 |
+-------------------+
| File-Header |
+-------------------+
| Optional-Header |
|- - - - - - - - - -|
| |----------------+
| Data Directories | |
| | |
| ( RVA, size) |-------------+ |
| |
| |---------+ | |
| | | | |
+-------------------+ | | |
| |-----+ | | |
| Section-Headers | | | | |
| |--+ | | | |
| | | | | | |
+-------------------+<-+ | | | |
| | | <-+ | |
| Section Data 1 | | | |
| | | <-----+ |
+-------------------+<----+ |
| | |
| Section Data 2 | |
| | <--------------+
+-------------------+
相关结构摘录MSDN和Winnt.h如下:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header //除了这项和e_magic基本都可以随便填
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
我们关心的为 e_lfanew,该地址指向了PE头结构在文件中的偏移.如我们的这个文件该值为
0x40,而0x40开始就是PE头的开始,标志为"PE"两个字符.
PE头部信息包括一个标志值 "PE\0\0",和FileHeader及OptionHeader,结构如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //总是"PE\0\0"
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
FileHeader的类型定义如下:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections; //Section个数,WinXp不能少于2(即使真正数据段只有一个),其他平台未知.
DWORD TimeDateStamp; //可以随便填
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
OptionHeader类型定义如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint; //入口地址,佷重要
DWORD BaseOfCode; //代码段起始地址
DWORD BaseOfData; //数据段起始地址.
DWORD ImageBase; //NT/2k/XP基本都是0x400000
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem; //这个别乱填,要不然可能又是非法Win32程序,MajorSubsystemVersion也是.
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes; //以下DataDirectory
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //每个地址都是Virtual,就是程序加载后该地址在内存中相对于ImageBase的偏移,带virtual的地址是这样
//在文件中的偏移为 Addr - 对应Section的VirtualAddress + 对应Section的文件内起始偏移
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
其中各个项意义如下:
1 Export table address and size
2 Import table address and size
3 Resource table address and size
4 Exception table address and size
5 Certificate table address and size
6 Base relocation table address and size
7 Debugging information starting address and size
8 Architecture-specific data address and size
9 Global pointer register relative virtual address
10 Thread local storage (TLS) table address and size
11 Load configuration table address and size
12 Bound import table address and size
13 Import address table address and size
14 Delay import descriptor address and size
15 Reserved
16 Reserved
各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;
对应这一结构我们的这个文件各重要字段为:
46 Section个数 : 2
5c 代码大小 : 200
68 OEP : 1000
6c BaseOfCode : 1000
74 ImageBase: 400000
78 Section数据内存对齐: 1000
7c Section数据文件对齐: 200
90 Image在内存整个空间大小: 3000 /2000也行,起始我们只用了1000,但填这个值提示非法Win32程序:(
94 头部大小: 200,好像填400,800也没关系.
B4 DataDirectory中的数目 : 10
c0 输入表地址: 1110
11c 输入地址表地址: 1100
B8-138 为 DataDirectory数组
138 开始为.text节头部 ,在我们这里名字就叫Cloud啦:)
140 size :36
144 Vaddr:1000
148 RawSize: 200
14c PointToRawData: 200
15c 属性 : 60000060 可读写执行段
160 开始为第二个节,这个节我们没有用,但是系统不允许只有一个节,放到这里作样子的了,骗骗加载器.
200 开始为真正的代码开始
300 开始为Import相关链表.
程序加载后内存中情况为
400000开始: MZ . . . 文件头部信息
401000开始: 对应为文件0x200开始的内容
401100开始: 对应为文件0x300开始的Import表 ,但此时加载器已经把各外部函数地址填好了.
ExitPorcess函数的地址填在 401100处
MessageBoxA函数的地址填在 401108处
--------------------改造----------------------
打开UltraEdit再次重写,这次的目标是512字节:
你可以在这里下载它:http://watercloud.diy.163.com/others/Hello2.exe
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000000 4D 5A 33 DB 74 13 58 53 50 83 C0 07 50 53 68 2C MZ3踭.XSP兝.PSh,
00000010 00 40 00 68 32 00 40 00 C3 E8 E8 FF FF FF 45 78 .@.h2.@.描? Ex
00000020 65 44 49 59 00 48 65 6C 6C 6F 21 00 FF 25 20 11 eDIY.Hello!.% .
00000030 40 00 FF 25 28 11 40 00 00 00 00 00 40 00 00 00 @.%(.@.....@...
00000040 50 45 00 00 4C 01 02 00 00 00 00 00 00 00 00 00 PE..L...........
00000050 00 00 00 00 70 00 0F 01 0B 01 00 00 00 02 00 00 ....p...........
00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000070 00 00 00 00 00 00 40 00 00 10 00 00 00 02 00 00 ......@.........
00000080 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 ................
00000090 00 30 00 00 00 02 00 00 00 00 00 00 02 00 00 00 .0..............
000000A0 00 01 00 00 00 00 00 00 00 01 00 00 00 10 00 00 ................
000000B0 00 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 ................
000000C0 30 11 00 00 3C 00 00 00 2E 74 65 78 74 00 00 00 0...<....text...
000000D0 00 02 00 00 00 10 00 00 00 02 00 00 00 01 00 00 ................
000000E0 00 00 00 00 00 00 00 00 00 00 00 00 60 00 00 60 ............`..`
000000F0 2E 72 64 61 74 61 00 00 02 00 00 00 00 20 00 00 .rdata.........
00000100 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000110 00 00 00 00 60 00 00 60 00 00 00 00 00 00 00 00 ....`..`........
00000120 7C 11 00 00 00 00 00 00 98 11 00 00 00 00 00 00 |.......?......
00000130 6C 11 00 00 00 00 00 00 00 00 00 00 8A 11 00 00 l...........?..
00000140 20 11 00 00 74 11 00 00 00 00 00 00 00 00 00 00 ...t...........
00000150 A6 11 00 00 28 11 00 00 00 00 00 00 00 00 00 00 ?..(...........
00000160 00 00 00 00 00 00 00 00 00 00 00 00 7C 11 00 00 ............|...
00000170 00 00 00 00 98 11 00 00 00 00 00 00 AB 00 45 78 ....?......?Ex
00000180 69 74 50 72 6F 63 65 73 73 00 4B 45 52 4E 45 4C itProcess.KERNEL
00000190 33 32 2E 64 6C 6C 00 00 BB 01 4D 65 73 73 61 67 32.dll..?Messag
000001A0 65 42 6F 78 41 00 55 53 45 52 33 32 2E 64 6C 6C eBoxA.USER32.dll
000001B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000001C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000001D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000001E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000001F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
加载后内存空间布局如下:
ImageBase: 400000
程序入口地址 : 400000 即从文件偏移000开始, 我们把代码56字节放到了dos头部
里了.
.text : 401000 -- 对应 文件 off 100 (但程序加载时从000开始加载的)
.data : 没有用
Import表信息在401130处(对应为文件130)
Import1
Org -> 40116c -> 40117c -> ExitProcess
Name -> 40118a -> Kernel32.dll
First -> 401120
Import2
Org -> 401174 -> 401198 -> MessageBoxA
Name -> 4011a6 -> User32.dll
First -> 401128
dll加载后在内存 401120 处存放了ExitProcess的地址
内存401128 处存放了MessageBoxA的地址
程序代码的jmp表就是:
40002c : jmp 401120
400032 : jmp 401128
然后程序里调用API就使用了40002c和400032这两个地址
对应文件最终被加载了两分,
一个在400000 系统自身要使用的表结构,但我们同时把执行代码也
放到了这里,并修改了程序入口指向这里.
另一个在401000处,这里我们主要利用了这个地址除了了Import表.
本来想使用400000处的导入表信息,但我把相关指针修改为000后系统报
非法win32程序,就没有办法了.
文件布局如下:
00-40 : dos头,但我们把代码也放到这里了.
40-58 : PE-File-Header
58-C8 : PE-Option-Header,其中B8-C8为DataDirectory (ExportDirectory和
ImportDirectory)
我们把DataDirectory已经减到不能在小了,无论如何要ImportDirectory.
c8-F0 : .text Section-Header
F0-118 : .rdata Section-Header (没有用处的节信息,但系统不允许只有一个节只好
装模作样放一个.)
好了真正的PE头部信息结束,后面本来是 文件对齐的填充,我们利用这里存放
ImportTable.
120-1B0: Import-Table
而且从中可以看到,就算把DataDirectory都去掉(WinXp去掉后会被认为非法Win32程
序),把Section减到只有
1个(Winxp最低也要两个才是合法文件) 文件头部信息也需要 0x88个字节. 如果支持
100文件对齐,并且不使用
Import表那么可以将PE文件做到256字节 ,但对于让xp也能运行512字节应该是极限了.
最后提供一个512字节的小工具:锁屏(Win2k/Xp)
http://watercloud.diy.163.com/others/LockWin.exe
参考:http://watercloud.nease.net/others/pe.txt 英文 作者不详
http://watercloud.diy.163.com/others/PE文件格式分析.pdf 中文 作者不详
MicroSoft MSDN
本文只是一个记录性文字,且本人水平很差,望大家斧正。