PE学习笔记
有一说一,自己看完书在看看视频,可以自己写一两个程序(比如输出hello world)来实战分析一下,印象会更深。
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; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;
|
PE标识
0004550h=======”PE00”
1 2 3 4 5 6 7 8 9
| typedef struct__IMAGE_FILE_HEADER { WORD Machine; wORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOf0ptionalHeader; WORD Characteristics; }IMAGE_FILE_HEADER,*PIMAGE_FILE_HEADER;
|
C:\Windows下两个文件夹
System32存储64位程序,SysWOW64存储32位程序
Characteristics 将他的字节转存二进制
比如
此时是左小右大,小端存储要修改位置
0127
转成二进制
0000 0001 0010 0111
节表数据结构
节属性说明
RVA和FOA的转换
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
|
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
|
按E 找到user 32 按 CTRL+N 找到MessageBox
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有导入表,不提供导出表(不提供函数给别人用(不是不能))
位置:
16个结构体,类型都一样 +96个字节(6行)16个数组128个字节
导出表在哪里,导出表有多大
导入表在哪里,导入表有多大
找到的是RVA要转FOA
导出表计算方法计算方法
1、找文件对齐
找到导出表的RVA为E260h 大小为A3h
接着去节表看看再哪个范围内
发现第一个节包含1000h-E303h
包含了导出表
文件偏移(磁盘文件的位置)=数据目录导出表地址(任意RVA)-该区段相对虚拟地址(RVA)+该区段的文件偏移地址(offset)
ep:E260h - 1000h + 400h = D660h
他的大小是包含子表的,不止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个字节一个成员
名称表有几个序号表就有几个
第一参数是dll模块句柄,就是PE文件再内存中展开的起始位置
第二个可以写函数名或序号
函数名称 -》 序号表- 》函数地址
寻找过程
序号来找:
eg:
15 -> 15 - base >函数地址里找
导入表
文件对齐是否与内存对齐一样,不一样请将RVA转为FOA
有很多个导入表,每个导入表20个字节,一直数到连续20个0结束
INT 导入名称表
IAT 导入地址表
重定位表
重定位表在第6个
每个重定位表有多大取决于SizeOdBlock有多大
直到连续8个字节为0结束
每2个字节代表一个地址
高位前4位无效 只需要12位
只有高位为0011 = 3 才需要将VirtualAddress+低12位
高位不是3是用来填位置的没用的数据,存在的价值是为了内存对齐
所以修复重定位表的之后需要判断高位是否为3