修改PE导入表注入DLL——实例图文教程——让你看的明明白白

一、前言

其实通过修改PE导入表注入DLL的教程很多,本文也只是其中的沧海一粟而已,但既然写出来,自我感觉应该还是有一点自我的东西的,至少自认为做到了思路清晰,每步都有据可依,让看客应该能做到“看的明明白白”!

本贴以《英雄无敌》1游戏程序为例子,向其添加一个DLL,调用其中的导出函数可以在施展回城术魔法时,让玩家可以选择到达具体的城堡。本文的最后效果与下文是一样的,只是处理手法不同而已!
https://www.cnblogs.com/dark-f/p/18450877

本贴内容如下:

。PE基础知识
。查看导入表是否有足够的空间
。移动导入表
。修改导入表的值
。删除绑定导入表(BOUND IMPORT TABLE)
。创建新导入表
。设置INT,Name,IAT的值
。修改IAT相关节区的属性
。效果检测
。调用导入函数
。游戏测试
。资源下载

使用的工具:

。PEview——PE文件查看软件
。CFF Explorer——PE文件查看修改软件
。010Editor——hex文件编辑软件
。OD或者X32DBG——逆向调式软件

二、PE基础知识

既然你都想动一动导入表了,那自然得了解一点PE的基础知识。
首先得明白,使用hex编辑软件打开程序与OD调式软件打开程序,所看到的地址是不同的。
这是hex编辑软件(010Editor)打开的样子

这是OD类调式软件(X32DBG)打开的样子

基础知识一:FOA和RVA

一眼就可以看出,两者每一行前面的数字是不同的。hex编辑软件是从00开始的,而OD类调式软件一般都是从004XXXXX开始的。这就是pe基础知识中的第一个要点:区别FOA和RVA。
FOA:程序在(磁盘)文件上的地址,hex编辑软件显示的就是FOA;
RVA:程序导入内存后的地址,OD类调式软件显示的就是RVA(实际地址还要加上Image Base)。
因此pe文件具有两种状态,它们的地址分别用FOA和RVA表示,并且一般情况下两者是不同的,但都代表着程序中的同一个地址。关键看你要在哪里修改程序!若要用hex编辑软件修改程序,则你必须知道FOA是多少;同样,若用OD类调式软件修改程序,则你必须要知道RVA的大小。下图显示了pe这两种状态出现的原因:

基础知识二:PE头格式

要搞PE修改,则大致要知道DOS头、PE头、标准pe头和扩展pe头。下图显示了《英雄无敌》1的这几个头的位置。

DOS头:前面4行(每行16字节),就看最后一个DW(DWORD),这里的数值就是pe头的位置,上图中是80,说明pe头在80位置。
PE头:它由三部分组成:PE标识(1个DW),标准pe头(5个DW,也就是一行再加1DW)和扩展pe头(大小为E0)。例如上图中,扩展pe头是从98开始的,加上E0,等于178,说明扩展pe头最后一个DW位置是174。前面说过,这些数值都是FOA,但由于包含区块表在内,pe文件直至区块表结尾,FOA与RVA是一样的。
其中,导入表信息位于从扩展pe头结尾倒数第15个双DW(即每2DW一数,倒着数到15,里面的地址是RVA),上图显示《英雄无敌》1的导入表RVA=D6000,大小等于C8。这个结果,也可以通过PEview来观察。
用PEview打开程序,左栏选择点开Selection .idata,选导入表,右边就能看到这个程序导入表从哪里开始,包含多少DLL。工具栏中还可以调整是FOA显示还是RVA显示,如图:

上图中看到的是导入表开始位置正是RVA=D6000,与前面010Editor中看到的一样。那么要知道它的开始位置FOA是多少,只要选择显示FOA即可,如图:

说明导入表起始位置的FOA=A2600,而RVA=D6000,即用010Editor打开它在A2600位置就是导入表,而用OD打开它,内存里在4D6000位置。
如果查看.idata的区块结构信息,就会从中看到:FOA(A2600),RVA(D6000),文件对齐后大小(1600),加载到内存中的映像大小(1480),如图

PE-DLL-exan-7

基础知识三:导入表结构

既然用到导入表,自然要对导入表的数据格式有个大致印象。
导入表是由IMAGE_IMPORT_DESCRIPTOR结构的数组组成,简称IID,没有特定的成员指出IID项数,但是会由全为0的IID结构作为结束。
IID结构的字段成员如下,其中OriginalFirstThunk、Name以及FirstThunk成员是我们添加DLL文件的关键。

IMAGE_IMPORT_DESCRIPTOR{
  union{
	  characteristics DWORD
	  OriginalFirstThunk DWORD //指向IMAGE_THUNK_DATA结构的数组
  }
  TimeDateStamp DWORD 	//时间标志,不用管,填0
  ForwarderChain DWORD 	//一般为0
  Name DWORD 				//指向DLL名称的指针
  FirstThunk DWORD		//指向IMAGE_THUNK_DATA结构的数组
};IMAGE_IMPORT_DESCRIPTOR

在PE文件尚未执行过时,OriginalFirstThunk与FirstThunk字段指向相同的结构,区别在于OriginalFirstThunk不可以重写,而FirstThunk可以被重写,当PE文件执行后FirstThunk指向的结构会用于存放导入函数的真实地址。因此我们修改时将OriginalFirstThunk与FirstThunk字段指向同个地址即指向IMAGE_THUNK_DATA结构的数组。而Name字段存放的是指向DLL文件名称的指针。

ypedef struct _IMAGE_THUNK_DATA32 {
  union {
    PBYTE ForwarderString;
    PDWORD Function;
    DWORD Ordinal;
    PIMAGE_IMPORT_BY_NAME AddressOfData; 	//指向IMAGE_IMPORT_BY_NAME
  } u1;
} IMAGE_THUNK_DATA32;

IMAGE_THUNK_DATA结构在不同情况下的成员不同,但是重点关注AddresOfData字段即可,该字段指向IMAGE_IMPORT_BY_NAME结构,该结构记录是导入函数的名称。

IMAGE_IMPORT_BY_NAME STRUCT{
  Hint WORD //忽略设置为0
  Name BYTE //导入函数名称
};IMAGE_IMPORT_BY_NAME

IMAGE_IMPORT_BY_NAME结构前面两位为0时,表示函数以字符串类型的函数名方式输入。因此构造时也就是将Hint设为0,后面跟着的字节为导入函数的名称。
上面这些,可能有朋友一看就头大了,不怕,用《英雄无敌》1的具体例子看就会清楚得多。上面已经知道导入表的FOA在A2600了,在010Editor里,向下拖动鼠标来到A2600处,如图

前面说了,这个导入表的大小是C8,看从A2600开始到A26C4结束,就是全部导入表的内容。那么这里有多少个需要导入的DLL呢?每5个DW一数,看看有多少个不全是0的5个DW,那就表示有多少个DLL需要导入,是不是很清楚?上图中看出是9个,这个结果可以从PEview中看到(不给图了),正好有9个DLL需要导入。还可以看到导入表中每5个DW是这样的,第一个DW不为0,第二和第三为0,第四和第五又不为0。这里第一个不为0的DW就是OriginalFirstThunk(什么东西,别管他);第四个DW就是Name,注释是DLL的名称指针;第五个DW就是FirstThunk(又是什么,也别管他)。虽然说不管他,但是要知道它们做什么,总之一句话,就是这里的2个数都是指针,指向的地址里又是一个指向IMAGE_IMPORT_BY_NAME结构的指针。够绕的,但是别心急不明白,马上就知道到底是什么了!
先看上面IMAGE_IMPORT_BY_NAME结构的成员就知道了,它只有2个成员,前面一个WORD(2字节),一般都是不用管的,即取0就行了,而后面跟着的就是DLL中需要导出函数的名称。原来所谓的IMAGE_IMPORT_BY_NAME结构,以《英雄无敌》1添加的Towngate.dll来说,需要导出的函数名称为GetTown,那就是要在文件中建立一个这样的数据00 00 47 65 74 54 6F 77 6E 00 ,它的首地址(RVA)就是指向IMAGE_IMPORT_BY_NAME结构的指针啦。也就是说,上面第一个和第五个DW指向的地址里就填这个RVA。而第一个和第五个DW里就是另外的一个数(RVA),这两个数指向的地址里都装着前面那个RVA。还没有明白吗?不着急,后面有具体的数据,能帮助你明白的。
至于第四个DW里填写什么?最简单啦,只要填上指向DLL名称字符串的首地址(RVA)就行了。
好,有了这些知识,足够来手动注入DLL了。

三、查看导入表是否有足够的空间

实际上修改导入表注入DLL,如果不想手动修改的话,很多PE查看修改软件都是可以直接帮你插入的,例如使用CFF Explorer,就可以直接插入DLL。用CFF Explorer打开要修改的程序(《英雄无敌》1),左侧任务栏里选Import Adder(导入表添加),右边上面点Add(添加)按钮,再选择要添加的DLL文件,软件会自动找到要添加的导出函数,如图

确认是要添加的函数后,点击Rebuild Import Table(重建导入表)按钮后,就会将DLL注入到程序中。

这样做的好处是在你即使不是很明白PE文件结构的情况下,也能方便地注入DLL。只是,工具帮你把DLL注入到程序中什么位置,你可能就不清楚了,当然你可以通过PE查看软件或者hex比较软件,都可以查出究竟注入到程序中什么位置。而且如果你不搞清楚注入到程序什么位置,那么以后调用函数时的使用方式将会不同,这往往会引起重定位表的调整。所以,软件帮你解决了注入的问题,却也给你添加了调用的难度,这正与那句“老外流行的:上帝给你关上了一道门,也必定给你开了一扇窗户”有异曲同工之妙。本文将采用手动注入的方式,所以要自己动手添加导入表。
首先在PEview中可以看到导入表的最后在哪里?在A26C4。

在010Editor里,向下拖动鼠标来到A26C4处,如图

去那里看看是否有插入自己DLL所需的空间,这个空间是5个DW。如果在导入表的最后少于10个DW的00字节,则没有足够的空间插入字节DLL所需的空间,多于则有足够空间。本例中,在A26C4后面从A26C8到A26DC正好还有DWORD[5]=0,说明空间刚好够用。
虽然本例就可以直接在这个后面添加一个导入表,但这样不具备一般性,因此,这里也演示一下如果空间不够怎么处理。
如果没有足够的空间来添加其他的DLL文件,则要考虑将整个导入表迁到别的地址。

四、移动导入表

有三种方式把导入表迁到别的地址,分别是:
1 查找文件中的空白区域
2 增加文件最后一个节区的大小
3 在文件末尾添加新的节区
一般情况下,由于文件对齐的需要,区块之间都会存在大量的由0填充的空白区域,因此,上面说的三种方法中,最常用的还是利用文件中的空白区域。本文就采用第一种方法找空白区域,演示一下怎么迁移导入表的操作。
实际上,在A2600后面的A3800处,就有大片的空白区域,可以把导入表迁移到那里去。原导入表大小是C8,现在要添加一个DLL文件则要加14,即至少需要C8+14=DC大小的空间来安排导入表。
查看这个区域是否可用?该区块内存大小1480,而文件中实际大小是1600,还有180的富余空间,这个区域是可以使用的(看前面PEview那个图)。那么就选择在A3A90这个位置创建导入表。

五、修改导入表的值

原RVA=D6000计算一下移到的新地址RVA,原FOA=A2600,新的FOA=A3A90,偏差Δ=A3A90-A2600=1490。因此新的RVA=D6000+1490=D7490,而大小为C8+14=DC。

用010Editor来修改

那么从现在开始导入表就是位于D7490,大小是DC了。

六、删除绑定导入表(BOUND IMPORT TABLE)

BOUND IMPORT TABLE(绑定导入表)是一种提高DLL加载速度的技术,本例的绑定导入表信息

若想正常导入自己的dll,需要向绑定导入表添加信息,但是该导入表是个可选项,不是必须存在的,可以删除(修改其值为0),当前实例中绑定导入表的值是0,所以也就不需要修改了。

七、创建新导入表

先将原先RVA=D6000(FOA=A2600)位置的导入表复制到RVA=D7490(FOA=A3A90)位置。先选中要复制的内容,右键选择复制:

来到A3A90处,选中同样多的字节,右键选择粘贴

这样原导入表就移到新的地址了。

八、设置INT,NAME,IAT的值

在PE文件尚未执行过时,OriginalFirstThunk(INT)与FirstThunk(IAT)字段指向相同的结构,区别在于OriginalFirstThunk不可以重写,而FirstThunk可以被重写,当PE文件执行后FirstThunk指向的结构会用于存放导入函数的真实地址。
基础知识里已经分析了INT、NAME和IAT中应该怎样添加数值了,下面就具体过程演示。
前面已经说了,要在文件中建立一个这样的数据00 00 47 65 74 54 6F 77 6E 00,共需要10个字节。可以把它放在A3BF0(FOA),它的RVA=A3BF0-A2600+D6000=D75F0。这样第一个DW和第五个DW指向的地址里就可以填写D75F0了。
其次,再把NAME字段存放的是指向DLL文件名称的指针搞好。DLL文件名称是Towngate.dll,即54 6F 77 6E 67 61 74 65 2E 64 6C 6C 00,共13个字节,可以把它安排在A3BD0,即RVA=A3BD0-A2600+D6000=D75D0。也就是说,第四个DW里就填写D75D0了。
最后,只要把上面的要填写的D75F0放在哪2个地址里?就把这2个地址写到第一个DW(INT)和第五个DW(IAT)里即可。
本例中,这2个地址分别设在A3BC0和A3BE0,它们里面都填写D75F0,它们的RVA分别计算下:第一个DW(INT)的RVA=A3BC0-A2600+D6000=D75C0和第五个DW(IAT)的RVA=D75E0,这2个数据分别填写入第一个DW和第五个DW中。至此,全部数据搞好了,就可以设置自己DLL的导入表数据了。
按照上面的计算结果,在A3B44的地方填入:C0 75 0D 00,隔2个DW,A3B50的地方填入:D0 75 0D 00,A3B54的地方填入:E0 75 0D 00,结果如图所示:

本例中导入函数只有1个,如果,导入函数多于1个呢?那这些数据又是怎样的?实际上,看一下原有程序中已有的DLL导入多于1个函数的情况就明白了。以《英雄无敌》1中kernel32.dll为例,它的导入函数是很多的,看它就足够了。下图就是它的导入表信息

可以看到,它的INT位于A2614,其RVA为D6124,即Δ=124,所以FOA=A2600+124=A2724,即INT从A2724开始,直到一个DW全为0为止,有多少DW即有多少个导入函数。如图

可以看到,IAT中的数据与INT的是一样的。

九、修改IAT相关节区的属性

加载PE文件到内存时,PE装载器会修改IAT,写入函数的实际地址,所以你将IAT写入到了哪个区块,那么相应的区块头里定义的区块属性必须拥有WRITE(可写)属性。
导入表所在区块是A2600,再加上1600的大小,即到A3C00,都是导入表的范围,导入表自然是具有可写属性的,因此,本例中无需修改相关区块的属性。
但,如果导入表新移到的区块属性没有写属性时,就要将其改成具有写属性。

那如果导入表迁移到了一个没有写属性的区块怎么办呢?举例说明一下怎么修改。假设上面存放第五个DW(IAT)里的数据指向的地址不是RVA=D75E0,而是一个别的数据(实际上是地址),那么先要清楚这个地址是在哪个区块里(例如:《英雄无敌》1里共用6个区块,分别是:.text、.data、.rdata、.idata、.rsrc和.reloc,假定那个地址是在.rdata区块)?则根据下图可知,该区块只有读属性,而没有写属性的

在010Editor中来到1C4处,将下图中的数值改为40 00 00 C0,那么该区块就具有读写属性了

至此,全部修改完成,可以看看程序加载后是否已经加载了这个DLL。

十、效果检测

这样修改能否成功加载DLL呢?可以用OD或者X32DBG类的调式软件打开修改后的程序,看看是否DLL已经加载?用X32DBG打开后,查看Symbols

可以看到towngate.dll和它的导出函数GetTown都已经正确加载,说明上面的修改是成功的(这里说明一下,实际修改时,导入表是不用迁移地址的,因为前面说了,原有导入表下正好有添加一个DLL的空白区域,所以下面显示的与上面迁移了导入表的结果在数据上有一点不同)。
在内存区查看第五个DW(IAT)指向的地址,按照前面的说法,在程序被导入内存后,那个地址会被导出函数的真正地址所取代,看看是否变化了?

上图显示,IAT里的数据本应与INT里的数据是一样的,但是现在它被改写了,而且被改写的地址也变成红色了,即这里就是导出函数的真正地址。
由于它是被程序载入内存时由PE系统自动修改的,代表了该函数的真正地址,因此,不管是否存在重定位问题,这个地址都是无需重定位的,所以可以无需重定位它而可以直接调用,下面的调用正是利用这个地址而无惧重定位问题!

十一、调用导入函数

调用函数的方法很简单,就是先把第五个DW(IAT)指向的那个地址XXXXX里的值赋给eax,然后call这个eax就行了,即

mov eax,dword ptr ds:[XXXXX]
call eax

由于本文是在前一篇文章基础上修改的,具体修改程序的说明就不再阐述了,结果如下图

再把原程序中的4351D9的那行跳转改成跳到修改的地方即可

具体是否有效,可以游戏检测一下效果。

十二、游戏测试

打开游戏,让英雄使用回城术

出现了DLL中函数调用结果,选择1

该英雄回到第一个城堡

说明这样修改、这样调用导出函数都是没有问题的。

十三、资源下载

修改后的程序和dll下载地址:
https://wwzd.lanzoup.com/iXoNc2dy469e

posted @ 2024-11-02 04:41  dark-f  阅读(38)  评论(0编辑  收藏  举报