欢迎大家一起来Hacking水友攻防实验室学习,渗透测试,代码审计,免杀逆向,实战分享,靶场靶机,求关注
PE文件学习
推荐工具:lord PE、stud PE 《PE权威指南》,了解格式,看雪,吾爱破解 ,EXE是如何组成的,如何逆向一个EXE文件和安卓文件。
EXE文件和DLL文件实际上是一样的,唯一的区别就是用一个字段标示出了这个文件是EXE文件还是DLL文件
64位不过就是把32位的字段拓展成64位,也叫PE32+文件,没本质区别。
PE文件的定义基本都在“我的电脑”——下的“winnt.h”头文件中。
而在这个“winnt.h”文件中的image format下就是系统定义的PE结构:
我们需要能看懂这张图片:(桌面)
Pe文件是一个平面地址结构,所有代码和数据都被合并在一起,组成一个很大的结构。
文件的内容被分为不同的区块(堆、栈……),各个区块按照页边界来对齐,区块没有大小限制,是一个连续的结构(区块 = 很多连续的页合起来)。
PE文件被放在硬盘中的时候,按照那张图一样放着,但是使用pe文件的时候要映射到内存中,注意映射过来之后不是100%格式没变,在硬盘中啥样在内存中还是啥样。而是由一个叫做PE装载器的东西决定哪里要映射哪里不要映射。一般文件较高偏移位置也要映射到较高的内存地址。
映射关系如下:
我们在使用(调用)一个pe文件的时候必须抓住它的基地址,一般使用函数:
HMODULE GETModuleHandle (LPCTSTR IpModuleName)
返回一个句柄指针(基地址编号,可以理解为基地址,指向这个基地址的指针) , 一个pe文件可以视为一个模块,参数是指向这个pe文件(模块)名称的指针。
小甲鱼PE详解之IMAGE_DOS_HEADER结构定义即各个属性的作用(PE详解01)_fish_c的博客-CSDN博客
小甲鱼PE详解之IMAGE_NT_HEADERS结构定义即各个属性的作用(PE详解02)_fish_c的博客-CSDN博客
PE文件头(Image_NT_Header)挨着DOS stub,在执行pe文件的时候,pe装载器将从IMAge_DOS_Header结构体中的elfanew中找到pe头的起始偏移量,加上基地址就可以得到pe头文件的指针。
就拿上图来说:00h + D0h = D0h pe头指针 = IMage 基址 + dos_header ->elfanew
从这里开始看到的是NT部分(pe头部分):
一开始是signature 5045表示是pe头标记(P/E的anscii码)
然后是文件头file_header:这个结构体包括:
机器平台(CPU类型):014c说明这是x86平台的CPU
这个EXE文件拥有的区块表数目:4块
文件何时被创建:
可选文件头大小:一般32位是00 E0,64位是00F0,决定了后面的可选文件头大小多大
文件信息标志,文件属性:012F , 这个需要或运算结合微软的表格来。
接下来是可选映像头部,
小甲鱼PE详解之IMAGE_OPTIONAL_HEADER32 结构定义即各个属性的作用(PE详解03)_fish_c的博客-CSDN博客
特别大一块,
这个地方记着不可以背,只是工具书!
Address of entry point,
Image base :
对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被**模块占据,所以EXE总是能够按照这个地址装入,这也意味着EXE 文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被**的DLL使用,所以 DLL文件中必须包含重定位信息以防万一。因此,在前面介绍的 IMAGE_FILE_HEADER 结构的 Characteristics 字段中,DLL 文件对应的 IMAGE_FILE_RELOCS_STRIPPED 位总是为0,而EXE文件的这个标志位总是为1。
在链接的时候,可以通过对link.exe指定/base:address选项来自定义优先装入地址,如果不指定这个选项的话,一般EXE文件的默认优先装入地址被定为00400000h,而DLL文件的默认优先装入地址被定为10000000h。
数据块如果可以一致,那就是“兼容”。
小甲鱼PE详解之区块表(节表)和区块(节)续(PE详解05)_fish_c的博客-CSDN博客
一般被感染之后会有个标志位记为1.
内存中以1000h对齐,磁盘中以200h对齐。
块名:
内存中块大小:
文件中(磁盘中)块的大小:3000
文件中的偏移:1000
重要的就只有前5个
还有块属性
小甲鱼PE详解之区块描述、对齐值以及RVA详解(PE详解06)_fish_c的博客-CSDN博客
有一个一定要会:
假如我们现在要找xmind这个EXE文件的输入表的RVA对应的真实磁盘地址:
rva地址是410C。
找到在rdata这个区块里面。算出相对于这个区块的RVA2是:410C - 4000 = 10C,再找出文件块中偏移地址:
加起来:410c 。
小甲鱼PE详解之输入表(导入表)详解(PE详解07)_fish_c的博客-CSDN博客
我们跟着小甲鱼做了一个实验,我认为很有必要做这个实验。。。
我们一开始无脑找到了massgebox函数(这是一个导入函数/输入函数)的来自的dll(它来自user。32.dll)的名称。用的是IDA静态分析。
PE装载器的核心操作是重写输入函数的真实地址
我们实验第一步:先根据“数据目录表”找到我们需要查找的EXE文件(hello.exe)文件的导入表(输入表)的磁盘位置:
导入表的相对虚拟地址是2A000,大小003C,我们对比块(节section)段可以得知导入表处在内存的相对虚拟地址的。Data节中,偏差为2A000-2A000 = 0.对应的磁盘地址(文件地址)是28000+0=28000。
接下来我们要去输入表的起始位置找5个双字,对应上图的结构。
记住,我们在没有运行这个程序(hello.exe)之前,它所需要的输入函数只是一个名称而已,不具备真正的函数入口,换言之他并没有成功地动态链接所需要的user32.dll进入内存。所以这个时候我们看这幅图:
无论是从INT还是IAT,我们都可以找到需要的函数:
也可以反推出来我们用IDA看到的地址其实是:
内存中,相对于我们要调用的user32.dll函数的IMAGE_THUNK_DATA的虚拟地址
如果我们需要查看程序执行之后的EXE文件(已经完成user32.dll的动态链接)的内存情况:
这幅图:
我们需要用loardPE dump一下(任何一款动态分析软件都可以):
右键完整转存。
同样的方式找到IAT中真正的massagebox地址:
7710002E就是真正的函数地址。但是这个地址不在内存里面,也不在我们的hello。Exe里面,而是在磁盘里面。所以我们用一般软件看不到。
当PE 文件被执行的时候,Windows 加载器将文件装入内存并将导入表(Export Table) 登记的动态链接库(一般是DLL 格式)文件一并装入地址空间,再根据DLL 文件中的函数导出信息对被执行文件的IAT 进行修正。
那导出表是干啥用的呢? 导出表就是记载着动态链接库的一些导出信息。通过导出表,DLL 文件可以向系统提供导出函数的名称、序号和入口地址等信息,比便Windows 加载器通过这些信息来完成动态连接的整个过程。
小甲鱼PE详解之输入表(导出表)详解(PE详解09)_fish_c的博客-CSDN博客
我现在觉得是:
加载过程重定位——需要某个函数的时候执行的时候,从输入表中可以看到真正需要这个函数(比如我们在hello.exe中需要messagebox函数,)的入口地址是什么,但是这个函数被我们封装在user32.dll下。——我们是先从磁盘中把整个user32.dll动态链接到hello。Exe程序的内存空间中去——然后再根据user32.dll的导出表export去找到具体需要的函数messagebox的入口地址(有两种方法:1、从序号找,2、从函数名称找)
例题分析:count。Dll文件中的export表中的函数
- 找到count。Dll的export表格
- 找到export对应的文件地址:
2060 - 2000 = 60 ;60+600=660
- 在010里面查看660H的内容(export内容)
我们可以对照这个结构来看:
IMAGE_EXPORT_DIRECTORY STRUCT
Characteristics DWORD ? ; 未使用,总是定义为0
TimeDateStamp DWORD ? ; 文件生成时间 (这个例子里面是3CAC7619)
MajorVersion WORD ? ; 未使用,总是定义为0
MinorVersion WORD ? ; 未使用,总是定义为0
Name DWORD ? ; 模块的真实名称 (动态链接库的名称在209C)
Base DWORD ? ; 基数,加上序数就是函数地址数组的索引值 base=1
NumberOfFunctions DWORD ? ; 导出函数的总数 这个dll有2个函数
NumberOfNames DWORD ? ; 以名称方式导出的函数的总数 同上行
AddressOfFunctions DWORD ? ; 指向输出函数地址的RVA 2088
AddressOfNames DWORD ? ; 指向输出函数名字的RVA 2090
AddressOfNameOrdinals DWORD ? ; 指向输出函数序号的RVA 2098
Count.dll |
1 |
2 |
2 |
2088 |
2090 |
2098 |
1. 从序号查找函数入口地址
下边小甲鱼带大家来模拟一下Windows 装载器查找导出函数入口地址的整个过程。如果已知函数的导出序号,如何得到函数的入口地址呢 ?
Windows 装载器的工作步骤如下:
- 定位到PE 文件头
- 从PE 文件头中的 IMAGE_OPTIONAL_HEADER32 结构中取出数据目录表,并从第一个数据目录中得到导出表的RVA
- 从导出表的 Base 字段得到起始序号
- 将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引
- 检测索引值是否大于导出表的 NumberOfFunctions 字段的值,如果大于后者的话,说明输入的序号是无效的
- 用这个索引值在 AddressOfFunctions 字段指向的导出函数入口地址表中取出相应的项目,这就是函数入口地址的RVA 值,当函数被装入内存的时候,这个RVA 值加上模块实际装入的基地址,就得到了函数真正的入口地址
2. 从函数名称查找入口地址
如果已知函数的名称,如何得到函数的入口地址呢?与使用序号来获取入口地址相比,这个过程要相对复杂一点!
Windows 装载器的工作步骤如下:
- 最初的步骤是一样的,那就是首先得到导出表的地址
- 从导出表的 NumberOfNames 字段得到已命名函数的总数,并以这个数字作为循环的次数来构造一个循环
- 从 AddressOfNames 字段指向得到的函数名称地址表的第一项开始,在循环中将每一项定义的函数名与要查找的函数名相比较,如果没有任何一个函数名是符合的,表示文件中没有指定名称的函数
- 如果某一项定义的函数名与要查找的函数名符合,那么记下这个函数名在字符串地址表中的索引值,然后在 AddressOfNamesOrdinals 指向的数组中以同样的索引值取出数组项的值,我们这里假设这个值是x
- 最后,以 x 值作为索引值,在 AddressOfFunctions 字段指向的函数入口地址表中获取的 RVA 就是函数的入口地址
一帮情况下病毒程序就是通过函数名称查找入口地址的,因为病毒程序作为一段额外的代码被附加到可执行文件中的,如果病毒代码中用到某些 API 的话,这些 API 的地址不可能在宿主文件的导出表中为病毒代码准备好。因此只能通过在内存中动态查找的方法来实现获取API 的地址。关于病毒代码具体的实现分析,小甲鱼在今后将跟大家共同研究讨论这个话题~
摘自《小甲鱼——-——————PE详解之输入表》
九、基址重定位
重新定义位置
但凡直接寻址,必然需要重定位:
直接寻址方式:
指令的地址码部分直接给出的不是操作数,而是操作数的存储器地址,这种方式称为直接寻址方式。
换句话说,机器字节码和偏移地址相同的,就是直接寻址。
我们还是拿上边那张图片来说事儿,我们说了上边的那些指令需要重定位,现在就假设重定位后的基地址由原来的10000000h 变为 20000000h了,那么类似这样的语句:inc dword ptr [10003000] 应该改成 inc dword ptr [20003000] 。
注意,重定位的算法我们可以总结为:将直接寻址指令中的双字地址加上模块的实际装入地址与模块建议装入地址之差。
我们建议装入的地址,也就是我们原先没有重定位过的基地址:
在PE头中已经定义过:
实际装入的地址我们不知道,但是可以从base relocation table中寻找。
那么重定位表格中到底是什么呢?
也就是存放着————“需要被修改的那些汇编代码的地址”
重定位表的结构如下:base relocation table
Virtual address 1000
Sizeofblock 18
Typeoffset
VirtualAddress 是 Base Relocation Table 的位置它是一个 RVA 值;
SizeOfBlock 是 Base Relocation Table 的大小;
TypeOffset 是一个数组,数组每项大小为两个字节(16位),它由高 4位和低 12位组成,高 4位代表重定位类型,低 12位是重定位地址,它与 VirtualAddress 相加即是指向PE 映像中需要修改的那个代码的地址。
从sizeofblock就可以算出来有多少个typeoffset(有多少个需要被重定向的语句):18-8(virtual address占8个字节) = 10H /2 (typeoffset每一项是2个字节) = 8H (可是只有7项,为何?因为最后的00000是一项结尾项,用来诠释typeoffset终结了)
我现在觉得是:
- 需要某个函数的时候执行的时候,从输入表中可以看到真正需要这个函数(比如我们在hello.exe中需要messagebox函数,)的入口地址是什么,但是这个函数被我们封装在user32.dll下。
- 我们是先从磁盘中把整个user3dll动态链接到hello。Exe程序的内存空间中去
- 然后再根据user32.dll的导出表export去找到具体需要的函数messagebox的入口地址(有两种方法:1、从序号找,2、从函数名称找)
- 执行这个messagebox函数的每一条汇编指令,可是问题来了,代码我们原先写在磁盘里面,相对地址偏移都是磁盘里面的,但凡涉及直接寻址的指令(比如:inc dword ptr【10003000】,这个时候的10003000并不是程序被加载(或者说映射)到内存之后的偏移了,相对寻址不用改是因为相对偏移在映射之后不变)都要重定位。
我们回到上图:
就拿第一个typeoffset 3028来说,3是最高位表示类型,后面的028加上virtual address1000 =1028 ,这个1028就是pe映像中(即程序被加载进内存中后)要修改的那个代码 Inc dword ptr【10003000】的地址,可以用IDA验证结论:
从1028开始就是要改的东西了(要改的其实只有10003000,inc不用改)
十、资源(汉化、改图标等)
资源一般指的是加速器图标等、窗口、界面、菜单、……………………
资源是“树”结构。
接下来以QQ为例子分析一下这个资源:
文件对齐值和内存对齐值一样的话,都是以1000为一页,这样的话就不会有额外的偏移了,找到资源表入口:6000
IMAGE_RESOURCE_DIRECTORY STRUCT
Characteristics DWORD ? 0 ;理论上为资源的属性,不过事实上总是0
TimeDateStamp DWORD ? ;资源的产生时刻0000 0000
MajorVersion WORD ? 0000 ;理论上为资源的版本,不过事实上总是0
MinorVersion WORD ? 0000
NumberOfNamedEntries WORD ? ;以名称(字符串)命名的入口数量0000
NumberOfIdEntries WORD ? ;以ID(整型数字)命名的入口数量0003
IMAGE_RESOURCE_DIRECTORY ENDS
资源属性为0000 0000
产生时刻0000 0000 版本 0000 和0000
0000+0003 = 0003 所以一共有3个“小跟班”:然后我们接下来看“入口”的结构继续分析:
IMAGE_RESOURCE_DIRECTORY_ENTRY STRUCT1
Name DWORD 0000 0003(图标资源类型) ? ;目录项的名称字符串指针或ID
OffsetToData DWORD 8000 0028? ;目录项指针
IMAGE_RESOURCE_DIRECTORY_ENTRY ENDS
IMAGE_RESOURCE_DIRECTORY_ENTRY STRUCT2
Name DWORD 0000 000E(版本信息) ? ;目录项的名称字符串指针或ID
OffsetToData DWORD 8000 0070 ? ;目录项指针
IMAGE_RESOURCE_DIRECTORY_ENTRY ENDS
IMAGE_RESOURCE_DIRECTORY_ENTRY STRUCT3
Name DWORD ? 0000 0018(自定义类型) ;目录项的名称字符串指针或ID
OffsetToData DWORD 8000 0088 ? ;目录项指针
IMAGE_RESOURCE_DIRECTORY_ENTRY ENDS
资源directory |
资源directory_entry1图标80000028 |
Directory—entry2 版本信息70 |
Directory——entry3 自定义类型88 |