《逆向工程核心原理》学习笔记(二):PE文件

本文详细介绍了PE文件格式,包括PE头的各个组成部分如DOS头、NT头、节区头、导入地址表(IAT)、重定位表等,并探讨了运行时压缩技术,以UPX为例分析了压缩原理和调试过程。此外,还讨论了如何从可执行文件中删除.reloc节区以及对UPackPE文件头的分析。最后,通过内嵌补丁的实践展示了程序补丁的实现方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

继续学习《逆向工程核心原理》,本篇笔记是第二部分:PE文件格式,包括PE文件、运行时压缩、重定位、UPack、内嵌补丁等内容

一、PE文件

1、PE文件格式

PE(Portable Executable)文件

  • windows下的32位可执行文件格式,64位称为PE+或PE32+
  • 大量信息以结构体形式存储在PE头中
  • 种类如下:
    在这里插入图片描述

以notepad.exe为例,用winhex打开如下:

在这里插入图片描述
其加载到内存时的情形如下

  • PE头:DOS头到节区头,内部信息大多以相对虚拟地址(Relative Virtual Address,RVA)形式存在
  • 文件中用偏移(offset)表示位置,内存中用虚拟地址(Virtual Address,VA)表示位置
  • PE头与各节区尾部都有一个NULL填充,使各个节区的起始位置都在最小单位的倍数位置上
    在这里插入图片描述

2、PE头

(1)DOS头

考虑对DOS文件的兼容
PE文件最前面有个40字节大小的IMAGE_DOS_HEADER结构体
来扩展已有的 DOS EXE 头

在这里插入图片描述
两个重要成员

  • e_magic:DOS签名(4D5A,ASCII值是MZ),所有PE文件都有
  • e_lfanew:指示NT头的偏移,指向NT头(IMAGE_NT_HEADER)所在位置

(2)DOS存根

DOS存根(stub)在DOS头下方

  • 可选项
  • 大小不固定

notepad的DOS存根是

在这里插入图片描述
在DOS环境中会输出:!This program cannot be run in DOS mode.
然后终止运行

感觉可以理解为,这部分是在DOS环境中会执行的文件(PE文件中的文件)

(3)NT头

IMAGE_NT_HEADERS有3个成员,如下图所示:

  • 签名(Signature)结构体,值是50 45 00 00,ASCII值是PE
  • 文件头
  • 可选头

在这里插入图片描述
文件头表现文件属性(设置错误则文件无法运行),如下图所示:
在这里插入图片描述

  • Machine码:定义在winnt.h文件中,每个CPU都有唯一的Machine码
  • NumberOfSections:指出文件中存在的节区数量,且必须与实际数量相同
  • SizeOfOptionalHeader:指出IMAGE_OPTIONAL_HEADER32结构体的长度(PE+是IMAGE_OPTIONAL_HEADER64)
  • Characteristics:标识文件属性,是否可运行,是否为DLL等,具体如下图所示
    在这里插入图片描述在这里插入图片描述

可选头:IMAGE_OPTIONAL_HEADER32是PE头结构体中最大的,如下图所示
在这里插入图片描述
在这里插入图片描述

  • Magic:32位是10B,64位是20B

  • AddressOfEntryPoint:EP的RVA值,指出程序的起始地址

  • ImageBase:指出文件加载到内存时的优先装入地址,通常exe是00400000,DLL是10000000

  • SectionAlignment:指定节区在内存中的最小单位

  • FileAlignment:指定节区在磁盘文件中的最小单位

  • SizeOfImage:指定PE Image在虚拟内存中所占空间大小

  • SizeOfHeader:指出整个PE头的大小,是FileAlignment的整数倍

  • Subsystem:区分系统驱动文件(.sys)和普通可执行文件(.exe,.dll),如下图所示
    在这里插入图片描述

  • NumberOfRvaAndSizes:指定DataDirectory数组的个数

  • DataDirectory:数组,每项定义如下图所示,需要注意0、1、2、9这几个
    在这里插入图片描述

(4)节区头

节区头定义了各节区属性,如下图所示:

在这里插入图片描述
节区头由IMAGE_SECTION_HEADER结构体组成,如下图所示:

在这里插入图片描述
几个重要值

  • VirtualSize:内存中节区大小,由SectionAlignment确定
  • PointerToRawData:磁盘文件中节区大小,由FileAlignment确定
  • VirtualAddress:内存中节区起始地址(RVA)
  • PointerToRawData:磁盘文件中节区起始位置
  • Charateristics:节区属性,具体值如下图所示
    在这里插入图片描述

3、RVA to RAW

RVA to RAW:PE文件加载到内存时,每个节区准确完成的内存地址与文件偏移间的映射

  • 查找RVA所在节区
  • 计算文件偏移(RAW),公式如下:
    在这里插入图片描述

4、IAT

导入地址表(Import Address Table,IAT)是一种表格,记录程序正在使用哪些库中的哪些函数

(1)DLL

动态链接库(Dynamic Linked Library, DLL)

  • 32位时引入
  • 库单独组成DLL,需要时调用
  • 内存映射技术使得DLL代码在多个进程中共享
  • 更新库的时候只要更新DLL文件

加载方式

  • 显式链接:程序使用DLL时加载,使用完释放内存
  • 隐式链接:程序开始时一同加载DLL,程序终止时释放内存

IAT就是隐式链接,几个原因:

  • 不同环境,函数和DLL的位置不同
  • DLL重定位问题使得无法对地址硬编码
  • PE头中用的是RVA

(2)IMAGE_IMPORT_DESCRIPTOR

IMAGE_IMPORT_DESCRIPTOR结构体记录了PE文件要导入哪些库文件,导入多少个库就有多少个IMAGE_IMPORT_DESCRIPTOR结构体,形成以NULL结尾的数组。IMAGE_IMPORT_DESCRIPTOR结构体具体如下图所示:

在这里插入图片描述
notepad.exe的kernel32.dll的IMAGE_IMPORT_DESCRIPTOR如下:

在这里插入图片描述
在这里插入图片描述

5、EAT

EAT使不同的应用程序可以调用库文件中提供的函数,即通过EAT才能得到从相应库中导出函数的起始地址,由IMAGE_EXPORT_DIRECTORY结构体保存导出信息
在这里插入图片描述
可以在PE头中找到IMAGE_EXPORT_DIRECTORY结构体的位置,起始地址是IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress值(即RVA值)

以notepad.exe为例,RVA是262C,所以文件偏移是1A2C
在这里插入图片描述
kernel32.dll的EAT如下图所示:
在这里插入图片描述
从库中获得函数地址的API为GetProcAddress(),原理如下:

在这里插入图片描述

6、小结

PE规范只是一种标准规范而已

书中推荐了一个工具PEView
当然吾爱破解上有个studyPE我感觉更好用

二、运行时压缩

1、数据压缩

  • 无损压缩:能100%恢复,如Run-Lenth、Lempel-Ziv、Huffman等算法
  • 有损压缩:损失一定信息,高压缩率,如MP3、MP4、jpg

2、运行时压缩器

运行时压缩器(Run-Time Packer)是针对可执行文件(PE文件)而言的,可执行文件内部有解压缩代码,在运行瞬间于内存中解压缩后执行

在这里插入图片描述

运行时压缩器(Packer)

  • 缩减PE文件大小
  • 隐藏PE文件内部代码与资源
  • 纯粹的:UPX、ASPack等
  • 不纯的:UPack、PESpin等

PE保护器(Protector)

  • 防止破解,采用多种反逆向技术
  • 保护代码和资源
  • 商用:ASProtect、Themida、SVKP等
  • 公用:UltraProtect、Morphine等

例子:用UPX压缩notepad.exe后对比如下:

  • PE头大小一样
  • 节区名称改变
  • 第一个节区的RawDataSize=0,实际上是运行时解压到第一个节区
  • EP从第一个节区变到第二个节区
  • 资源节区(.rsrc)大小几乎不变

在这里插入图片描述

三、调试UPX压缩的notepad程序

1、notepad.exe的EP代码

在OD里看notepad.exe

  • 010073B2处调用了GetModuleHandleA API,获取notepad.exe的ImageBase
  • 010073B4010073C0处比较MZ和PE签名

在这里插入图片描述

2、notepad_upx.exe的EP代码

在OD里看notepad_upx.exe,会有个警告弹窗

  • EP地址01015330 ,是第二个节区的末端,即实际压缩的notepad源代码在上方
  • 第二个节区的起始地址01011000和第一个节区的起始地址01001000分别设置到ESI和EDI,如此同时设置ESI和EDI,可以预见ESI所指缓冲区到EDI所指缓冲区的内存发生了复制,从ESI读取数据解压后保存到EDI

在这里插入图片描述

3、跟踪UPX文件

数量庞大的代码通常用以下跟踪命令

在这里插入图片描述
基本流程:

  • Ctrl+F8开始跟踪
  • 在遇到循环时,可以执行F7暂停
  • 然后观察循环内容
  • 再F2设置断点,F9跳出

(1)循环1

在这里插入图片描述
内容为从EDX(01001000)中读取放入EDI(01001001

(2)循环2

在这里插入图片描述
这是个解压循环,从ESI指的第二节区读取数据,解压,放入EDI指的第一节区

(3)循环3

在这里插入图片描述
这段循环恢复源代码的CALL/JMP指令的目标地址

(4)循环4

在这里插入图片描述
这个循环设置IAT

在这里插入图片描述
最终在010154BB处跳转到notepad.exe的OEP

4、快速查找UPX OEP

UPX的EP代码在PUSHAD和POPAD之间
所以在POPAD之后的JMP就是跳转到OEP的
只要在这个JMP设置断点就能找到OEP

在这里插入图片描述
也可以在Dump窗口找到栈地址设置硬件断点

四、基址重定位表

1、PE重定位

在向进程的虚拟内存加载DLL/SYS文件时
若ImageBase处已经有DLL/SYS文件
就涉及PE重定位,如下图所示
(EXE文件会首先加载到内存中,无需考虑重定位)
在这里插入图片描述

2、PE重定位操作原理

PE重定位的基本操作原理

  • 在应用程序中查找硬编码的地址
  • 读取值后,减去ImageBase(VA->RVA)
  • 加上实际加载地址(RVA->VA)

查找硬编码地址会用到重定位表(Relocation Table)——记录硬编码地址偏移的表
notepad.exe的重定位表如图所示:

在这里插入图片描述
基址重定位表是IMAGE_BASE_RELOCATION结构体数组,如下:
在这里插入图片描述

五、从可执行文件中删除.reloc节区

1、.reloc 节区

VC++生成的PE文件的重定位节区是.reloc

  • 一般位于所有节区最后
  • 删除后文件照常运行

2、示例:reloc.exe

删除.reloc流程

  • 整理.reloc节区头
  • 删除.reloc节区
  • 修改IMAGE_FILE_HEADER
  • 修改IMAGE_OPTIONAL_HEADER

PEView打开reloc.exe,如下图所示

  • 可以看到.reloc节区头从00000270文件偏移开始,大小为28(270-297)
  • .reloc节区偏移地址从0000C000开始

在这里插入图片描述
用winhex打开reloc.exe,如下图所示

  • 找到.reloc节区头对应位置全改为0
  • 找到.reloc节区对应位置全删了

在这里插入图片描述
在这里插入图片描述
修改IMAGE_FILE_HEADER中的Numbers of Sections如下

在这里插入图片描述
在这里插入图片描述
修改IMAGE_OPTIONAL_HEADER的Size of Image如下
在这里插入图片描述
如此就完成了对.reloc 节区的删除

六、UPack PE 文件头分析

1、UPack

UPack是一个叫dwing的中国人编写的
以对PE头独特的变形技法闻名

可以用Stud_PE(可以到吾爱破解下载)查看PE文件头,如下:

在这里插入图片描述

2、比较PE文件头

notepad.exe文件头如下:
在这里插入图片描述
UPack处理后的notepad.exe文件头如下:
在这里插入图片描述

3、分析UPack的PE文件头

(1)重叠文件头

把MZ文件头和PE文件头巧妙重叠
用Stud_PE查看如下:

在这里插入图片描述

PE头起始位置由e_lfanew(在3C文件偏移处,如上图所示)决定
一般有e_lfanew=MZ文件头大小(40)+DOS存根大小(VC++下是A0)=E0
在UPack中e_lfanew=10,钻了规则空子,使得PE头和MZ头可以重叠

(2)IMAGE_FILE_HEADER.SizeOfOptionalHeader

SizeOfOptionalHeader表示PE头中紧接IMAGE_FILE_HEADER下的IMAGE_OPTIONAL_HEADER结构体的长度(E0),它还确定了节区头的起始偏移
在这里插入图片描述

UPack将这个值改为148,比正常值大些(如上图所示),使得节区头从170开始
这个额外空间可以加入解码代码

(3)IMAGE_OPTIONAL_HEADER.NumbreOfRvaAndSizes

这个值指出紧接着的IMAGE_DATA_DIRECTORY结构体数组的元素个数,正常是10
UPack改为0A,如下图所示
在这里插入图片描述
但是IMAGE_DATA_DIRECTORY结构体数组个数就是10
于是UPack修改后,IMAGE_DATA_DIRECTORY结构体后6个字节被忽略
UPack可以在这6个字节中加入自己的代码,如下图所示:
在这里插入图片描述

(4)IMAGE_SECTION_HEADER

IMAGE_SECTION_HEADER结构体中,UPack会覆盖程序本身不需要的区域

在这里插入图片描述
书中整理了下,如下:

在这里插入图片描述

(5)重叠节区

UPack的特征之一:可以随意重叠PE节区和文件头

第一节区和第三节区的起始偏移和size都一致,但RVA不同,如下图所示:
在这里插入图片描述

为了更好的理解,书中给出了下图:
在这里插入图片描述

(6)RVA to RAW

常规变换之前说过,如下:
在这里插入图片描述
但是UPack第一个节区的PointerToRawData10,而不是FileAlignment200)的整数倍
于是如果按正常变换会被强制识别为0,就报错

(7)导入表

先获取导入表地址如下:

  • RVA是271EE
  • size是14

在这里插入图片描述
RVA转换为RAW,如下:
在这里插入图片描述
找到内容如下:
在这里插入图片描述
这里的玄机在于

  • 按照规定,导入表由一系列IMAGE_IMPORT_DESCRIPTOR结构体组成,最后以一个内容为NULL的结构体结束
  • 但是上图框出来的第一个结构体后面既不是第二个结构体,也不是内容为NULL的结束结构体
  • 看似违反规定,但是偏移在200以下的部分不会映射到第三节区,如下图所示
    在这里插入图片描述
    映射之后,27200开始就全是NULL了,这就符合规定了

简单讲,就是映射到内存里之后,是符合规定的;但是单看文件似乎是损坏的

七、“内嵌补丁”练习

1、内嵌补丁

内嵌补丁(Inline Patch):难以直接修改指定代码时,插入并运行洞穴代码(Code Cave)后,对程序打补丁,如下图所示,很直观,很好理解

在这里插入图片描述

代码补丁和内嵌补丁的区别如下:

在这里插入图片描述

2、练习:Patchme

名为ap0x的逆向分析者制作的小程序,运行如下:

在这里插入图片描述
在这里插入图片描述
扔进OD,401007之后都是加密的

在这里插入图片描述
尝试search for strings
在这里插入图片描述
显然是加密了的

那就调试吧

可以看到三个循环
在这里插入图片描述
在这里插入图片描述
仔细观察
第一个循环里的4010A3调用后两个循环
4010F5-401248进行解密

他的代码结构大致如下

在这里插入图片描述
在这里插入图片描述
加补丁代码的方法

  • 设置到文件的空白区域,补丁代码较少时选这个
  • 扩展最后节区后设置
  • 添加新节区后设置

修改完后的一些地方如下
在这里插入图片描述

结语

对PE头和两种加壳有了初步了解

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值