PE文件格式
文章目录
13. PE文件格式
PE是微软基于UNIX的COFF(common object file format)制作的。本想提高移植性,但实际上只能在windows上使用。
种类 | 主扩展名 | 种类 | 主扩展名 |
---|---|---|---|
可执行 | exe, scr | 驱动程序 | sys, vxd |
库 | dll, ocx, cpl, drv | 对象文件 | obj |
可以说,obj之外的文件都是可执行的。
PE头:dos头 —— section header ;PE体:余下部分。
PE头与各区块的尾部存在一个区域,NULL padding,因为有最小使用单位。
dos头之后有一个可选项,dos存根(dos stub),大小不固定,即使没有,文件也能运行。它由代码和数据混合而成。
对于32位的程序,dos下执行会输出"this program cannot be run in DOS mode";windows中会运行32位代码。
exe和dll被装载到用户内存的0-7fffffffh
;sys被装载到内核内存的80000000h-ffffffffh
。
装载到内存中的形态称为映像(image)。
13.4 RVA to RAW
关于RVA to RAW的转换公式,也有例外。
RVA=ABA8,根据上图,则raw file offset = aba8 - 9000 + 7c00 = 97a8,显然不对,因为VirtualSize
比SizeOfRawData
大。注意,VirtualSize
是对齐之前的值。
13.5 IAT
加载dll有两种方式:
- explicit linking,显式链接,使用dll时加载,使用完毕后释放内存;
- implicit linking,隐式链接,程序开始时即加载dll,程序终止时再释放内存。
IAT提供的机制为隐式链接。
为什么要call [函数地址的地址]
,而不是不直接call dll函数地址
(这是dos时代的方式)?
- dll版本各不相同(如kernel32),程序员也不知道程序将来在哪种系统上运行,为确保所有环境下都能正常调用,编译器保存了函数地址的地址,执行时,将函数地址写在该处;
- 另一个原因是dll重定位,导致无法对实际地址硬编码;
- 最后,PE头中表示的地址不是VA,而是RVA。
IAT输入顺序:
- 读取
IID.Name
,获取库名称字符串(“kernel32.dll”); - 装载库,
LoadLibrary("kernel32.dll")
; - 读取
IID.OriginalFirstThunk
,获取INT地址; - 逐一读取INT数组的值,获取相应
_IMAGE_IMPORT_BY_NAME
的地址(RVA); - 使用
_IMAGE_IMPORT_BY_NAME.Name
或Hint
,获取相应函数的起始地址; - 读取
IID.FirstThunk
,获得IAT地址; - 将函数地址写入IAT数组;
- 重复4-7,知道INT结束。
13.6 EAT
从库中获得函数地址的api为GetProcAddress()
。原理如下:
- 它通过
_IMAGE_EXPORT_DIRECTORY.AddressOfNames
; - 遍历函数名称数组,
strcmp()
比较字符串,查找函数,假设找到的索引为name_index
; - 利用
AddressOfNameOrdinals
转到ordinals数组; ordinals[name_index]
查找对应序号;- 通过
AddressOfFunctions
转到函数地址数组EAT; EAT[ordinal]
即指定函数地址。
以kernel32.dll为例,所有导出函数j均有名称,且AddressOfNameOrdinals
数组以index==ordinal
的形式存在。也有dll的导出函数没有名称,只能通过ordinal导出,ordinal也未必等于AddressOfNameOrdinals
数组索引。
对于没有名称的函数,
AddressOfFunctions[ordinal-IED.Base]
即对应函数地址。
_IMAGE_FILE_HEADER.TimeDateStamp
可以用ctime_s()
转换为可读格式。
#include<stdio.h>
#include<time.h>
int main()
{
time_t curtime = 0x47918EA2;
char buf[30] = { 0 };
ctime_s(buf, 30, &curtime);
printf("当前时间 = %s\n", buf);
return 0;
}
14-15. 运行时压缩
关键词:运行时压缩器(run-time packer),lossless/loss data compression.
运行时压缩是针对PE而言的,文件内部有解压缩代码。
把普通PE文件创建为运行时压缩文件的程序叫做压缩器(packer);经过反逆向(anti-reversing)处理的压缩器叫做保护器(protector)。
压缩器
压缩器目的:
- 较小文件,便于传输和保存;
- 隐藏代码和资源(解压缩后可以通过内存dump窗口查看)。
压缩器分类:
- 单纯用于压缩的,UPX,ASPack;
- 破坏PE头、隐藏目的的恶意压缩器,UPack,PESpin,NSAnti。
有很多工具可以帮助划分。
保护器
保护器压缩后的文件反而会变大。用于防止破解、保护代码与资源。
常用于游戏、恶意代码。
分类:
- 商用:ASProtect, Themida, SCKP
- 公用:UltraProtect, Morphine.
upx举例
压缩前的exe,一般有以下部分:
- 调用
GetModuleHandle()
api,获取exe的ImageBase
; cmp
比较MZ、PE签名。
ollydbg打开压缩文件时,判断文件为压缩文件,选择是或不是。
upx压缩后,区块表会多出upx0,upx1
,且upx0
的RawDataSize
为0。解压缩代码与压缩的源码都在第二个区块upx1
,运行瞬间会将源码解压到第一个区块。
pushad ;push eax~edi
mov esi,addr_of_upx1
lea edi,addr_of_upx0
像这样的开头,可以预见要从esi缓冲区解压缩数据复制到edi缓冲区。
遇到循环,先了解作用再跳出。
UPX壳的特征之一,是EP代码位于pushad,popad
之间。popad
之后紧跟着jmp oep;
跟踪过程有4个循环:
- 复制一部分数据;
- 解压缩后复制;
- 恢复
call/jmp
; upx1
有INT,反复调用GetProcAddress()
获取api地址,写入ebx所指的原IAT区域。
16. 基址重定位
使用DDK(driver development kit)创建的sys文件默认ImageBase
为010000h。
windows vista之后引入aslr,每次运行exe都会加载到随即地址。aslr也适用于dll、sys文件。同一系统的kernel32.dll、user32.dll等会被加载到固有的ImageBase
,所以,系统的dll实际不会发生重定位。
无法加载到ImageBase
地址时,若未进行PE重定位处理,就会导致“内存地址引用错误”,异常终止。
_IMAGE_OPTIONAL_HEADER64.DataDirectory[5]
为基址重定位表的地址。
恶意代码通常把TypeOffset.Type
修改为0,IMAGE_REL_BASED_ABSOLUTE
,略去PE装载器的重定位过程。
17. 从可执行文件中删除.reloc
exe中基址重定位表对运行没什么影响,可用PEView或HexEditor通过以下4步删除:
- 整理reloc节区头,记下
VirtualSize
(设为e40),然后用0覆盖头部; - 删除reloc节区,;
- 修改
IMAGE_FILE_HEADER
,NumberOfSections
减1; - 修改
IMAGE_OPTIONAL_HEADER
,SizeOfImage
减去SectionAlignment
对齐后的VirtualSize
(e40->1000),。
18. UPack PE文件头
多数恶意代码使用UPack,大部分杀软干脆将UPack压缩的文件直接识别为恶意文件。所以分析时要关闭杀软。
UPack直接压缩源文件,所以压缩前记着备份。
PEView等工具无法读取压缩后的PE头,推荐使用Stud_PE工具。
重叠文件头
也是其它压缩器常用的方法,可以把dos头和pe头重叠,节约文件头空间,提高分析难度。
_IMAGE_FILE_HEADER.SizeOfOptionalHeader
该字段在32位中是eo,64位PE32+中是f0。
_IMAGE_OPTIONAL_HEADER64
的起始地址加上SizeOfOptionalHeader
就是_IMAGE_SECTION_HEADER
。
UPack把SizeOfOptionalHeader
改为148h。UPack基本特征是使PE文件头变形,向文件头添加解码的代码。增大该字段后,就在_IMAGE_OPTIONAL_HEADER64
和_IMAGE_SECTION_HEADER
之间添加了额外空间,添加代码。
PE工具把这段代码解析为PE头,就会引发错误。
_IMAGE_OPTIONAL_HEADER64.NumberOfRvaAndSizes
改变该字段也是为了插入代码。
这个字段指出DataDirectory
数组的元素个数。UPack把它从10h改为了0ah,导致后6个元素被忽略,被忽略的这块区域可以用来覆写自己的代码。
_IMAGE_SECTION_HEADER
UPack会用自身数据覆盖程序运行不需要的结构体成员。
重叠节区
UPack的另一个特征是可以随意重叠PE节区与文件头。
例如,可以让两个节区的PointerToRawData
和SizeOfRawData
一致,但内存相关的VirtualSize
和VirtualAddress
不冲突。PE规范没有明确这样是不行的。
RVA to RAW
_IMAGE_SECTION_HEADER.PointerToRawData
应该是_IMAGE_OPTIONAL_HEADER64.FileAlignment
(0x200)的整数倍。PE装载器发现不是整数倍时,会强制将其识别为整数倍(10–>0),这样UPack文件就能运行了。
很多PE工具,包括ollydbg的早期版本,都不能找出UPack的EP。
IID 数组???
IID[0]
之后,既不是第二个IID,也不是空结构体。这里是第3个节区的结束,之后的部分不会映射到第3个节区内存。
从文件看导入表好像损坏了,其实已经在内存中准确表现。
IID.FirstThunk
有时IID.OriginalFirstThunk
(INT)为0时,不能通过它来找API名称字符串,那么跟踪FirstThunk
(IAT)也可以。
20. 内嵌补丁
inline code patch。
当OEP经过运行时压缩或加密时难以直接修改,可以让EP代码解密后修改jmp,运行洞穴代码(code cave),再跳转到OEP对内存代码打补丁。
普通补丁 | 内嵌补丁 | |
---|---|---|
对象 | 文件 | 文件&内存 |
次数 | 1次 | 文件1次,内存中每次运行时 |
方法 | 直接 | 间接 |
解密示例代码:
mov ebx,xxx
mov ecx,xxx
00010203:
xor byte ptr ds:[ebx], xxx;
sub ecx
inc ebx
cmp ecx,0
jnz 00010203
code cave 可以填充在NULL padding的区域。