作业正好写利用010分析PE文件,写一个博客复盘一下。
正在努力学习的小白,可能会有错误希望大佬们可以指出来(保命)。
参考资料:
https://cloud.tencent.com/developer/article/1910254
https://www.cnblogs.com/bonelee/p/17388067.html
利用到的工具
Masm32,010editor
一、PE文件概念
1.PE文件是什么?
PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)
PE文件是指32位可执行文件,也称为PE32。64位的可执行文件称为PE+或PE32+,是PE(PE32)的一种扩展形式。
2.PE的文件结构
PE加载到内存后的映射
image内存展开
(4)PE文件加载
Windows 加载器在加载PE文件至内存的过程如下
1.先读入PE文件DOS头、PE头、Section头
2.判断PE头ImageBase所存储的加载地址是否可用,如果已被占用,则重新分配空间
3.根据Section头部信息,把文件各个Section映射至系统分配的空间,并根据各个Section定义的数据来修改所映射的页属性
4.如果文件加载地址不是ImageBase所定义的地址,则重新修改ImageBase值
5.根据PE文件的输入表加载所需的DLL至进程空间
6.替换IAT表中的数据为实际调用函数地址
7.根据PE头生成初始化堆栈
8.创建初始化线程,开始运行PE文件进程
二、准备工作
下面利用实例来分析一下PE文件
首先利用masm32编写了一个非常简单的win32汇编程序
代码如下:
.386
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
includelib C:\masm32\lib\user32.lib
include kernel32.inc
includelib kernel32.lib
.data
szText db 'Hello World! Welcome to School.',0ah,0dh,0ah,0dh
szTText db 'HAHA',0
szOption db 'summer',0ah,0dh
.code
start:
invoke MessageBox,NULL,offset szText,offset szOption,MB_OK
invoke ExitProcess,NULL
end start
在cmd下生成链接:
ml /c /coff 1.asm
link /subsystem:windows 1.obj
运行程序:
三、010 PE文件分析
使用010Editor工具打开PE文件运行模板
DOS头
在之前我们已经了解了PE文件的整体结构了,并且我们进行了静动态差异的文件分析,其开头部分就是DOS部分,包含了DOS MZ文件头和DOS块,那么我们来了解一些DOS部分的结构和其相关意义。
Windows PE 文件DOS头包含64B,其目的是兼容早期的DOS操作系统。
(1)MZ文件头
该部分固定大小为40H个字节。
这个结构体作用是给16位平台看的,而我们现在的环境大部分都是32位和64位的,所以现在的平台不再需要这个完整的结构体了,只需要其中的两个成员MZSignature(4D5A是一种标识)和AddressOfNewExeHeader(指示“NT映象头的偏移地址”,其中000000B0是NT映象头的文件偏移地址,定位PE文件头开始位置,用于PE文件合法性检验。)。
000000B0指向PE文件开头
利用010进行修改,除了MZSignature和AddressOfNewExeHeader都变为0
(2)DOS块(DOS stub)
DOS块就是夹在DOS MZ文件头和PE文件头之间的内容
DOS Stub 紧接着DOS头后,是链接器链接执行文件时加入的数据,通常为“This program cannot be run in DOS mode”。出现这句话是因为没有在DOS系统运行等原因,随便修改这个地方不会影响程序的运行。
PE头
PE整体的头包括以下内容
DWORD Signature //PE标识
struct IMAGE_FILE_HEADER FileHeader //标准PE头
struct IMAGE_OPTIONAL_HEADER32 OptionalHeader //扩展PE头
(1)Signature //PE标识
字串“PE\0\0”,改标识不能够被破坏。
(2)IMAGE_FILE_HEADER FileHeader //标准PE头
WORD Machine; // 可以运行在什么样的CPU上,如果它的值为0x0则表示可以运行在任意的CPU上,支持在Intel 386以及后续的型号CPU运行则值为0x14c,支持64位的CPU型号则值为0x8664。
WORD NumberOfSections; // 表示节的数量
DWORD TimeDateStamp; // 编译器编译的时候插入的时间戳,可以进行修改,与文件属性里面的创建时间和修改时间是无关的
DWORD PointerToSymbolTable; // 调试相关
DWORD NumberOfSymbols; // 调试相关
WORD SizeOfOptionalHeader; // 扩展PE头的大小
WORD Characteristics; // 文件属性
根据上面的图可以了解到,这个文件是在Intel 386以及后续的型号CPU上运行,有3个节表,创建时间是2022年9月19日 2:26:45,扩展PE头大小是224。
struct FILE_CHARACTERISTICS Characteristics里面的属性如下图所示
(3)IMAGE_OPTIONAL_HEADER32 OptionalHeader //扩展PE头
分析的文件是32位,结构体如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; //
Magic表示当前PE文件是32位还是64位,32位时该值对应0x10B,64位时该值对应0x20B
BYTE MajorLinkerVersion;
// 链接器版本号
BYTE MinorLinkerVersion;
// 链接器版本号
DWORD SizeOfCode;
// 所有代码节的总和(文件对齐后的大小),编译器填的(没用)
DWORD SizeOfInitializedData;
// 包含所有已经初始化数据的节的总大小(文件对齐后的大小),编译器填的(没用)
DWORD SizeOfUninitializedData;
// 包含未初始化数据的节的总大小(文件对齐后的大小),编译器填的(没用)
DWORD AddressOfEntryPoint;
// 程序入口
DWORD BaseOfCode;
// 代码开始的基址,编译器填的(没用)
DWORD BaseOfData;
// 数据开始的基址,编译器填的(没用)
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文件的映射的尺寸
DWORD SizeOfHeaders;
// 所有头加节表按照文件对齐后的大小,否则加载会出错
DWORD CheckSum;
// 校验和
WORD Subsystem;
// 子系统,驱动程序(1)、图形界面(2) 、控制台/DLL(3)
WORD DllCharacteristics;
// 文件特性
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;
成员DWORD AddressOfEntryPoint表示当前程序入口地址(红色)900h
成员DWORD ImageBase表示示内存镜像基址 (蓝色)地址为400000h
打开ollydbg最终程序的入口地址为400900h
节表
在PE中,节数据有几个,分别对应着什么类型以及其他相关的属性都是由PE节表来决定的,PE节表是一个结构体数组,结构体的定义如下所示
#define IMAGE_SIZEOF_SHORT_NAME
8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
// ASCII字符串(节名),可自定义,只截取8个字节,可以8个字节都是名字
union {
// Misc,双字,是该节在没有对齐前的真实尺寸,该值可以不准确
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;
DWORD PointerToRawData
节区在文件中的偏移如下图所示其中.text(红色).rdata(黑色).data(绿色)
偏移地址与框内的三个节的开始的位置相同
3个节
struct IMAGE_SECTION_DATA Section[0] 400H-5ffH(.text):代码节
struct IMAGE_SECTION_DATA Section[1] 600H-7ffH(.rdata):引入函数节
struct IMAGE_SECTION_DATA Section[2] 800H-9ffH(.data):数据节
代码节
Opcode Data[512]:操作码,本次用的是win32汇编语言编写
引入函数节
UCHAR Data[512]:字节型
与程序中的这部分对应
数据节
UCHAR Data[512]:字节类型
与程序中这部分对应
在010中可以对这部分的内容进行更改
导入表 IMPORT_DESCRIPTOR
与.rdata相对应
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // 包含指向IMAGE_DATA(输入名称表)RVA 的结构数组 }; DWORD TimeDateStamp; //当可执行文件不与被导入的DLL进行绑定时,此字段为0 DWORD ForwarderChain; //第一个被转向的API索引 DWORD Name; //指向被导入的DLL 名称 DWORD FirstThunk; //指向输入地址表(IAT)RVA,IAT是一个IMAGE_THUNK_DATA结构的数组 } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR
到这里就分析完我所编写简单的PE文件