5.1.PE文件结构
1、什么是可执行文件?
可执行文件(executable fle)指的是可以由操作系统进行加载执行的文件。
可执行文件的格式:
Windows平台:
PE(Portable Executable)文件结构
Linux平台:
ELF(Executable and Linking Format)文件结构
哪些领域会用到PE文件格式:
<1>病毒与反病毒
<2>外挂与反外挂
<3>加壳与脱壳(保护与破解)
<4>无源码修改功能、软件汉化等
2、如何识别PE文件
<1> PE文件的特征(PE指纹)
分别打开.exe .dlI .sys 等文件,观察特征前2个字节。
<2>不要仅仅通过文件的后缀名来认定PE文件
5.2.PE文件的两种状态
1、PE文件主要结构体
- IMAGE_DOS_HEADER占64个字节。
- DOS Sub:IMAGE_DOS_HEADER尾部的四个字节指向PE文件的开始位置。IMAGE_DOS_HEADER尾部到PE文件头开始的中间部分是DOS_Sub部分(大小不固定)
- PE文件头标志:PE头是前面4个字节
- PE文件表头:IMAGE_FILE_HEADER是20个字节
- 扩展PE头:IMAGE_OPTIONAL_HEADER在32位中占224个字节(这个大小是可以修改的)
- IMAGE_SECTION_HEADER:40个字节
2、PE文件的两种状态
5.3.DOS头属性说明
IMAGE_DOS_HEADER结构体
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
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
主要就看两个成员
WORD e_magic; //PE文件判断表示 4D5A,ascii是MZ
LONG e_lfanew; //存储PE头首地址
- e_magic两个字节和e_lfanew四个字节内容不能修改
- 开头e_magic和结尾e_lfanew中间的成员部分可以随意修改
- e_lfanew到PE头文件中间的DOS Stub部分可以随便修改
5.4.标志PE头属性说明
1、PE头
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature; //PE标识,占4字节
IMAGE_FILE_HEADER FileHeader; //标志PE头
IMAGE_OPTIONAL_HEADER64 OptionalHeader; //扩展PE头
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
PE标识不能破坏,操作系统在启动一个程序的时候会检测这个标识。
2、标准PE头(占20字节)
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;//可以运行在什么样的CPU上 任意:0 Intel 386以及后续:14C x64:8664
WORD NumberOfSections;//表示节的数量
DWORD TimeDateStamp;//编译器填写的时间戳 与文件属性里面(创建时间、修改时间)无关
DWORD PointerToSymbolTable;//调试相关
DWORD NumberOfSymbols;//调试相关
WORD SizeOfOptionalHeader;//可选PE头的大小(32位PE文件:0xE0 64位PE文件:0xF0)
WORD Characteristics;//文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Characteristics文件属性
文件属性
Characteristics值为: 01 0F
转换为二进制:0000 0001 0000 1111
说明下标0,1,2,3,8有值,根据下标是不是1,然后查看对应的文件属性
5.5.扩展PE头属性说明
1、扩展PE头结构体(总共224字节)
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic; // 分辨32位程序还是64位,如果32位则10B,64位则20B
BYTE MajorLinkerVersion; //链接器版本号
BYTE MinorLinkerVersion; //链接器版本号
DWORD SizeOfCode; //所有代码节的总和 文件对齐后的大小 编译器填写的,无用处
DWORD SizeOfInitializedData; //已经初始化数据的节的总大小 文件对齐后的大小 编译器填写的,无用处
DWORD SizeOfUninitializedData; // 未初始化数据的节的总大小 文件对齐后的大小 编译器填写的,无用处
DWORD AddressOfEntryPoint; // 程序入口
DWORD BaseOfCode; //代码开始的基址 编译器填写的,无用处
DWORD BaseOfData; //数据开始的基址 编译器填写的,无用处
//
// NT additional fields.
//
DWORD ImageBase; //内存镜像基址
DWORD SectionAlignment; //内存对齐
DWORD FileAlignment; //文件对齐
WORD MajorOperatingSystemVersion; //操作系统版本号
WORD MinorOperatingSystemVersion; //操作系统版本号
WORD MajorImageVersion; //PE文件自身的版本号
WORD MinorImageVersion; //PE文件自身的版本号
WORD MajorSubsystemVersion; //运行所需要子系统的版本号
WORD MinorSubsystemVersion; //运行所需要子系统的版本号
DWORD Win32VersionValue; //子系统版本的值,必须为0
DWORD SizeOfImage; //内存中整个PE文件的映射尺寸,比实际的值大,必须是SectionAlignment整数倍
DWORD SizeOfHeaders; //所有的头+节表按照文件对齐后的大小
DWORD CheckSum; //校验和,可伪造
WORD Subsystem; //子系统, 驱动程序(1) 图形界面(2) DLL(3)
WORD DllCharacteristics; //文件特性 不是针对DLL文件的
DWORD SizeOfStackReserve; //初始化保留的栈的大小
DWORD SizeOfStackCommit; //初始化实际提交的大小
DWORD SizeOfHeapReserve; //初始化保留的堆的大小
DWORD SizeOfHeapCommit; //初始化实际提交的大小
DWORD LoaderFlags; //调试相关
DWORD NumberOfRvaAndSizes; //目录项数目
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数组,
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
2、ImageBase和AddressOfEntryPoint
ImageBase; //内存镜像基址
AddressOfEntryPoint; // 程序入口,相对于ImageBase的偏移
实例
程序入口:0193BE
内存镜像:400000
程序真正入口=内存镜像+程序入口=4193BE
通过DTDebug确认
3、 DllCharacteristics文件特性
5.6.PE节表
节表结构体(占40字节)
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //ASCII字符串 可自定义 只截取8个字节(占8字节)
union { //Misc双子是该字节没有在对齐前的真实尺寸 该值可以不准确(占4字节)
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; //在内存中的偏移地址加上ImageBase才是内存中的真正地址
DWORD SizeOfRawData; //节在文件中对齐后的尺寸
DWORD PointerToRawData; //节区在文件中的偏移
DWORD PointerToRelocations; //调试相关
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; //节的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
#define IMAGE_SIZEOF_SECTION_HEADER 40
DOS头64字节+PE标识4字节+PE标准头20字节+PE扩展头224字节,然后就是节表的起始位置,每个节表占40个字节
5.7.RVA与FOA的转换
1、RVA(相对虚拟地址)到FOA(文件偏移地址)的转换:
<1>得到RVA的值:内存地址- ImageBase
<2>判断RVA是否位于PE头中,如果是: FOA== RVA
<3>判断RVA位于哪个节:
RVA>=节VirtualAddress
RVA <=节.VirtualAddress +当前节内存对齐后的大小
差值= RVA-节VirtualAddress;
<4> FOA=节.PointerToRawData +差值;
如果文件对齐和内存对齐的值一样,则RVA=内存地址- ImageBase,F0A=RVA,就可以得出在文件中的地址
5.8.空白区添加代码
给程序添加一个MessageBox对话框,步骤
- 在PE的空白区构造一段代码
- 修改入口地址为新增代码的地址
- 新增代码执行后,跳回到入口地址
1、MessageBox的反汇编硬编码
E8 表示call
6A表示push
9: MessageBox(0,0,0,0);
00401028 8B F4 mov esi,esp
0040102A 6A 00 push 0
0040102C 6A 00 push 0
0040102E 6A 00 push 0
00401030 6A 00 push 0
00401032 FF 15 8C 42 42 00 call dword ptr [__imp__MessageBoxA@16 (0042428c)]
00401038 3B F4 cmp esi,esp
0040103A E8 31 00 00 00 call __chkesp (00401070)
2、找到要运行的程序的MessageBoxA的地址
用DTDdbug打开程序,点“E”,找到“USER32.DLL”,按“Ctrl+n”,然后找到MessageBoxA函数的地址
构造自己的代码,找一段空白区,写上自己的代码
先执行我们要写的代码(弹出信息框),执行完,然后jmp到程序入口位置
构造要写入的代码
6A 00 6A 00 6A 00 6A 00 E8 00 00 00 00 E9 00 00 00 00
E8表示call
E8后面的硬编码 = 要跳转的地址 - E8指令当前的地址 - 5
要跳转的MessageBoxA的地址:77D5050B
E8后面的硬编码 = 77D5050B - (ImageBase+F98)- 5 = 7794F56E
程序入口:000193BE
ImageBase:00400000
程序运行入口=ImageBase+程序入口=004193BE
E9后面的硬编码 = 004193BE - 400F9D - 5 = 1841C
最终代码
6A 00 6A 00 6A 00 6A 00 E8 6E F5 94 77 E9 1C 84 01 00
修改程序入口
把入口改成我们自己构造的代码的起始位置F90
5.9.扩大节
1、为什么要扩大节
我们可以在任意空白区添加自己的代码,但如果添加的代码比较多,空白区不够怎么办?
2、扩大节的步骤
<1>分配一块新的空间,大小为S
<2>将最后-一个节的SizeOfRawData和VirtualSize改成N
N = (SizeOfRawData或者VirtualSize内存对齐后的值)+ S
<3>修改SizeOflmage大小
S = 1000
VirtualSize:78B0 当前节内存中没有对齐的实际大小
SizeOfRawData:8000 当前节文件对齐后的大小
N = 8000 + 1000 = 9000
修改VirtualSize和SizeOfRawData值
扩大节,添加1000h,也就是十进制4096字节。右键-->粘贴-->粘贴零字节-->4096
修改SizeOflmage的值,先内存对齐后再加1000
SizeOflmage结果为
5.10.新增节
1、新增节的步骤:
<1>判断是否有足够的空间,可以添加一个节表.
<2>在节表中新增一个成员.
<3>修改PE头中节的数量.
<4>修改sizeOflmage的大小.
<5>在原有数据的最后,新增一个节的数据(内存对齐的整数倍).
<6>修正新增节表的属性.
2、节表结构
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //ASCII字符串 可自定义 只截取8个字节(占8字节)
union { //Misc双子是该字节没有在对齐前的真实尺寸 该值可以不准确(占4字节)
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; //在内存中的偏移地址加上ImageBase才是内存中的真正地址
DWORD SizeOfRawData; //节在文件中对齐后的尺寸
DWORD PointerToRawData; //节区在文件中的偏移
DWORD PointerToRelocations; //调试相关
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; //节的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
#define IMAGE_SIZEOF_SECTION_HEADER 40
在节表中新增一个节,把.txet节的40个字节复制粘贴到新增加的节,然后修改新增加节的成员属性
- 前8个字节是节的名字:随便改个名字
- 把之前最后一个节的VirtualSize(内存中没有对齐的实际值)改为内存对齐后的值
改为8000
修改新增加节的VirtualSize和SizeOfRawData,因为新增加的节大小为1000h
新增加节的VirtualAddress = 上一个节内存对齐后的大小+上一个节.VirtualAddress
新增加节
VirtualAddress = 00008000+0002B000 = 00033000
PointerToRawData=VirtualAddress
修改sizeOflmage的大小
修改为34000
在原有数据的最后,新增一个节的数据,新增加节的大小为1000h
先删除第一个节前面的40个字节(因为前面新增加了一个节表,数据全部往后推移了40个字节)
在最后面添加1000h字节
5.11.导出表
1、如何查找导出表
扩展PE头最后一个成员是一个数组(包含16和元素),每个数组对应一个表(每个表占8字节),如导出表、导入表等。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //表的起始位置RVA
DWORD Size; //表的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
2、导出表结构
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 导出函数地址表RVA
DWORD AddressOfNames; // RVA from base of image 导出函数名称表RVA
DWORD AddressOfNameOrdinals; // RVA from base of image 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
3、导出表成员 40字节
导出表位置,数组DataDirectory[0]
起始位置2AD80
Name:2ADBC (RVA),然后从2ADBC的位置开始找,到以0结尾,就是导出表的名字
NumberOfFunctions:导出函数的个数 2个
NumberOfNames:以函数名字导出的函数个数 2个
AddressOfFunctions:导出函数地址表RVA
AddressOfNames:导出函数名称表RVA
AddressOfNameOrdinals:导出函数序号表RVA。序号是两个字节,序号的个数跟函数名称的个数相同
这里序号为0和1
4、参考
- 总共四个函数
- 所有导出函数的个数为5,因为序号中间隔了个14没有。函数个数 = 最大序号 - 最小序号 + 1
- 以函数名导出的函数个数为3,因为有一个函数没有名字
- 把函数地址对应的二进制复制到OD里面,可以查看到具体是什么函数
5.12.导入表_确定依赖模块
1、定位导入表
导入表位置,数组DataDirectory[1]
第一个导入表开始的位置:22A10
2、导入表结构
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA指向IMAGE_THUNK_DATA结构数组
};
DWORD TimeDateStamp; // 时间戳
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; //RVA 指向dll名字,该名字以0结尾
DWORD FirstThunk; // RVA 指向IMAGE_THUNK_DATA结构数组
} IMAGE_IMPORT_DESCRIPTOR;
3、导入表个数
导入表的个数判断:,每个导入表占20个字节,判断有多少个导入表,以20个0为结尾的位置
4、查看依赖的模块名
第一个模块名字
查看
5.13.导入表_确定依赖函数
1、确定需要导入的函数
第一个成员指向的是一张表INT(导入名称表),INT表里面每个成员都是结构体IMAGE_THUNK_DATA,大小是4个字节
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
2、INT表里面的结构体
INT表位置22A88,INT表里面有多少个成员(4个字节),就说明依赖当前导入模块多少个函数。结尾标志:四个字节都是00
INT表
3、确定需要导入的函数的名字
确定函数名字为ExitThread
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //可能为空,编译器决定,如果不为空,是函数在导出表中的索引
BYTE Name[1]; //函数名称,以0结尾
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
5.14.导入表_确定函数地址
PE文件加载前
PE文件加载后
5.15.重定位表
重定位表的位置(第六个表)
导入表位置,数组DataDirectory[5]
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;