返回顶部
扩大
缩小

Zhang_derek

五、PE

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个字节。
image
<2>不要仅仅通过文件的后缀名来认定PE文件

5.2.PE文件的两种状态

1、PE文件主要结构体
image

  • 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文件的两种状态
image

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部分可以随便修改
    image

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文件属性
image
文件属性
Characteristics值为: 01 0F
转换为二进制:0000 0001 0000 1111
说明下标0,1,2,3,8有值,根据下标是不是1,然后查看对应的文件属性
image

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

image
通过DTDebug确认
image
3、 DllCharacteristics文件特性
image

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个字节
image

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函数的地址
image
构造自己的代码,找一段空白区,写上自己的代码

先执行我们要写的代码(弹出信息框),执行完,然后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

image
修改程序入口
image
把入口改成我们自己构造的代码的起始位置F90
image

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
image
修改VirtualSize和SizeOfRawData值
image
扩大节,添加1000h,也就是十进制4096字节。右键-->粘贴-->粘贴零字节-->4096
image
修改SizeOflmage的值,先内存对齐后再加1000
image
SizeOflmage结果为
image

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(内存中没有对齐的实际值)改为内存对齐后的值
    image
    改为8000
    image
    修改新增加节的VirtualSize和SizeOfRawData,因为新增加的节大小为1000h
    image
    新增加节的VirtualAddress = 上一个节内存对齐后的大小+上一个节.VirtualAddress
新增加节
VirtualAddress = 00008000+0002B000 = 00033000
PointerToRawData=VirtualAddress

image
修改sizeOflmage的大小
image
修改为34000
image
在原有数据的最后,新增一个节的数据,新增加节的大小为1000h

先删除第一个节前面的40个字节(因为前面新增加了一个节表,数据全部往后推移了40个字节)
image
在最后面添加1000h字节
image

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]
image
起始位置2AD80
image
Name:2ADBC (RVA),然后从2ADBC的位置开始找,到以0结尾,就是导出表的名字
image
NumberOfFunctions:导出函数的个数 2个
image
NumberOfNames:以函数名字导出的函数个数 2个
image
image
AddressOfFunctions:导出函数地址表RVA
image
AddressOfNames:导出函数名称表RVA
image
AddressOfNameOrdinals:导出函数序号表RVA。序号是两个字节,序号的个数跟函数名称的个数相同

这里序号为0和1
image
4、参考

  • 总共四个函数
  • 所有导出函数的个数为5,因为序号中间隔了个14没有。函数个数 = 最大序号 - 最小序号 + 1
  • 以函数名导出的函数个数为3,因为有一个函数没有名字
  • 把函数地址对应的二进制复制到OD里面,可以查看到具体是什么函数
    image

5.12.导入表_确定依赖模块

1、定位导入表

导入表位置,数组DataDirectory[1]

第一个导入表开始的位置:22A10
image
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为结尾的位置
image
4、查看依赖的模块名

第一个模块名字
image
查看
image

5.13.导入表_确定依赖函数

1、确定需要导入的函数
image
第一个成员指向的是一张表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
image
INT表
image
3、确定需要导入的函数的名字
image
确定函数名字为ExitThread

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;		//可能为空,编译器决定,如果不为空,是函数在导出表中的索引
    BYTE    Name[1];	//函数名称,以0结尾
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

image

5.14.导入表_确定函数地址

PE文件加载前
image
PE文件加载后
image

5.15.重定位表

重定位表的位置(第六个表)

导入表位置,数组DataDirectory[5]

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;     
    DWORD   SizeOfBlock;        
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

posted on 2021-04-18 18:42  zhang_derek  阅读(484)  评论(0编辑  收藏  举报

导航