转帖处:http://www.netcicala.com/article/sort013/info-102.html
作者:佚名 来源:不详 发布时间:2007-6-6 16:11:37
PE知识学习(一)
PE 文件的知识是基本的知识.网上有很多这方面的资料.然而系统讲解的却不多.我不是这方面的专家,却希望能抛砖引玉,得到这方面的一些指点.
计算机这门科学是实践性很强的一门学问,如果想扎扎实实的学会点东西,还是要亲自动手试一试.
在继续向下看之前,我假定你会基本的C语言和简单的使用VC6.0,以下的例子都用用到这些.除此之外,不再做任何假设.
关于pe的一些结构可以在winnt.h这个头文件里找到.
声明一下:这里所有的结构及常量定义都是基于intel的x86 CPU的,在其他的系统上可能有所不同,你应该去查看相应的资料.关于这点以后不再声明.
首先在pe文件的开始是这样一个结构(为了方便阅读,我加上了字节偏移):
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
00h WORD e_magic; // Magic number **DOS头标记
02h WORD e_cblp; // Bytes on last page of file
04h WORD e_cp; // Pages in file
06h WORD e_crlc; // Relocations
08h WORD e_cparhdr; // Size of header in paragraphs
0ah WORD e_minalloc; // Minimum extra paragraphs needed
0ch WORD e_maxalloc; // Maximum extra paragraphs needed
0eh WORD e_ss; // Initial (relative) SS value
10h WORD e_sp; // Initial SP value
12h WORD e_csum; // Checksum
14h WORD e_ip; // Initial IP value
16h WORD e_cs; // Initial (relative) CS value
18h WORD e_lfarlc; // File address of relocation table
1ah WORD e_ovno; // Overlay number
1ch WORD e_res[4]; // Reserved words
24h WORD e_oemid; // OEM identifier (for e_oeminfo)
26h WORD e_oeminfo; // OEM information; e_oemid specific
28h WORD e_res2[10]; // Reserved words
3ch LONG e_lfanew; // File address of new exe header **指向PE头部
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
这个结构就是DOS MZ头,是为了向下兼容的.当在DOS下运行windows程序不至于出错.
我们只关心两个域:e_magic 和e_lfanew.
e_magic 的值应该等于0x5A4D,像下面定义这样的:
#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ
e_lfanew是一个指针,指向PE文件头在PE文件中的偏移.
PE文件头是这样一个结构,它包含了许多PE装载器要用到的域.
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; **PE文件标识
IMAGE_FILE_HEADER FileHeader; **映像文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; **映像可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature的定义如下:
#define IMAGE_NT_SIGNATURE 0x00004550 // PE00
好了,让我们实际做点事.写个小程序查看一下PE文件的这些信息,并判断一个PE文件是否有效.
代码如下,你可以在VC中建一个控制台程序,然后把这段代码拷进去(我建议你手工输入.什么?你不愿意?哈哈,我可是一个一个字母敲进去的,你比我还懒:) )
#include "stdafx.h"
#include "windows.h"
#include "stdio.h"
int main(int argc, char* argv[])
{
FILE *p;
IMAGE_DOS_HEADER mydosheader;
unsigned long sig;
p = fopen("test1.exe","r+b");
if(p == NULL)return -1;
fread(&mydosheader,sizeof(mydosheader),1,p);
fseek(p,mydosheader.e_lfanew,SEEK_SET);
fread(&sig,4,1,p);
fclose(p);
printf("IMAGE_DOS_HEADER dump:/n");
printf("e_magic : %04x/n",mydosheader.e_magic);
printf("e_cblp : %04x/n",mydosheader.e_cblp);
printf("e_cp : %04x/n",mydosheader.e_cp);
printf("e_crlc : %04x/n",mydosheader.e_crlc);
printf("e_cparhdr : %04x/n",mydosheader.e_cparhdr);
printf("e_minalloc: %04x/n",mydosheader.e_minalloc);
printf("e_maxalloc: %04x/n",mydosheader.e_maxalloc);
printf("e_ss : %04x/n",mydosheader.e_ss);
printf("e_sp : %04x/n",mydosheader.e_sp);
printf("e_csum : %04x/n",mydosheader.e_csum);
printf("e_ip : %04x/n",mydosheader.e_ip);
printf("e_cs : %04x/n",mydosheader.e_cs);
printf("e_lfarlc : %04x/n",mydosheader.e_lfarlc);
printf("e_ovno : %04x/n",mydosheader.e_ovno);
printf("e_res[0] : %04x/n",mydosheader.e_res[0]);
printf("e_oemid : %04x/n",mydosheader.e_oemid);
printf("e_oeminfo : %04x/n",mydosheader.e_oeminfo);
printf("res2[0] : %04x/n",mydosheader.e_res2[0]);
printf("lfanew : %08x/n",mydosheader.e_lfanew);
if((mydosheader.e_magic ==IMAGE_DOS_SIGNATURE) &&
(sig == IMAGE_NT_SIGNATURE))
printf("有效的PE文件/n");
else
printf("无效的PE文件/n");
return 0;
}
运行附件中dump1.exe请在dos窗口下.
PE知识学习(二)
上一贴我们了解了pe头部的dos部首部分,我们知道在这个结构里e_magic和e_lfanew这两个域对我们来说很重要.同时我们也提到了e_lfanew域指向IMAGE_NT_HEADERS32结构在pe文件的偏移.
补充声明一下:这里的知识是适用于32位字的机器上的.
下面我们接着看IMAGE_NT_HEADERS32结构,这个部分在pe文件的学习里至关重要.
IMAGE_NT_HEADERS32的结构定义如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; **PE文件标识 "PE",0,0
IMAGE_FILE_HEADER FileHeader; **映像文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; **映像可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
这其中又包含了两个结构.我们一点一点的往下看.
IMAGE_FILE_HEADER这个结构的定义如下:
typedef struct _IMAGE_FILE_HEADER {
00h WORD Machine; **运行平台
02h WORD NumberOfSections; **区块数目
06h DWORD TimeDateStamp; **文件日期时间戳
0Ah DWORD PointerToSymbolTable; **指向符号表
0Eh DWORD NumberOfSymbols; **符号表中的符号数量
12h WORD SizeOfOptionalHeader; **映像可选头结构的大小
14h WORD Characteristics; **文件特征值
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
我们看一下这几个域:
1) 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
2) NumberOfSections
pe文件中区块的数量,关于区块下面还要讲到,这里先有个印象就可以了.
3)TimeDateStamp
文件日期时间戳,指这个pe文件生成的时间,它的值是从1969年12月31日16:00:00以来的秒数.
4)PointerToSymbolTable
Coff调试符号表的偏移地址.
5)NumberOfSymbols
Coff符号表中符号的个数. 这个域和前个域在release版本的程序里是0.
6)SizeOfOptionalHeader
IMAGE_OPTIONAL_HEADER32结构的大小(即多少字节).我们接着就要提到这个结构了.事实上,pe文件的大部分重要的域都在IMAGE_OPTIONAL_HEADER结构里.
7)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文件的特征值就是这些属性值加在一起的.
希望这些没有让你头晕,其实内容不多,只是一个IMAGE_FILE_HEADER结构,而这个结构包含7个域而已.
让我们先熟悉这个结构,我们编个程序来显示这些信息.
#include "stdafx.h"
#include "windows.h"
#include "stdio.h"
#include "conio.h"
int main(int argc, char* argv[])
{
FILE *p;
LONG e_lfanew; //指向IMAGE_NT_HEADERS32结构在文件中的偏移
IMAGE_FILE_HEADER myfileheader;
p = fopen("test1.exe","r+b");
if(p == NULL)return -1;
fseek(p,0x3c,SEEK_SET);
fread(&e_lfanew,4,1,p);
fseek(p,e_lfanew+4,SEEK_SET); //指向IMAGE_FILE_HEADER结构的偏移
fread(&myfileheader,sizeof(myfileheader),1,p);
printf("IMAGE_FILE_HEADER结构:/n");
printf("Machine : %04X/n",myfileheader.Machine);
printf("NumberOfSections : %04X/n",myfileheader.NumberOfSections);
printf("TimeDateStamp : %08X/n",myfileheader.TimeDateStamp);
printf("PointerToSymbolTable : %08X/n",myfileheader.PointerToSymbolTable);
printf("NumberOfSymbols : %08X/n",myfileheader.NumberOfSymbols);
printf("SizeOfOptionalHeader : %04X/n",myfileheader.SizeOfOptionalHeader);
printf("Characteristics : %04X/n",myfileheader.Characteristics);
getch();
return 0;
}
此程序在win98 + vc6.0 环境下编译通过.
pe知识学习(三)
前两个贴子我们已经介绍了pe文件的两个结构,希望还没有让你看晕.下面我把pe文件的结构列出来,让我们有个全局的印象.
_______________________________
| IMAGE_DOS_HEADER | <-- Dos部首
-------------------------------
| PE,0,0 | <-- PE文件标志
-------------------------------
| IMAGE_FILE_HEADER | <-- 映像文件头
-------------------------------
| IMAGE_OPTIONAL_HEADER32 | <-- 映像可选头
-------------------------------
| Section Table | <-- 节表
-------------------------------
| .text | <-- 代码区段
-------------------------------
| .data | <-- 数据区段
-------------------------------
| .idata | <-- 输入表
-------------------------------
| .edata | <-- 输出表
-------------------------------
| .reloc | <-- 重定位表区段
-------------------------------
| .... |
-------------------------------
| 调试信息 |
-------------------------------
好了,我们接着看看IMAGE_OPTIONAL_HEADER32结构.这个结构的域比较多,但是和后面要讲到的节表一样,非常重要.希望你能够用心体会,并动手实践一下.
IMAGE_OPTIONAL_HEADER32的结构定义如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
00h WORD Magic; //幻数,32位pe文件总为010bh
02h BYTE MajorLinkerVersion; //连接器主版本号
03h BYTE MinorLinkerVersion; //连接器副版本号
04h DWORD SizeOfCode; //代码段总大小
08h DWORD SizeOfInitializedData; //已初始化数据段总大小
0ch DWORD SizeOfUninitializedData; //未初始化数据段总大小
10h DWORD AddressOfEntryPoint; //程序执行入口地址(RVA)
14h DWORD BaseOfCode; //代码段起始地址(RVA)
18h DWORD BaseOfData; //数据段起始地址(RVA)
//
// NT additional fields.
//
1ch DWORD ImageBase; //程序默认的装入起始地址
20h DWORD SectionAlignment; //内存中区块的对齐单位
24h DWORD FileAlignment; //文件中区块的对齐单位
28h WORD MajorOperatingSystemVersion; //所需操作系统主版本号
2ah WORD MinorOperatingSystemVersion; //所需操作系统副版本号
2ch WORD MajorImageVersion; //自定义主版本号
2eh WORD MinorImageVersion; //自定义副版本号
30h WORD MajorSubsystemVersion; //所需子系统主版本号
32h WORD MinorSubsystemVersion; //所需子系统副版本号
34h DWORD Win32VersionValue; //总是0
38h DWORD SizeOfImage; //pe文件在内存中的映像总大小
3ch DWORD SizeOfHeaders; //从pe文件开始到节表(包含节表)的总大小
40h DWORD CheckSum; //pe文件CRC校验和
44h WORD Subsystem; //用户界面使用的子系统类型
46h WORD DllCharacteristics; //为0
48h DWORD SizeOfStackReserve; //为线程的栈初始保留的虚拟内存的默认值
4ch DWORD SizeOfStackCommit; //为线程的栈初始提交的虚拟内存的大小
50h DWORD SizeOfHeapReserve; //为进程的堆保留的虚拟内存的大小
54h DWORD SizeOfHeapCommit; //为进程的堆初始提交的虚拟内存的大小
58h DWORD LoaderFlags; //为0
5ch DWORD NumberOfRvaAndSizes; //数据目录结构数组的项数,总为 00000010h
60h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
//数据目录结构数组
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
下面再具体解释一下各个域的含义.不要闲罗嗦,后面这些知识实在是太重要了.
1)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
2)MajorLinkerVersion 连接程序的主版本号 如vc6.0的为06h
3)MinorLinkerVersion 连接程序的次版本号 如vc6.0的为00h
4)SizeOfCode pe文件代码段的大小.是FileAlignment的整数倍.
5)SizeOfInitializedData 所有含已初始化数据的块的大小,一般在.data段中.
6)SizeOfUninitializedData 所有含未初始化数据的块的大小,一般在.bss段中.
7)AddressOfEntryPoint 程序开始执行的地址,这是一个RVA(相对虚拟地址).对于exe文件,这里是启动代码;对于dll文件,这里是libMain()的地址.
在脱壳时第一件事就是找入口点,指的就是这个值.
8)BaseOfCode 代码段基地址,微软的连接程序生成的程序一般把这个值置为1000h,
9)BaseOfData 数据段基地址
10)ImageBase pe文件默认的装入地址.windows9x中exe文件为400000h,dll文件为10000000h.
11)SectionAlignment 内存中区块的对齐单位.区块总是对齐到这个值的整数倍.x86的32位系统上默认值位1000h
12)FileAlignment pe文件中区块的对齐单位.pe文件中默认值为 200h.
13)MajorOperatingSystemVersion
14)MinorOperatingSystemVersion
上面两个域是指运行这个pe文件所需的操作系统的最低版本号.windows95/98和windows nt 4.0 的内部版本号都是 4.0 ,而windows2000的内部版本号是5.0
15)MajorImageVersion
16)MinorImageVersion
上面两个域是指用户自定义的pe文件的版本号.可以通过连接程序来设置,如: LINK /VERSION:2.0 MyApp.obj一般在升级时使用.
17)MajorSubsystemVersion
18)MinorSubsystemVersion
上面两个域是指运行这个pe文件所要求的子系统的版本号.
19)Win32VersionValue 总是0
20)SizeOfImage pe文件装入内存后映像的总大小.如果SectionAlignment域和FileAlignment域相等,那么这个值也是pe文件在硬盘上的大小.
21)SizeOfHeaders 从文件开始到节表(包含节表)的总大小.其后是各个区段的数据.
22)CheckSum pe文件的CRC校验和.
23)Subsystem pe文件的用户界面使用的子系统类型.定义如下:
#define IMAGE_SUBSYSTEM_UNKNOWN 0 // Unknown subsystem.
#define IMAGE_SUBSYSTEM_NATIVE 1 // Image doesnt 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.
24)DllCharacteristics 总为0
25)SizeOfStackReserve 为线程的栈初始保留的虚拟内存的大小,默认为00100000h.如果在调用CreateThread函数时指定堆栈的大小为0,被创建的线程的堆栈的初始大小就与这个值相同.
26)SizeOfStackCommit 为线程的栈初始提交的虚拟内存的大小.微软的连接程序把这个值置为 1000h.
27)SizeOfHeapReserve 为进程的堆保留的虚拟内存的大小.默认值为 00100000h.
28)SizeOfHeapCommit 为进程的堆初始提交的虚拟内存的大小.微软的连接程序把这个值置为1000h.
29)LoaderFlags 通常为0
30)NumberOfRvaAndSizes 数据目录结构数组的项数,总为 00000010h
这个值定义如下:
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
31)IMAGE_DATA_DIRECTORY DataDirectory[0x10] 数据目录结构数组
IMAGE_DATA_DIRECTORY结构定义如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; 相对虚拟地址
DWORD Size; 大小
} 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
pe知识学习(四)
首先在这里感谢laoxuetong的支持,没有你我可能不会继续写下去了.
下面我们要学习的可以说是pe文件的核心内容了.即块表(section table)和各种块(区段)的结构.这些内容比较多且长.因此我会把写完的先发出来,然后在慢慢的续完.希望你有耐心看下去.学习有时是很枯燥的.因此在适当的时候我也会给出点应用的实例.
你可能还记得,区块的数量在IMAGE_FILE_HEADER结构的NumberOfSections域定义.好了,我们看看和区块密切相关的块表的结构定义.
块表结构的定义如下:
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
00h BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //块名,8个字节长
08h union {
DWORD PhysicalAddress; //obj文件中,区段的实际地址
DWORD VirtualSize; //exe和dll文件中区段在文件中对齐前的大小
} Misc;
0ch DWORD VirtualAddress; //块的RVA(相对虚拟地址)
10h DWORD SizeOfRawData; //在文件中对齐后的大小
14h DWORD PointerToRawData; //在文件中的偏移
18h DWORD PointerToRelocations; //重定位的偏移(obj文件中使用)
1ch DWORD PointerToLinenumbers; //行号表的偏移(调试用)
1eh WORD NumberOfRelocations; //重定位项数目(obj文件中使用)
20h WORD NumberOfLinenumbers; //行号表中行号的数目
24h DWORD Characteristics; //块属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
块表结构描述了区段的一些重要的属性,下面具体解释一下各个域的含义.
1)Name[8] 8个字节的区段名,不足8个后面用0补齐.
2)VirtualSize 在exe和dll文件中这个域包含的是区段还没有按FileAlignment域对齐前的大小.如果这个结构描述的是代码段,那么这个域的值就是实际的代码量的大小.在pe文件的diy时,这个域很有用.它指出了区段中有多少没有使用的空间.我们可以在没有使用的空间里插入自己的代码.好多病毒也是把代码插入剩余的空间里.(呵呵,不要学坏.)
3)VirtualAddress 在exe文件中,这个域是pe文件映射到虚拟内存后该区段的RVA地址.这个值加上基地址(IMAGE_OPTIONAL_HEADER32.ImageBase)后,就得到了该区段在内存中的实际起始地址.
4)SizeOfRawData 这个域是它描述的区段按IMAGE_OPTIONAL_HEADER32.FileAlignment域对齐后在文件中的大小.如果FileAlignment为 0200h,VirtualSize为035Ah,则这个值为 0400h.
5)PointerToRawData 它描述的区段的起始地址在pe文件中的偏移.
6)PointerToRelocations
7)PointerToLinenumbers
8)NumberOfRelocations
9)NumberOfLinenumbers
上面这四个域在发行版本的程序里都是0.
10)Characteristics 该区段的属性信息.用于表示这个区段是代码、数据、可读、可写等等.
这个域定义如下(重要的已经做了中文注释):
// IMAGE_SCN_TYPE_REG 0x00000000 // Reserved.
// IMAGE_SCN_TYPE_DSECT 0x00000001 // Reserved.
// IMAGE_SCN_TYPE_NOLOAD 0x00000002 // Reserved.
// IMAGE_SCN_TYPE_GROUP 0x00000004 // Reserved.
#define IMAGE_SCN_TYPE_NO_PAD 0x00000008 // Reserved.
// IMAGE_SCN_TYPE_COPY 0x00000010 // Reserved.
#define IMAGE_SCN_CNT_CODE 0x00000020 // Section contains code.
//区段包含代码
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // Section contains initialized data.
//区段包含已初始化数据
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // Section contains uninitialized data.
//区段包含未初始化数据
#define IMAGE_SCN_LNK_OTHER 0x00000100 // Reserved.
#define IMAGE_SCN_LNK_INFO 0x00000200 // Section contains comments
// or some other type of information.
// IMAGE_SCN_TYPE_OVER 0x00000400 // Reserved.
#define IMAGE_SCN_LNK_REMOVE 0x00000800 // Section contents will not become part of image.
#define IMAGE_SCN_LNK_COMDAT 0x00001000 // Section contents comdat.
// 0x00002000 // Reserved.
// IMAGE_SCN_MEM_PROTECTED - Obsolete 0x00004000
#define IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000 // Reset speculative exceptions handling bits
// in the TLB entries for this section.
#define IMAGE_SCN_GPREL 0x00008000 // Section content can be accessed relative to GP
#define IMAGE_SCN_MEM_FARDATA 0x00008000
// IMAGE_SCN_MEM_SYSHEAP - Obsolete 0x00010000
#define IMAGE_SCN_MEM_PURGEABLE 0x00020000
#define IMAGE_SCN_MEM_16BIT 0x00020000
#define IMAGE_SCN_MEM_LOCKED 0x00040000
#define IMAGE_SCN_MEM_PRELOAD 0x00080000
#define IMAGE_SCN_ALIGN_1BYTES 0x00100000 //
#define IMAGE_SCN_ALIGN_2BYTES 0x00200000 //
#define IMAGE_SCN_ALIGN_4BYTES 0x00300000 //
#define IMAGE_SCN_ALIGN_8BYTES 0x00400000 //
#define IMAGE_SCN_ALIGN_16BYTES 0x00500000 // Default alignment if no others are specified.
#define IMAGE_SCN_ALIGN_32BYTES 0x00600000 //
#define IMAGE_SCN_ALIGN_64BYTES 0x00700000 //
#define IMAGE_SCN_ALIGN_128BYTES 0x00800000 //
#define IMAGE_SCN_ALIGN_256BYTES 0x00900000 //
#define IMAGE_SCN_ALIGN_512BYTES 0x00A00000 //
#define IMAGE_SCN_ALIGN_1024BYTES 0x00B00000 //
#define IMAGE_SCN_ALIGN_2048BYTES 0x00C00000 //
#define IMAGE_SCN_ALIGN_4096BYTES 0x00D00000 //
#define IMAGE_SCN_ALIGN_8192BYTES 0x00E00000 //
// Unused 0x00F00000
#define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 // Section contains extended relocations.
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // Section can be discarded.
//该区段可丢弃
#define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 // Section is not cachable.
#define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 // Section is not pageable.
#define IMAGE_SCN_MEM_SHARED 0x10000000 // Section is shareable.
//该区段可共享
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 // Section is executable.
//该区段可执行
#define IMAGE_SCN_MEM_READ 0x40000000 // Section is readable.
//该区段可读
#define IMAGE_SCN_MEM_WRITE 0x80000000 // Section is writeable.
//该区段可写
下面我们看一下pe文件里常用的一下区段:
1).text
code
这里一般放的是代码.
2).data
这里一般放的是已初始化的数据.
3).idata
这里一般放的是输入表.这个后面还要详细讲.
4).rsrc
这里一般放的是资源.
5).reloc
这里一般放的是基地址重定位表.
6).edata
这里一般放的是输出表.
7).tls
这里一般是线程局部存储数据.
8).bbs
这里一般放的是未初始化的数据.
pe知识学习(五)
从这贴开始,我介绍几个常用的区段:输入表,输出表和重定位表.
我们知道,程序调用外部的dll函数通常都是下面这种形式:
call my_label
...
my_label: jmp dword ptr [xxxxxxxx]
对一个dll中的函数的调用总是通过一个地址间接的调用的.这些地址就放在输入表里.
输入表(Import Table),简而言之,就是描述该pe文件从哪几个动态连接库导入了什么函数的一组结构数组.在这里我希望能用最简洁的语言让你明白什么是输入表.输入表的组成并不复杂,只用到三个结构.它们是:IMAGE_IMPORT_DESCRIPTOR,IMAGE_THUNK_DATA,IMAGE_IMPORT_BY_NAME.
我们先看一下框图.
IMAGE_IMPORT_DESCRIPTOR
|--------------------|
|-------------------------| OriginalFirstThunk |
| |--------------------|
| | TimeDateStamp |
| |--------------------|
| | ForwarderChain |
| |--------------------|
| | Name |----> "USER32.DLL"
| |--------------------|
| | FirstThunk |---------------------------|
| |--------------------| |
| |
| Hint-name table IMAGE_IMPORT_BY_NAME import address table(IAT) |
| |------------------| |--------------------| |------------------| |
|-> | IMAGE_THUNK_DATA |-->| 44 | "GetMessage" |<--| IMAGE_THUNK_DATA |<---|
|------------------| |----|---------------| |------------------|
| IMAGE_THUNK_DATA |-->| 72 | "LoadIcon" |<--| IMAGE_THUNK_DATA |
|------------------| |----|---------------| |------------------|
| ...... |-->| .. | ...... |<--| ...... |
|------------------| |----|---------------| |------------------|
| NULL | | NULL |
|------------------| |------------------|
当然,这是描述从一个dll中引入函数的情形.从几个dll中引入函数,那么就有几个这样的结构.同时,这也是磁盘文件上的结构.装入内存后FirstThunk指向的结构数组会被修改.可以看下面的图.
我们先来熟悉一下这三个结构的定义:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date/time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //指出函数在所在的dll的输出表中的序号
BYTE Name[1]; //指出要输入的函数的函数名
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
下面我们讲解一下IMAGE_IMPORT_DESCRIPTOR结构的各个域的含义:
1)union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
这个联合指向一个 IMAGE_THUNK_DATA 类型的结构数组.这个联合不是很重要,可以为0.
2)TimeDateStamp
该dll的时间日期戳,一般为0.
3)ForwarderChain
正向连接索引.一般为0.
4)Name
dll名字的RVA.
5)FirstThunk
这个域也是一个RVA,指向一个DWORD数组,数组以NULL结束.数组中的每个DWORD实际上是一个IMAGE_THUNK_DATA结构的联合体。IMAGE_THUNK_DATA联合体通常被解释为一个指向IMAGE_IMPORT_BY_NAME结构的RVA.
从上图我们可以看出有两个并行的指针数组都指向IMAGE_IMPORT_BY_NAME结构.事实上,OriginalFirstThunk指向的IMAGE_THUNK_DATA结构数组从来不被修改,该数组有时也叫提示名表(Hint-name table),提示名表总是指向IMAGE_IMPORT_BY_NAME结构数组.而FirstThunk指向的IMAGE_THUNK_DATA结构数组在该pe文件被加载时,加载程序会修改该数组的内容.加载程序迭代搜索数组的每一个指针,找到每一个IMAGE_IMPORT_BY_NAME结构所对应的输入函数的地址,然后加载程序用找到的地址修改相应的IMAGE_THUNK_DATA结构.
如前面提到的
call my_label
...
my_label: jmp dword ptr [xxxxxxxx]
其中的xxxxxxxx就是FirstThunk指向的IMAGE_THUNK_DATA数组中的一个的值.因为FirstThunk所指向的数组在加载后是所有输入函数的地址,因此它被称为输入地址表(Import Address Table,IAT).
pe文件加载后输入表的情形如下:
IMAGE_IMPORT_DESCRIPTOR
|--------------------|
|-------------------------| OriginalFirstThunk |
| |--------------------|
| | TimeDateStamp |
| |--------------------|
| | ForwarderChain |
| |--------------------|
| | Name |----> "USER32.DLL"
| |--------------------|
| | FirstThunk |---------------------------|
| |--------------------| |
| |
| Hint-name table IMAGE_IMPORT_BY_NAME import address table(IAT) |
| |------------------| |--------------------| |------------------| |
|-> | IMAGE_THUNK_DATA |-->| 44 | "GetMessage" | |ptr of GetMessage |<---|
|------------------| |----|---------------| |------------------|
| IMAGE_THUNK_DATA |-->| 72 | "LoadIcon" | | ptr of LoadIcon |
|------------------| |----|---------------| |------------------|
| ...... |-->| .. | ...... | | ...... |
|------------------| |----|---------------| |------------------|
| NULL | | NULL |
|------------------| |------------------|
输入表在pe知识里是最重要的一部分.希望你能够结合一下pe工具实际理解这部分的内容.
pe知识学习(六)
有输入表就有输出表,本贴开始介绍输出表.
大部分dll都会输出一些函数.有些pe文件也会有输出表.通常输出表都是放在.edata区段的.因此.edata区段的注要成分是函数名表,入口点地址,输出函数的序号.
输出表的开始部分是一个IMAGE_EXPORT_DIRECTORY结构,之后紧接着是由该结构中的某个域所指向的数据.
IMAGE_EXPORT_DIRECTORY结构定义如下:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
1)Characteristics
这个值总为0.
2)TimeDateStamp
含有这个导出表的文件被生成的时间.
3)MajorVersion
4)MinorVersion
版本信息.总为0.
5)Name
含有这个导出表的pe文件的名字的RVA.
6)Base
输出函数序号的开始值.
7)NumberOfFunctions
数组 AddressOfFunctions 中元素的个数.这个值就是导出表中导出函数的个数.
8)NumberOfNames
以名字输出的函数的个数.
9)AddressOfFunctions
这是一个RVA,指向一个由函数地址组成的数组.每一个函数地址是本模块中的一个输出函数的入口地址.
10)AddressOfNames
这是一个RVA,指向一个由字符串指针组成的数组,每个字符串是本模块中以名字输出的输出函数的函数名.
11)AddressOfNameOrdinals
这是一个RVA,指向一个word类型的数组,该word类型数组是本模块中所有以名字输出的输出函数的输出序号.
假设一个dll有三个导出函数,分别如下:
序号 函数名
1 "myfun1"
2
3 "myfun2"
其中序号为2的函数只能通过序号导出.图示如下:
IMAGE_EXPORT_DIRECTORY 函数地址表
|---------------------------| |------->|------------------|
| Characteristics | | | 0x400042"myfun1" |
|---------------------------| | |------------------|
| ...... | | | 0x400085 |
|---------------------------| | |------------------|
| NumberOfFunctions = 3 | | | 0x400197"myfun2" |
|---------------------------| | |------------------|
| NumberOfNames = 2 | |
|---------------------------| | 函数名表
| AddressOfFunctions |--| |----->|------------|
|---------------------------| | | 0xXXXXXXXX |->"myfun1"
| AddressOfNames |----| |------------|
|---------------------------| | 0xXXXXXXXX |->"myfun2"
| AddressOfNameOrdinals |----| |------------|
|---------------------------| |
| 函数名称地址索引表
|----->|-----------|
| 1 |
|-----------|
| 3 |
|-----------|
我们来看一下pe加载程序的工作机制.假设它知道函数名"myfun2",那么加载程序将首先遍历函数名表,找到匹配的函数名"myfun2".由于"myfun2"在函数名表里的索引是2,所以加载函数将在函数名称地址索引表的第二个元素里取得函数在函数地址表里的索引3,然后加载程序就会在函数地址表的第三个元素里取得函数的入口地址0x400197.
这就是以名称导出函数的过程.
如果是以序号导出函数地址的,那将更简单.加载程序将直接用序号在函数地址表里取出函数的入口地址.可以看出,以序号导出函数比以名称导出函数快,但以序号导出函数地址会带来维护的问题.有些api函数在不同的系统上导出序号并不相同.所以微软不推荐使用序号来导出函数.
这里讲的是Base为1时的情形,如果Base域大于1,则在取得函数在函数地址表中的索引后,用这个索引值减去Base就可以得到函数在函数地址表中的偏移值.
pe知识学习(七)--完
这一贴介绍一下pe文件中的重定位表.
重定位的概念不难理解.简单的说,就是因为程序被连接后一些变量或者函数调用或跳转指令使用了绝对地址,当装载程序不能把pe映像装到预定的地址(ImageBase)时,那么这些绝对地址就需要调整.否则程序将访问到错误的地址.
exe文件一般不需要重定位,因为每个exe文件映像都有自己独立的地址空间,它总能被映射到预定的地址.而dll文件一般是映射到exe文件的地址空间的.当多个dll文件的预定地址发生冲突时,就不能保证会被映射到预定的地址了.所以dll文件一般都需要重定位的.
那么重定位是怎样实现的呢?
在pe文件里用这样一个结构来描述一个重定位数据项:
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
// WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;
1)VirtualAddress
这个域包含这个重定位数据项的起始RVA值,紧跟在结构后面的偏移值要加上这个值才是一个真正的需要重定位的数据的RVA值.
如果这个域为0,则代表一系列重定位数据项的结束.
2)SizeOfBlock
重定位数据项的大小.
3)TypeOffset[1]
这是一个WORD类型的数组.数组的元素个数由(SizeOfBlock - 8 ) / 2 得到.每个元素的低12位代表一个偏移值,该偏移值加上VirtualAddress就是需要修正的数据的RVA值.而高4位代表该偏移值的类型.该类型定义如下:
#define IMAGE_REL_BASED_ABSOLUTE 0
#define IMAGE_REL_BASED_HIGH 1
#define IMAGE_REL_BASED_LOW 2
#define IMAGE_REL_BASED_HIGHLOW 3
#define IMAGE_REL_BASED_HIGHADJ 4
#define IMAGE_REL_BASED_MIPS_JMPADDR 5
#define IMAGE_REL_BASED_SECTION 6
#define IMAGE_REL_BASED_REL32 7
#define IMAGE_REL_BASED_MIPS_JMPADDR16 9
#define IMAGE_REL_BASED_IA64_IMM64 9
#define IMAGE_REL_BASED_DIR64 10
#define IMAGE_REL_BASED_HIGH3ADJ 11
其中和intel的cpu有关的只有两种类型.其他的都用于i386以外的cpu.
0 (IMAGE_REL_BASED_ABSOLUTE):代表该偏移值无意义.只是为了使所有重定位数据项的大小位DWORD的整数倍.
3 (IMAGE_REL_BASED_HIGHLOW): 把该偏移值加上 VirtualAddress就是要修正的数据的RVA值.
由于WORD中只有低12位表示偏移值,因此一个重定位项只能修正一页的数据(4k).如果需要重定位的数据超过4k,那么一个pe文件里就有多个重定位项.
加载程序修正过程如下:假设IMAGE_OPTIONAL_HEADER.ImageBase的值为0x400000,而实际pe映像被加载的地址为0x500000,实际加载的地址比预定的高0x100000,那么需要修正的数据都会被加上0x100000.
这里所说的需要修正的数据是指前面提到的变量的绝对地址和调用或跳转指令里含有的绝对地址.
到这里pe知识学习就告一段落了.希望这些帖子能帮助你了解pe文件的大概知识.更细节的知识可以到msdn里查找.我的水平有限,错误之处还望你能够给予指正.我将不胜感激.