在可选PE头的最后部分拥有16个数据目录表,其结构如下
typedef struct _IMAGE_DATA_DIRECTORY {
这16个表是指PE文件中描述各种数据结构的数据目录表,具体如下:
导出表(Export Table)
导入表(Import Table)
资源表(Resource Table)
异常处理表(Exception Table)
安全相关表(Certificate Table)
重定位表(Base Relocation Table)
调试信息表(Debugging Information Table)
版权信息表(Architecture-Specific Data Table)
全局指针寄存器表(Global Pointer Register Table)
TLS表(Thread Local Storage Table)
负载配置表(Load Configuration Table)
网络地址转换表(Bound Import Table)
导出地址表(Import Address Table)
延迟加载导入表(Delay Import Table)
COM运行时描述表(COM Runtime Descriptor Table)
保留(Reserved)
其中导入表,导出表,重定位表,IAT表等表比较重要
===
静态链接库的生成与使用
新建项目时选择静态链接库
cpp文件
int Plus(int x,int y)
头文件
#pragma once
新建项目将生成的lib文件和头文件复制到新项目中
引用即可
但是这种静态链接库是直接加载到了程序中,没有实现模块化
动态链接库的生成与使用
头文件
extern "C" _declspec(dllexport) int Plus (int x,int y);
cpp文件
int Plus(int x,int y)
将生成的lib和dll文件放入到新建项目中进行导入
#pragma comment(lib,"dllmy.lib")
已经在OD发现我们的自定义dll了
这16个数据目录表所描述的各种数据结构,实际上是存储在文件的不同区段(节表)中的,根据上面的数据目录表结构VirtualAddress是内存地址,那如何定位在文件中的地址呢
根据软件查看导入表的RVA为2D0C0
通过查看节表发现第二个区块的
VirtualAddress+VirtualSize是大于导入表的RVA,因此导入表是包含在第二个区块内的
因此导入表的RVA-区块的RVA得出导入表相对于区块的位置,然后加上区块的PointerToRawData也就是导入表在区块中的实际位置
导出表结构:
typedef struct _IMAGE_EXPORT_DIRECTORY {
pe结构的导出方式一共分为两种,一种是名字导出,一种是序号导出
名字导出:
如图为:
AddressOfFunctions,AddressOfNameOrdinals,AddressOfNames的表结构
其中AddressOfFunctions存的是函数的地址
AddressOfNameOrdinals为序号表
AddressOfNames中存的是函数名称的地址
假如我们要导出test函数,那么计算机首先会向AddressOfNames的地址进行遍历,如果遍历到了并且发现在下标为2遍历到的,那么就会去寻找AddressOfNameOrdinals下标为2存储的序号,发现为4,然后去AddressOfFunctions寻找下标为4的地址,这个地址就是函数的真正地址
序号导出:
根据序号导出和AddressOfNameOrdinals没有关系,如果给出的值为5,那么真正的地址位置就是5-base,之后去AddressOfFunctions寻找下标即可
注意:导出时要进行RVA转FOA的转换
编写程序打印所有的导出表信息:
#include <iostream>
定义:记录需要绝对地址修正的表,大多数绝对地址如果imagebase变化的话就无法使用,需要修正程序所调用的那些绝对地址。
修正方法:需要重定位的地址 + 偏移(当前基址 - PE的基址)
开了随机基址的程序才需要重定位,而DLL通常都有重定位表,因为不一定能够加载到DLL指定的ImageBase上。
OS如何判定是否重定位?
先查看随机地址标志,标志开启,地址重定位
再查看数据目录项 5 是否位NULL,不为NULL,基址重定位。
我们现在都是玩固定基址的PE,随机基址涉及到要修代码,如果有重定位信息,就可以在内存中随便申请一块内存,把代码放进去跑。
我们知道随机基址需要重定位表来修代码, 那么是修什么代码呢
实际上我们修的是使用绝对地址的代码,例如API的调用,通过IAT调用,这里就是使用的绝对地址,当模块基址改变时,原VA地址并没有保存API函数地址,所以就需要修正到正确的位置去获取API地址。
怎么保存需要重定位的数据地址呢?
按分页存,每个分页中需要重定位的地址(基于分页值的偏移)
优点:存储空间小,一个分页有多个地址需要重定位时,只需要存一个分页即可。2字节,和全是2 字节的偏移值。
00001000 0000 0024 0078
重定位表结构
重定位表描述待修复的值所在的地方,这个值是一个RVA。数据目录处的Size字段有用,是重定位表的总大小。
重定位表位于数据目录第3项。
重定位表结构(一项)
typedef struct _IMAGE_BASE_RELOCATION {
VirtualAddress
这个虚拟地址是一组重定位数据的开始RVA地址,只有重定位项的有效数据加上这个值才是重定位数据真正的RVA地址
SizeOfBlock
它是当前重定位块的总大小,因为VirtualAddress和SizeOfBlock都是4字节的,所以(SizeOfBlock - 8)才是该块所有重定位项的大小,(SizeOfBlock - 8) / 2就是该块所有重定位项的数目。
TypeOffset[1]
重定位项在该结构中没有体现出来,他的位置是紧挨着这个结构的,可以把他当作一个数组,宽度为2字节。表示该地址处有一个地址需要进行重定位
每一个重定位项分为两个部分:高4位和低12位
高4位表示了重定位数据的类型(0x00没有任何作用仅仅用作数据填充,为了4字节对齐。0x03表示这个数据是重定位数据,需要修正。0x0A出现在64位程序中,也是需要修正的地址)
低12位就是重定位数据相对于VirtualAddress的偏移,也就是上面所说的有效数据。之所以是12位,是因为12位的大小足够表示该块中的所有地址(每一个数据块表示一个页中的所有重定位数据,一个页的大小位0x1000)。
修正方法:被重定位处原来的地址 + 偏移(当前基址 - PE的基址)
:(VA - ImageBase) + NewImageBase
重定位表只是记录了修哪里,以及怎么修,并不会记录修多少,因为既然是随机基址,那么基址的值就不固定,必须每次软件起来才能确定。
注意:.reloc:的节一般用于存储重定位表,但是不作为定位重定位表的依据,应使用数据目录定位。
重定位表应用之 LoadDll
介绍:Dll 加载器 ,单独装载一个独立的DLL
返回值:返回模块的实例句柄
思考:
Dll的代码装载到哪个内存?申请一块内存,进行装载。
处理重定位数据,遍历重定位表
步骤:
申请Dll 装载所需的内存空间
拷贝PE头
根据节表拷贝节,对齐空隙使用 00 填充
拷贝节
处理导入表
处理重定位表
清理资源
返回模块句柄
处理重定位
获取分页内偏移数组地址 和 需要重定位的偏移个数
判断 TypeOffset 是否为填充 00。
是:跳过不处理,进行下一个
否:需要重定位,修正该地址处的重定位数据
GetprocAddress 从模块链里面找模块。TEB里面
DLL加载成功后,不能摸出DLL的MZ 和 PE 标志么?
不能。有的API调用的时候会检测这两个标志。
应用:API模拟,反dump
新的注入方式 LoadDll
不调用LoadLibary,使用远程注入的方式 DLL的内容注入到别人的进程里,然后调用DllMian
远程线程调用Dllmain。
===
PE(Portable Executable)结构是Windows操作系统下可执行文件的格式之一,导入表(Import Table)是PE结构中重要的一个部分。导入表记录了程序需要引用的外部库函数以及这些函数在内存中的位置,这些外部库函数通常由其他DLL文件提供。
假设我们汇编调用的是call CreateFile其本质是调用call CreateFile的函数地址那我么你在哪里存储这个函数地址呢?其实pe文件在导入DLL后会遍历DLL中的函数并且将函数的地址存放到另一个内存地址中,因此我们调用函数都是间接调用
我们用OD加载飞鸽传书 ALT+E跳转到可执行模块
选中feige.exe Ctrl+N查看调用的DLL函数
选择一个函数按回车键查看调用过这个函数的位置
双击进入,查看代码都是间接调用
复制这个地址在数据窗口Ctrl+G跟随,之后右键选择 长型》地址 就可以发现简介调用了这个函数
由于有两处函数调用我们查看另一处发现也是相同的地址
而存储函数地址的表叫做IAT(导入地址表)
IMAGE_IMPORT_DESCRIPTOR 是Windows操作系统中PE文件格式的结构体之一,用于描述动态链接库(DLL)的导入信息。
这个结构体在Windows SDK中的头文件winnt.h中被定义,可以在C/C++程序中进行引用。以下是该结构体的定义:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
该结构体定义了一个DLL的导入信息,包括以下几项:
OriginalFirstThunk: 指向导入函数名称表(Import Name Table,INT)的指针,这个表中存储了需要导入DLL中的函数名字。
TimeDateStamp:DLL文件的创建时间或重新绑定时间戳。
ForwarderChain:指向一个链表,记录该DLL导入的其他DLL列表(也就是链表的下一个DLL)。
Name:DLL的名称。
FirstThunk:指向导入地址表(Import Address Table,IAT)的指针,这个表中存储了需要导入DLL中的函数的地址。
在PE文件解析或修改中,可以通过 IMAGE_IMPORT_DESCRIPTOR 结构体读取或修改DLL的导入信息。
我们看一下OriginalFirstThunk指向的结构体
DLL函数的导入方式有两种,分别是序号导入和函数名称导入,如果DWORD的最高位是1是序号导入,低2个字节就是序号值
最高位是0是RVA,而RVA指向
PIMAGE_IMPORT_BY_NAME,其中Name[1]指向函数的名称
导入表遍历顺序 = 导入表加载的顺序
检查Name 和FirstThunk ,如果任一为NULL,则停止遍历
取FirstThunk 的项(数组中的元素),如果为NULL, 就取OriginalFirstThunk 对应的项,如果为NULL,则遍历下一项
判断项的最高位,如果为1,则取低WORD为序号,如果为0,则作为RVA 取出IMAGE_IMPORT_BY_NAME 中的函数名
循环遍历下一项
while(Name != NULL && FirstThunK != NULL)
把MessageBoxA当做常量还是能找到
#include <Windows.h>
但是如果换成数组就找不到了
何为绑定导入
一般情况下,在程序加载前IAT表和INT表中的内容相同,都是程序引用的dll中的函数的函数名或序号;
加载完成后IAT表中将替换为函数的真正地址;
但在加载前IAT表中直接写绝对地址是可以实现的;
加载前在IAT表中保存绝对地址的优点:
启动程序快;
在启动程序时需要:申请4gb内存空间、贴exe、贴dll、将IAT表修复为地址等等;
如果直接用绝对地址,则省去了修复IAT表的操作;
缺点:
dll重定位时,如果dll没能占据自身ImageBase处的地址,则需要修复绝对地址;
dll被修改时,dll被修改,IAT表中对应的函数地址可能被改,需要修复函数地址;
如何判断绑定导入
在导入表中结构中有个属性:TimeDateStamp;
该属性表示时间戳;
如果值为0则表示当前的dll的函数没有被绑定,在程序加载时会调用系统函数获取函数地址;
如果值为-1则表示当前的dll的函数已经绑定,而且绑定的时间存在另外一张表里;那张表就是绑定导入表;
绑定导入表的结构
typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
打印绑定导入表
#include "PE.h"
pe结构是学习二进制以及免杀必备的知识,不能只看理论要多练多敲才能掌握真谛。
往期推荐