我们知道,很多PE分析工具都可以查看一个EXE文件的引用DLL文件函数表,其实,这个本身就是存储在EXE头部的一个重要信息,今天,我们就来研究一下:
我们借用一张PE结构图来分析:
一个EXE完整的PE结构分五大部分。见上图.
最开头的是部分是DOS部首,DOS部首由两部分组成:DOS的MZ文件标志和DOS stub(DOS存根程序)。之所以设置DOS部首是微软为了兼容原有的DOS系统下的程序而设立的。紧接着的是真正的PE文件头。值得注意的是PE文件头中的IMAGE_OPTIONAL_HEADER32是一个非常重要的结构,PE文件中的导入表、导出表、资源、重定位表等数据的位置和长度都保存在这个结构里。
我们按照顺序,先说说IMAGE_FILE_HEADER。
IMAGE_FILE_HEADER这个结构的定义如下:
01.
typedef
struct
_IMAGE_FILE_HEADER {
02.
00h
WORD
Machine;
//运行平台
03.
02h
WORD
NumberOfSections;
//区块数目
04.
06h
DWORD
TimeDateStamp;
//文件日期时间戳
05.
0Ah
DWORD
PointerToSymbolTable;
//指向符号表
06.
0Eh
DWORD
NumberOfSymbols;
//符号表中的符号数量
07.
12h
WORD
SizeOfOptionalHeader;
//映像可选头结构的大小
08.
14h
WORD
Characteristics;
//文件特征值
09.
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
这个结构体表明一个PE文件的基本特征属性,也是一个PE文件的入口
Machine域说明这个pe文件在什么CPU上运行,具体如下:
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
NumberOfSections
pe文件中区块的数量.
TimeDateStamp
文件日期时间戳,指这个pe文件生成的时间,它的值是从1969年12月31日16:00:00以来的秒数.
PointerToSymbolTable
Coff调试符号表的偏移地址.
NumberOfSymbols
Coff符号表中符号的个数. 这个域和前个域在release版本的程序里是0.
SizeOfOptionalHeader
IMAGE_OPTIONAL_HEADER32结构的大小(即多少字节).我们接着就要提到这个结构了.事实上,pe文件的大部分重要的域都在IMAGE_OPTIONAL_HEADER结构里.
Characteristics
这个域描述pe文件的一些属性信息,比如是否可执行,是否是一个动态连接库等.具体定义如下:
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // 重定位信息被移除
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // 文件可执行
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // 行号被移除
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // 符号被移除
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // 程序能处理大于2G的地址
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32位机器
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // .dbg文件的调试信息被移除
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // 如果在移动介质中,拷到交换文件中运行
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // 如果在网络中,拷到交换文件中运行
#define IMAGE_FILE_SYSTEM 0x1000 // 系统文件
#define IMAGE_FILE_DLL 0x2000 // 文件是一个dll
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // 文件只能运行在单处理器上
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
所以,根据这个结构体的信息,我们就可以判断一个文件究竟是不是一个真正的PE文件,该PE文件的类型是可执行的还是可调用的(DLL)
我们可以写个简单的小程序来读取这个信息:
01.
#include "stdafx.h"
02.
#include "windows.h"
03.
#include "stdio.h"
04.
#include "conio.h"
05.
06.
int
main(
int
argc,
char
* argv[])
07.
{
08.
FILE
*p;
09.
LONG
e_lfanew;
//指向IMAGE_NT_HEADERS32结构在文件中的偏移
10.
IMAGE_FILE_HEADER myfileheader;
11.
12.
p =
fopen
(
"test1.exe"
,
"r+b"
);
//自定义读取的exe文件
13.
if
(p == NULL)
return
-1;
//如果打开失败就返回
14.
15.
fseek
(p,0x3c,SEEK_SET);
//注意这里是指针偏移,也就是绕过开头的DOS区块
16.
fread
(&e_lfanew,4,1,p);
17.
fseek
(p,e_lfanew+4,SEEK_SET);
//指向IMAGE_FILE_HEADER结构的偏移
18.
fread
(&myfileheader,
sizeof
(myfileheader),1,p);
19.
20.
printf
(
"IMAGE_FILE_HEADER结构:\n"
);
21.
printf
(
"Machine : %04X\n"
,myfileheader.Machine);
22.
printf
(
"NumberOfSections : %04X\n"
,myfileheader.NumberOfSections);
23.
printf
(
"TimeDateStamp : %08X\n"
,myfileheader.TimeDateStamp);
24.
printf
(
"PointerToSymbolTable : %08X\n"
,myfileheader.PointerToSymbolTable);
25.
printf
(
"NumberOfSymbols : %08X\n"
,myfileheader.NumberOfSymbols);
26.
printf
(
"SizeOfOptionalHeader : %04X\n"
,myfileheader.SizeOfOptionalHeader);
27.
printf
(
"Characteristics : %04X\n"
,myfileheader.Characteristics);
28.
getch();
29.
return
0;
30.
}
注释比较详细了,大家根据这个就可以读取一个PE文件的基本特征信息了.以上代码VC6编译通过
紧接着上一节,我们来研究下IMAGE_OPTIONAL_HEADER32,这个属于PE中附加结构信息,同样是很重要的。
我们先来看看它的结构:
01.
typedef
struct
_IMAGE_OPTIONAL_HEADER {
02.
//
03.
// Standard fields.
04.
//
05.
06.
00h
WORD
Magic;
//幻数,32位pe文件总为010bh
07.
02h
BYTE
MajorLinkerVersion;
//连接器主版本号
08.
03h
BYTE
MinorLinkerVersion;
//连接器副版本号
09.
04h
DWORD
SizeOfCode;
//代码段总大小
10.
08h
DWORD
SizeOfInitializedData;
//已初始化数据段总大小
11.
0ch
DWORD
SizeOfUninitializedData;
//未初始化数据段总大小
12.
10h
DWORD
AddressOfEntryPoint;
//程序执行入口地址(RVA)
13.
14h
DWORD
BaseOfCode;
//代码段起始地址(RVA)
14.
18h
DWORD
BaseOfData;
//数据段起始地址(RVA)
15.
16.
//
17.
// NT additional fields.
18.
//
19.
20.
1ch
DWORD
ImageBase;
//程序默认的装入起始地址
21.
20h
DWORD
SectionAlignment;
//内存中区块的对齐单位
22.
24h
DWORD
FileAlignment;
//文件中区块的对齐单位
23.
28h
WORD
MajorOperatingSystemVersion;
//所需操作系统主版本号
24.
2ah
WORD
MinorOperatingSystemVersion;
//所需操作系统副版本号
25.
2ch
WORD
MajorImageVersion;
//自定义主版本号
26.
2eh
WORD
MinorImageVersion;
//自定义副版本号
27.
30h
WORD
MajorSubsystemVersion;
//所需子系统主版本号
28.
32h
WORD
MinorSubsystemVersion;
//所需子系统副版本号
29.
34h
DWORD
Win32VersionValue;
//总是0
30.
38h
DWORD
SizeOfImage;
//pe文件在内存中的映像总大小
31.
3ch
DWORD
SizeOfHeaders;
//从pe文件开始到节表(包含节表)的总大小
32.
40h
DWORD
CheckSum;
//pe文件CRC校验和
33.
44h
WORD
Subsystem;
//用户界面使用的子系统类型
34.
46h
WORD
DllCharacteristics;
//为0
35.
48h
DWORD
SizeOfStackReserve;
//为线程的栈初始保留的虚拟内存的默认值
36.
4ch
DWORD
SizeOfStackCommit;
//为线程的栈初始提交的虚拟内存的大小
37.
50h
DWORD
SizeOfHeapReserve;
//为进程的堆保留的虚拟内存的大小
38.
54h
DWORD
SizeOfHeapCommit;
//为进程的堆初始提交的虚拟内存的大小
39.
58h
DWORD
LoaderFlags;
//为0
40.
5ch
DWORD
NumberOfRvaAndSizes;
//数据目录结构数组的项数,总为 00000010h
41.
60h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
//数据目录结构数组
42.
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
这个结构体非常的庞大,大家通过注释可以看出,这个结构体保存了相当全面的PE附件信息。
下面针对重要内容进行一个解释:
Magic 幻数,32位pe文件总为010bh
这个常数的定义如下:
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
MajorLinkerVersion 连接程序的主版本号 如vc6.0的为06h
MinorLinkerVersion 连接程序的次版本号 如vc6.0的为00h
SizeOfCode pe文件代码段的大小.是FileAlignment的整数倍.
SizeOfInitializedData 所有含已初始化数据的块的大小,一般在.data段中.
SizeOfUninitializedData 所有含未初始化数据的块的大小,一般在.bss段中.
AddressOfEntryPoint 程序开始执行的地址,这是一个RVA(相对虚拟地址).对于exe文件,这里是启动代码;对于dll文件,这里是libMain()的地址.
在脱壳时第一件事就是找入口点,指的就是这个值.
BaseOfCode 代码段基地址,微软的连接程序生成的程序一般把这个值置为1000h,
BaseOfData 数据段基地址
ImageBase pe文件默认的装入地址.windows9x中exe文件为400000h,dll文件为10000000h.
SectionAlignment 内存中区块的对齐单位.区块总是对齐到这个值的整数倍.x86的32位系统上默认值位1000h
FileAlignment pe文件中区块的对齐单位.pe文件中默认值为 200h.
MajorOperatingSystemVersion
MinorOperatingSystemVersion
上面两个域是指运行这个pe文件所需的操作系统的最低版本号.windows95/98和windows nt 4.0 的内部版本号都是 4.0 ,而windows2000的内部版本号是5.0
MajorImageVersion
MinorImageVersion
上面两个域是指用户自定义的pe文件的版本号.可以通过连接程序来设置,如: LINK /VERSION:2.0 MyApp.obj一般在升级时使用.
MajorSubsystemVersion
MinorSubsystemVersion
上面两个域是指运行这个pe文件所要求的子系统的版本号.
Win32VersionValue 总是0
SizeOfImage pe文件装入内存后映像的总大小.如果SectionAlignment域和FileAlignment域相等,那么这个值也是pe文件在硬盘上的大小.
SizeOfHeaders 从文件开始到节表(包含节表)的总大小.其后是各个区段的数据.
CheckSum pe文件的CRC校验和.
Subsystem pe文件的用户界面使用的子系统类型.定义如下:
#define IMAGE_SUBSYSTEM_UNKNOWN 0 // Unknown subsystem.
#define IMAGE_SUBSYSTEM_NATIVE 1 // Image doesn't require a subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2 // Image runs in the Windows GUI subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 // Image runs in the Windows character subsystem.
#define IMAGE_SUBSYSTEM_OS2_CUI 5 // image runs in the OS/2 character subsystem.
#define IMAGE_SUBSYSTEM_POSIX_CUI 7 // image runs in the Posix character subsystem.
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 // image is a native Win9x driver.
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9 // Image runs in the Windows CE subsystem.
DllCharacteristics 总为0
SizeOfStackReserve 为线程的栈初始保留的虚拟内存的大小,默认为00100000h.如果在调用CreateThread函数时指定堆栈的大小为0,被创建的线程的堆栈的初始大小就与这个值相同.
SizeOfStackCommit 为线程的栈初始提交的虚拟内存的大小.微软的连接程序把这个值置为 1000h.
SizeOfHeapReserve 为进程的堆保留的虚拟内存的大小.默认值为 00100000h.
SizeOfHeapCommit 为进程的堆初始提交的虚拟内存的大小.微软的连接程序把这个值置为1000h.
LoaderFlags 通常为0
NumberOfRvaAndSizes 数据目录结构数组的项数,总为 00000010h
这个值定义如下:
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
IMAGE_DATA_DIRECTORY DataDirectory[0x10] 数据目录结构数组
IMAGE_DATA_DIRECTORY结构定义如下:
1.
typedef
struct
_IMAGE_DATA_DIRECTORY {
2.
DWORD
VirtualAddress;
// 相对虚拟地址
3.
DWORD
Size;
//大小
4.
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
这个结构包含了pe文件中重要部分的RVA地址和大小.这个数组使操作系统的加载程序能够快速定位特定的区段.具体定义如下:
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
通过上一节的例子。我们也依旧可以从这个结构体中读出需要的信息.