本来是想学习魔改壳和脱壳后修正IAT的,发现涉及到很多PE的东西,以前也大概学过,这次就全都总结一下。
参考链接:
https://docs.microsoft.com/en-us/windows/win32/debug/pe-format
https://www.52pojie.cn/thread-326995-1-1.html
https://blog.csdn.net/whatday/article/details/99709317
https://bbs.pediy.com/thread-181433-1.htm
https://www.cnblogs.com/LyShark/p/13731329.html
https://blog.csdn.net/apollon_krj/category_7066517.html
https://www.cnblogs.com/LyShark/p/13731329.html
https://www.cnblogs.com/zpchcbd/p/13071219.html
PE
- PE头
- DOS头
- 节区头
- PE体
几个常用概念:
- **偏移(offset)**:文件中偏移。
- **虚拟地址(VA, Virtual Address)**:内存中使用,进程虚拟内存的绝对地址。
- **相对虚拟地址(RVA, Relative Virtual Address)**:从基准位置(ImageBase)开始的相对地址。
测试代码:
1 |
|
PE头
DOS头
40h,64字节
1 | struct _IMAGE_DOS_HEADER{ |
DOS头除了开头一个字和结束的一个双字,中间都可以换成没用的数据。
DOS存根
DOS头和NT头中间,没什么用可以扬了。
这是为了兼容MS-DOS,作用就在DOS环境下打一个This program cannot be run in DOS mode.
然后退出。
NT头
1 | //NT头 |
标准文件头
1 | //标准PE头:最基础的文件信息,共20字节 |
文件属性Characteristics各bit的信息:
可选文件头
1 | //可选PE头 |
FileBuffer
是磁盘上.exe
文件在内存中的一份拷贝,但是FileBuffer
无法直接在内存中运行,必须经过PE loader
(装载器)装载以后成为ImageBuffer
。ImageBuffer
是FileBuffer
的”拉伸”。即exe–>FileBuffer–>ImageBuffer
- .exe首地址(基址)为0
- FileBuffer首地址也为0
- ImageBuffer首地址为ImageBase
- 而真正的程序入口地址是:ImageBase + AddressOfEntryPoint(OEP)
丢进IDA里看一下,找1530这个地址,可以发现是mainCRTStartup
的地址,即入口点函数。
1 | struct _IMAGE_DATA_DIRECTORY{ |
16个_IMAGE_DATA_DIRECTORY
分别存储了以下信息:
导入表、导出表、资源、异常信息、安全证书、重定位表、调试信息、版权所有、全局指针、TLS、加载配置、绑定导入、IAT、延迟导入、COM信息、最后一个保留未使用。
节表
1 |
|
Flag | Value | Description |
---|---|---|
IMAGE_SCN_CNT_CODE | 0x00000020 | The section contains executable code. |
IMAGE_SCN_CNT_INITIALIZED_DATA | 0x00000040 | The section contains initialized data. |
IMAGE_SCN_CNT_UNINITIALIZED_ DATA | 0x00000080 | The section contains uninitialized data. |
IMAGE_SCN_MEM_EXECUTE | 0x20000000 | The section can be executed as code. |
IMAGE_SCN_MEM_READ | 0x40000000 | The section can be read. |
IMAGE_SCN_MEM_WRITE | 0x80000000 | The section can be written to. |
PE体
IID、IAT、绑定导入表
可选头的DataDirectory数组指明了IID和IAT的地址。
DataDirectory[1]
中的VA指向IIDDataDirectory[11]
中的VA指向绑定导入表DataDirectory[12]
中的VA指向IAT
IID(导入表)
IID结构体记录了PE文件要导入哪些库文件。
1 | typedef struct IMAGE_IMPORT_DESCRIPTOR { |
- 联合体值为0时(一般用Characteristics判断是否是0),表示这是导入表结构体数组最后一个元素.除了最后这一个元素,其它每一个结构体都保存了一个dll信息。
联合体的值不为0时,用OriginalFirstThunk(RVA)来索引INT的地址。这张INT表存放了该dll的导出函数的信息(序号与函数名)。 - TimeDateStamp: 当时间戳值为0时,表示未加载前IAT表与INT表完全相同,不存在绑定导入表;
当时间戳不为0(为-1)时,表示IAT与INT表不同,IAT存储的是该dll的所有函数的绝对地址。这样在未加载前就直接填充函数地址的方式称为函数地址的绑定,其地址是根据绑定导入表来确定的。也就是说当时间戳为-1时绑定导入表才有效,而真正的时间戳存放到绑定导入表中,否则无效。 - ForwarderChain: 一般情况下我们也可以忽略该字段。在老版的绑定中,它引用API的第一个forwarder chain(传递器链表)。
- Name: RVA指向dll的名字字符串。
- FirstThunk: RVA指向IAT表。
IAT与INT
IAT(Import Address Table)、INT(import Name Table)
PE装载器把导入函数输入至IAT的顺序:
- 读取IID的Name成员,获取库名称字符串。
- 装载相应库。
-> LoadLibrary("dll_name")
- 读取IID的OriginalFirstThunk,获取INT地址。
- 逐一读取INT中数组的值,获取相应
IMAGE_IMPORT_BY_NAME
地址。- 使用
IMAGE_IMPORT_BY_NAME
获取相应函数的起始地址。
-> GetProcAddress("fun_name")
- 读取IID的FirstThunk(IAT)成员,获得IAT地址
- 将获得的函数地址填入IAT相应的数组值
- 重复4-7,直到INT结束。
- 加载到内存前
两者结构相同,IAT和INT都指向一个结构体数组,此数组存储要导入的函数的序号和函数名。
IAT和INT的元素为IMAGE_THUNK_DATA结构,而其指向为IMAGE_IMPORT_BY_NAME结构。
1 | typedef struct _IMAGE_THUNK_DATA32 { |
4字节最高位如果为0,则这4字节值为IMAGE_IMPORT_BY_NAME的RVA;
4字节最高位如果为1,则去掉最高位,剩下的31bit值时dll函数在导出表中的导出序号。
1 | typedef struct _IMAGE_IMPORT_BY_NAME { |
- 加载到内存后
INT保持不变,IAT根据导入表INT(IAT加载前)的内容和导出表信息,修改为对应的函数的地址信息。
绑定导入表
依据导入表中的时间戳来判断IAT是否进行了绑定导入。
- 为0是没有绑定。
- 为-1是进行了绑定导入,此时IAT中填写的是DLL函数的地址。
当DLL地址重定位时,则绑定导入表和重定位表需要修改。
1 | //最后一个结构全0表示绑定导入表结束 |
NumberOfModuleForwarderRefs
是指该dll自身依赖的dll的个数。
值为n代表该结构后面紧跟了n个IMAGE_BOUND_FORWARDER_REF
结构。
1 | typedef struct _IMAGE_BOUND_FORWARDER_REF { |
注意:这两个结构体中所有的OffsetModuleName都是相对于绑定导入表首地址的偏移地址。
即:绑定导入表首地址 + OffsetModuleName = RVA
EAT
导出表(Export Table)。
这玩意一般是给DLL用的,别的exe可以用DLL导出的函数,exe一般没有导出表。
DataDirectory[0]
中的VA指向导出表。
1 | typedef struct _IMAGE_EXPORT_DIRECTORY { |
- 每个dll都有一个导出表,每个导出表有三个子导出表(地址AddressOfFunctions、名字AddressOfNames、序号AddressOfOrdinals)。
- NumberOfFunctions是函数序号最大值与最小值之间的差值。
- NumberOfNames是函数以名字导出的个数。
- 二者可以不一样大。
- 一个函数必定有地址,但不一定有名字。
- 导出表中AddressOfFunction指向的地址表大小 = NumberOfFunctions * 4;
- AddressOfNames指向的名字表大小 = NumberOfNames * 4;
- AddressOfNameOrdinals指向的序号表中的值是非准确的,应该均加上Base才是真正的序号(Base等于序号表中最小的值)。而序号表大小 = NumberOfNames * 2。
- 地址表可能大于等于名字表,也有可能小于名字表,因为一个函数可能没有名字,也可能有多个名字。但是一般情况下,名字表均不会大于地址表。并且一个函数必然有地址,不一定有名字,名字表和序号表一一对应。
从库中获取函数地址的API称为GetProcAddress()
函数。该API引用EAT来获取指定API的地址。
下面说明它如何获取函数地址。
- 用AddressOfNames成员转到函数名称数组
- 通过比较数组中字符串,查找指定函数名
- 用AddressOfNameOrdinals成员转到函数序号数组
- 在序号数组中查找相应的序号值
- 用AddressOfFunctions转到函数地址数组
- 在地址数组中用序号作为索引,获得指定函数的起始地址
壳
去除UPX特征
修改区段名
十六进制编辑器打开,可以看到UPX0、UPX1、UPX2三个区段名,随便改成其他的什么名字,再试试upx的-d
命令
可以看到已经不能脱壳了。
去除UPX头
0x200到0x224处的数据是UPX头,供UPX脱壳识别的一些信息,不影响程序运行,只与解压缩壳有关,全部给他扬了。
程序还可以正常运行,不过一些UPX版本信息之类的已经是无了。
特征码
不过就算扬了UPX头,exeinfo之类的查壳程序也还是能检测出来程序有UPX壳,因为检测UPX是通过一些特征码来检测的。
UPX解压缩时必然会用到一些汇编指令,这些指令的机器码就成为了用来检测UPX的特征码。
UPX特征码:
1 | 特征码1:60 BE ?? ?? ?? 00 8D BE ?? ?? ?? FF |
[De1CTF2019]Re_Sign
为什么用这个题,因为写这玩意的时候手头没有现成的32位程序,我也懒得去编译一个了
不能直接upx -d
,拖进winhex看看,发现是UPX头被扬了
第一次接触这个题是大概半年之前了,当时手脱壳脱不出来,搜了很多wp,要么跳过脱壳的过程直接开始分析,要么就是用吾爱破解的静态脱壳机(这玩意确实好用,不过现在新版UPX的壳已经脱不了了),又看到有人说是导入表乱了,反正是一堆问题 =_=
最近重新开始学PE文件格式,顺带把壳和傀儡进程也学习一下,想到这个题又鼓捣了一通。刚开始以为是ASLR的问题,查了发现这个文件没开ASLR……然后又试着脱了一下,当场脱出来了,运行也一切正常。本来想详细写一下手脱壳过程,但是复现的时候又双叒叕gg了,死活复现不出来,干,只能放弃了。真是玄学。
修改特征码
试一下改特征码1,把入口的pushad扬了。
顺手改一下区段名。
程序还能正常运行,但是脱壳机已经gg了。
DIE扫一下,已经识别不出来UPX壳了。
Exeinfo看一下
识别出来壳,不过已经识别不出来是UPX壳了。
换NFD看看
疑似UPX壳,看来还是识别出来了。
再继续瞎改一通,思路就是保持汇编指令的含义不变,但是使用别的指令。
特征码2:
特征码3:
其实特征码3这里没看懂原文short改long怎么改的(wtcl wtcl
于是我就使劲盯着这段代码看,后来发现最后的指令是popad恢复现场,那么前面不管pop到哪个寄存器里应该就都无所谓了,只要保证堆栈平衡就行,于是把eax改成ebx。
然后再加2个垃圾区段
这个启发式搜索害挺🐂🍺的,不开的话扫不出来壳,开的话扬了特征码加了垃圾区段还是能扫出来壳,以后一定要学学这是个啥东西(坑++
移动PE头
这个用的还是我自己写的64位程序。
选中的部分是可以扬了的DOS头,不影响程序运行。
扬了DOS头要修改PE头的入口地址,并且保证PE头的长度等于原来的DOS头加PE头长度,不然后面的地址都会出问题。
DOS头的长度是0x40,扬了DOS头之后在PE头后面贴0x40个无用数据。当然DOS头也可以只扬一部分 怎么高兴怎么来
修改后
测试可以正常运行
这里我一定要吐槽一下,刚开始在winhex里贴数据的时候我一直贴的是40个,然后每次都失败。刚开始猜想是不是64位和32位文件格式不同之类的问题,然后用32位程序试了一下。32位的DOS头长度是0xc0,所以要贴0xc0个数据,输数据的时候我才恍然大悟,c0是无效数据,那么其实我输入40他给我贴的是40个数据而不是0x40个数据
哈哈,有傻子,我不说是谁 :)
呃呃呃,隔的时间太久,之前修改过特征码的文件已经被我扬了,就不测exeinfo和DIE了,先记这么多(逃
IAT修复
🕊️🕊️🕊️学了再来补(逃
关于本文
本文作者 云之君, 许可由 CC BY-NC 4.0.