PE学习笔记

有一说一,自己看完书在看看视频,可以自己写一两个程序(比如输出hello world)来实战分析一下,印象会更深。

image-20210704165647109

image-20210324212714865

文件图

DOS属性说明

MZ标识(e_magic)不能动,+3C位置的e_lfanew不能动,中间的都可以改动,中间的是给16位看的,现在是32/64位的系统,不需要这部分。同时DOS Stub可以随意更改不影响。

PE头属性说明

1
2
3
4
5
typedef struct_IMAGE_NT_HEADERS {
DWORD Signature; //PE标识(4字节
IMAGE_FILE_HEADER FileHeader;//标准PE头(20字节
IMAGE_OPTIONAL_HEADER32 OptionalHeader;//扩展PE头(224字节
} IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;

PE标识

0004550h=======”PE00”

IMAGE_FILE_HEADER FileHeader;标准PE头

1
2
3
4
5
6
7
8
9
typedef struct__IMAGE_FILE_HEADER {
WORD Machine; //可以运行在什么样的CPU上任意:0 Intel 386以及后续:14C x64:8664
wORD NumberOfSections; //表示节的数量
DWORD TimeDateStamp; //编译器填写的时间戳与文件属性里面(创建时间、修改时间)无关(1970年到编译器编译段程序
DWORD PointerToSymbolTable; //调试相关
DWORD NumberOfSymbols; //调试相关
WORD SizeOf0ptionalHeader; //可选PE头的大小(32位PE文件:OxE0 64位PE文件:OxF0)
WORD Characteristics; //文件属性
}IMAGE_FILE_HEADER,*PIMAGE_FILE_HEADER;

C:\Windows下两个文件夹

System32存储64位程序,SysWOW64存储32位程序

Characteristics 将他的字节转存二进制

比如image-20210324215838182

此时是左小右大,小端存储要修改位置

0127

转成二进制

0000 0001 0010 0111

image-20210325125852801

IMAGE_OPTIONAL_HEADER32 OptionalHeader;扩展PE头

image-20210325170912232

image-20210325171126065

节表数据结构

image-20210325190344706

节属性说明

image-20210325190357790

RVA和FOA的转换

image-20210325190859385

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
VA:虚拟内存地址(Virtual Address)PE 文件被操作系统加载进内存后的地址。
RVA:PE文件的相对虚拟地址(Relative Virual Address)是PE文件中的数据、模块等运行在内存中的实际地址相对PE文件装载到内存的基址之间的距离。举例说明,如果PE文件装入虚拟地址(VA)空间的400000h处,且进程从虚址401000h开始执行,我们可以说进程执行起始地址在RVA 1000h。
FOA:文件偏移地址(File Offset Address),和内存无关,它是指某个位置距离文件头的偏移。

//学习链接:https://blog.csdn.net/weixin_43742894/article/details/105235629

RVA到FOA的转换:
<1> 得到RVA的值:内存地址-lmageBase
<2> 判断RVA是否位于PE头中,如果是:FOA== RVA
<3> 判断RVA位于哪个节:
RVA>=节.VirtualAddress && RVA<=节.VirtualAddress +当前节内存对齐后的大小
差值 = RVA - 节.VirtualAddress;
<4> FOA = 节.PointerToRawData + 差值;

eg:
DWORD SectionAlignment 1000h 128h 4h Fg: Bg:0xFFE0FF
DWORD FileAlignment 200h 12Ch 4h Fg: Bg:0xFFE0FF
DWORD SizeOfHeaders 400h 144h 4h Fg: Bg:0xFFE0FF
DWORD VirtualAddress E260h 168h 4h Fg: Bg:0xFFE0FF .text FOA = 0xD660

image-20210720202153131

E260h <= 1000h+D400h

RVA<=节.VirtualAddress +当前节内存对齐后的大小

E260h - 1000h = D260h

FOA = D260h +400h = D660h

空白区域添加代码

弹出错误弹窗例子

1
2
3
4
5
6
7
8
9
10
11
MessageBox(0,0,0,0); 直接用他的是需要导入表 E8 call不需要导入表
用硬编码
我们要用E8 直接call
<0> 构造要写入的代码
<1> 在PE的空白区构造一段代码
<2>
<3>
6A 00 6A 00 6A 00 6A 00 E8(call) 00 00 00 00 E9(jmp) 00 00 00 00 (后面的计算方法为要跳转的地址)
要跳转的地址-E8指令当前的指令当前的地址-5h(16进制算的)
要跳转的地址-E9指令当前的指令当前的地址-5h(16进制算的)
//执行完还要跳回去所以需要jmp

image-20210326132850887

按E 找到user 32 按 CTRL+N 找到MessageBoximage-20210326132954921

image-20210326133713691

1
2
3
4
5
6
7
8
9
10
11
12
MessageBox: 76E1FD1E
ImageBase: 400000
MessageBox - (E8指令当前的指令当前文件的地址+ImageBase) -5
76E1FD1E - 400F98 - 5 = 76A1ED81
E8(call) 00 00 00 00
E8(call) 81 ED A1 76
入口: 183D7 (4183D7)
4183D7 - 400F9D -5 = 17435
E9(jmp) 00 00 00 00
E9(jmp) 35 74 01 00
6A 00 6A 00 6A 00 6A 00 E8 81 ED A1 76 E9 35 74 01 00
去找到程序的入口改成90 0f 00 00

扩大节

1
2
3
4
5
<1>分配一块新的空间,大小为S
<2>将最后一个节的SizeOfRawData和VirtualSize改成N
N = (SizeOfRawData或者VirtualSize)
内存对齐后的值
<3>

新增节

1
2
3
4
5
<1>判断是否有足够的空间,可以添加一个节表.
<2>在节表中新增一个成员.
<3>修改PE头中节的数量.<4>修改sizeOflmage的大小.
<5>再原有数据的最后,新增一个节的数据(内存对齐的整数倍).
<6>修正新增节表的属性

合并节

1
2
3
4
5
6
7
<1>按照内存对齐展开
<2>将第一个节的内存大小、文件大小改成一样
Max = SizeOfRawData>VirtualSize?SizeOfRawData:VirtualSize
SizeOfRawData = VirtualSize =
最后一个节的VirtualAddress + Max - SizeOfHeaders内存对齐后的大小.
<3>将第一个节的属性改为包含所有节的属性.
<4>修改节的数量为1.

学习网站:https://cloud.tencent.com/developer/article/1432558

导出表

表示当前PE文件提供了哪些函数

通常情况下exe有导入表,不提供导出表(不提供函数给别人用(不是不能))

位置:

image-20210329210408067

16个结构体,类型都一样 +96个字节(6行)16个数组128个字节

导出表在哪里,导出表有多大

image-20210329210457617

导入表在哪里,导入表有多大

image-20210329210556827

找到的是RVA要转FOA

导出表计算方法计算方法

1、找文件对齐

image-20210401152750440

找到导出表的RVA为E260h 大小为A3h

image-20210401152832938

接着去节表看看再哪个范围内

image-20210401153359965

发现第一个节包含1000h-E303h

包含了导出表

文件偏移(磁盘文件的位置)=数据目录导出表地址(任意RVA)-该区段相对虚拟地址(RVA)+该区段的文件偏移地址(offset)

ep:E260h - 1000h + 400h = D660h

image-20210329212133637

他的大小是包含子表的,不止40h

name 是指针

最后三个address也都是指针

1
2
3
4
5
6
7
8
9
10
11
12
13
struct IMAGE_EXPORT_DIRECTORY ExportDir		D660h	A3h	Fg: Bg:	TWAIN_32.dll
DWORD Characteristics 0 D660h 4h Fg: Bg:
time_t TimeDateStamp 06/14/2032 19:14:36 D664h 4h Fg: Bg:
WORD MajorVersion 0 D668h 2h Fg: Bg:
WORD MinorVersion 0 D66Ah 2h Fg: Bg:
DWORD Name E2BEh D66Ch 4h Fg: Bg: .text FOA = 0xD6BE -> TWAIN_32.dll
DWORD Base 1 D670h 4h Fg: Bg:
DWORD NumberOfFunctions 6 D674h 4h Fg: Bg:
DWORD NumberOfNames 5 D678h 4h Fg: Bg:
DWORD AddressOfFunctions E288h D67Ch 4h Fg: Bg: .text FOA = 0xD688
DWORD AddressOfNames E2A0h D680h 4h Fg: Bg: .text FOA = 0xD6A0
DWORD AddressOfNameOrdinals E2B4h D684h 4h Fg: Bg: .text FOA = 0xD6B4

看第一个子表 函数地址表 每四个字节一个成员

可以直接将函数对应的16进制放到OD里看汇编解代码

看第二个子表 函数名称表 每四个字节一个成员

是按照首字母顺序排序的

看第三个子表 函数序号表 每2个字节一个成员

名称表有几个序号表就有几个

image-20210401171410292

第一参数是dll模块句柄,就是PE文件再内存中展开的起始位置

第二个可以写函数名或序号

函数名称 -》 序号表- 》函数地址

寻找过程

image-20210401172236139

序号来找:

eg:

15 -> 15 - base >函数地址里找

导入表

image-20210329210556827

文件对齐是否与内存对齐一样,不一样请将RVA转为FOA

image-20210401184925678

有很多个导入表,每个导入表20个字节,一直数到连续20个0结束

image-20210401192412447

INT 导入名称表

image-20210721142911639

IAT 导入地址表

image-20210721142934755

image-20210721144543602

image-20210721144643766

重定位表

重定位表在第6

image-20210721151032756

每个重定位表有多大取决于SizeOdBlock有多大

直到连续8个字节为0结束

每2个字节代表一个地址

高位前4位无效 只需要12位

只有高位为0011 = 3 才需要将VirtualAddress+低12位

高位不是3是用来填位置的没用的数据,存在的价值是为了内存对齐

所以修复重定位表的之后需要判断高位是否为3