IMAGE_OPTIONAL_HEADER
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; // 判断32位0x10b,64位0x20b
BYTE MajorLinkerVersion; // 主链接器版本号,仅供参考
BYTE MinorLinkerVersion; // 副链接器版本号,仅供参考
//系统分配内存不看着3个值,对于调试器有影响
DWORD SizeOfCode; // 代码所占空间大小,仅供参考,OD反汇编代码,通过这个字段获得内存
// SizeOfCode可以尝试出极限值,让OD调试可以使用,但是会卡很久
// OS是从节表里算的代码段的大小
DWORD SizeOfInitializedData; // 已初始化数据所占空间大小,仅供参考,微软没使用
DWORD SizeOfUninitializedData;// 未初始化数据所占空间大小,仅供参考,微软没使用
DWORD AddressOfEntryPoint; // ***** oep:原本的程序入口
//这个值可以修改,但是修改过后必须跳转到在该偏移处跳转到真正入口
DWORD BaseOfCode; // 代码基址 (无用)
DWORD BaseOfData; // 数据基址 (无用)
DWORD ImageBase; // ****** 建议模块地址:exe映射加载到内存中的首地址 = PE 0处,即实例句柄hInstance
// 一般而言,exe文件可遵从装载地址建议,但dll文件无法满足 (开了随机基址可能也不是这个值,是通过重定位表得到)
//这个值最好不要改,改的话要改动大量地方,因为函数和全局变量的地址也需要跟着改变
DWORD SectionAlignment; //****** 内存对齐值,数据在内存的对齐值,很多内存地址,大小都要依赖他来计算
// OS要求是对齐值是0x1000的倍数 ,否则拒绝运行(从win7开始)
//默认1000h 一个分页大小,系统管理内存是以分页为单位
DWORD FileAlignment; //****** 文件对齐值, 0x200 => 512, 磁盘的一个扇区大小 (vc6是1000h)
// OS要求是对齐值是0x200的倍数,否则拒绝运行(从win7开始)
// 对齐值都是2的倍数 如果把所有节合并了 就可以设为1,否则不可以随便修改因为要配合节检查
//主副系统相关版本号 除了 MajorSubsystemVersion 不可修改,其他5个可以
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion; //主子系统版本号 不可以修改 这里改成4可以在xp运行,系统检查这个
WORD MinorSubsystemVersion;
/* VS编译的软件在低版本系统中无法运行,实际上是把这个版本号写入到了PE格式里了,版本号不对就没法运行 */
DWORD Win32VersionValue; // win32版本值 xp上不可以改 ,win7和win10可以修改
//改值是通过节表计算得到的
DWORD SizeOfImage; //****** 表示PE文件都加载到内存中需要多大的内存,与 SectionAlignment 对齐,改的话不可以改变分页数量(但最好对齐)
// 系统先拿到这个字段,看下自己是否能给这么大的内存,内存不够就不运行了
// 这个值是对齐以后的值
// 系统在加载运行之前会计算一遍PE的大小,看和这里是否一致,不一致就拒绝运行;防止随便填写值,把内存搞挂
DWORD SizeOfHeaders; // ****** PE头所有头加起来总大小,与 FileAlignment 对齐
// 选项头后面的节有多少项,是由头的大小这个字段决定的,这个字段决定软件有几个段
DWORD CheckSum; //校验值 3环程序随便改, 0环程序会检查,不允许改 可以用 MapFileAndCheckSum 计算值
// 单独看这个字段没用,这个字段必须和数字签名配合使用
WORD Subsystem; //******* 子系统 不能随意修改 /subsystem dll、exe、sys ...
// VS中 设置-链接器-所有选项-子系统
WORD DllCharacteristics; //描述应用程序的一些相关信息(例如是否开了随机基址等) 这个字段的差异比较大
// 区分是否是DLL,根据宏0x0040来区分
// 随机基址可以修改为不是随机基址,相反则不行,随机基址要在PE中存放CPU代码信息(代码重定位信息),不是随机基址就没有信息;
// 随机基址主要防止远程攻击
// 开了随机基址后,ImageBase 字段怎么办? 仅会参考下这个字段, 反汇编工具用的ImageBase确定地址,如果开启了随机基址,反汇编的地址仅供参考
// 反汇编中的地址是否是真实的地址,要看这个程序是否随机,直接改成不随机就行
/*
#define IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA 0x0020 // Image can handle a high entropy 64-bit virtual address space.
#define IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE 0x0040 // DLL can move.
#define IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY 0x0080 // Code Integrity Image
#define IMAGE_DLLCHARACTERISTICS_NX_COMPAT 0x0100 // Image is NX compatible 和DEP(数据执行保护)一样 堆栈段不可以执行代码,把这个标志位去掉就可以执行代码了
#define IMAGE_DLLCHARACTERISTICS_NO_ISOLATION 0x0200 // Image understands isolation and doesn't want it
#define IMAGE_DLLCHARACTERISTICS_NO_SEH 0x0400 // Image does not use SEH. No SE handler may reside in this image
#define IMAGE_DLLCHARACTERISTICS_NO_BIND 0x0800 // Do not bind this image.
#define IMAGE_DLLCHARACTERISTICS_APPCONTAINER 0x1000 // Image should execute in an AppContainer
#define IMAGE_DLLCHARACTERISTICS_WDM_DRIVER 0x2000 // Driver uses WDM model
#define IMAGE_DLLCHARACTERISTICS_GUARD_CF 0x4000 // Image supports Control Flow Guard.
#define IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE 0x8000
*/
//这四个值可以改,但是不能改得太离谱
DWORD SizeOfStackReserve; //栈保留,为线程保留栈大小,一开始提交一部分,必要的时候才提交剩下的部分,就是占了一段内存,只是保留内存地址,不是保留内存,保证内存的连续性
// 这个值如果是4096,PE运行不起来,为什么? 保留太少了,容易溢出
DWORD SizeOfStackCommit; //栈提交,一开始给的内存
DWORD SizeOfHeapReserve; //堆保留,OS先给一块内存,然后慢慢分配;malloc的地址是有规律的,虽然不连续,但是靠的很近,其实是在一块区域给的;
//malloc底层是HeapAlloc,底层有很多可以申请空间的api,不同api在不同区域分配空间
DWORD SizeOfHeapCommit; //堆保留
/*
64位 一个进程可以给到 128T
32 2G
*/
DWORD LoaderFlags; //跟调试相关,目前用不到,值可以随便改
DWORD NumberOfRvaAndSizes; //下面数组个数(最小可以改为2,前2个表是导入,导出表,必须要有)
//数据目录表(成员2个 dword 第一个是内存偏移,第二个是大小 )
//描述PE中各种个样的表的位置和大小,每个下标对应一个固定的表(前2个不能改,导入,导出表,改了无法调API)
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //柔性数组 个数由上面值决定,但是总大小为16个
// 最多15个表,为什么会有16项呢? 最后一项是0
// 通过调整文件头中的SizeOfOptionalHeader 来拓展这个数组大小
/*
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory 导出函数的表
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory 调用api会在这个表里有记录
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
*/
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
注解
01 链接器版本号
判断软件是用什么版本编译的手段:
MajorLinkerVersion:主链接器版本号14
MinorLinkerVersion:副链接器版本号29
操作系统不看这两个字段,可以随意修改,版本号仅供参考;
一般的软件不会修改这个版本号,有一定的参考价值;
02: AddressOfEntryPoint
地址概念:
Virtual Address(VA) : 00401000 程序运行起来的地址
Relative Virtual Address(RVA) : 1000 相对模块基址的偏移
File Address(FA) : PE格式中C8就是文件(指的是PE文件)地址
问题1:为什么VA用的少?
.dll 模块基址不固定,VA就没用了,因此PE结构中很少有结构体用VA表示;
问题2:RVA和FA区别?
情况1:分析文件开头的4D5A:4D5A在文件中FA是0,当程序被加载到内存之后,模块基址开始也是4D5A,RVA也是0,这个时候:RVA == FA
情况2: 分析最后的位置:OS申请内存一次申请一个页,不会一个字节一个字节申请,如下图:
前面100个字节放进前面申请4K内存,最后部分有可能放到再申请4K内存中,RVA != FA
问题3: AddressOfEntryPoint (EP) 是什么地址?
是RVA,OS只需要模块基址+RVA就知道在哪里开始执行代码了;
所以,OD调试,取得地址是模块基址加上RVA的值就知道了
问题4: 如果恶意的把AddressOfEntryPoint改掉怎么办?
这个时候就可以注入代码了,在stdub[]中藏点代码
Entry Point (EP)
Old Entry Point (OEP)
无论刚开始EP跳转到哪里,最终还是要跳转回OEP执行函数入口的代码;
问题5:常说找OPE,为什么要找OEP?
找到旧的OEP,填回到EP,删除恶意代码;
问题6:脱壳
如果代码是被加密的,解密的代码在恶意代码里怎么办?
这样把OEP改回去,代码还是没法运行;通过加密代码的RVA算出FA,把代码全部填回到文件去,这样就做到了在文件中提前解密了;
问题7:AddressOfEntryPoint值位0,是有效的PE格式吗?,可以正常运行?
有效,可以,4D5A的字符串反汇编是一个减法,一个出栈,因此要反向操作回去,才可以正常运行;
03 ImageBase
ImageBase 建议模块基址
命令行编译的时候可以通过 /base修改
link /subsystem:windows /base:0x01000000 pe.obj
开了随机基址,PE里面就会有一个重定位表,记录了所有需要修改的地方,没开没有
04 DataDirectory
系统会通过格式来便利这个表,而不是大小,这个大小随便改,每个字段的大小都可以改,有的软件会用这个大小来遍历表,让他崩