一、前言
dll文件我们知道,它是不能自己运行的,只能被加载到其它进程中去执行代码。
而利用这个特性,我们就能够使用dll来完成注入操作,方法的原理为:
- 将我们的代码写在一个dll文件中
- 强迫目标进程加载我们的dll文件
一旦我们的dll文件被加载进入了目标进程,那我们自然可以为所欲为了!因为dll与它在一个进程内,你可以任意访问其内部的地址。
如果不用注入,虽然也能实现动态修改,但会更加麻烦:
- 使用
VirtualQueryEx
、VirtualProtectEx
函数查询、修改目标进程内存的权限。 - 使用
ReadProcressMemory
、WriteProcessMemory
函数读取、写入目标进程内存中的数据。
由于不在一个进程内,这些函数实际上都是在跨进程操作,并且这种方式想要让目标进程执行自己的代码非常麻烦,一般只会用于小块数据的修改。
而dll不一样,一旦被加载了,是可以执行我们自己的代码的:动态库与静态库。
dll文件也有一个入口函数,一旦被加载,就会执行该函数,我们就可以在里面写一些自己的代码来操作该进程。
二、dll注入方式
一般情况下,程序加载dll有以下三种方式:
- 进程创建时,会加载其输入表中dll,也就是静态输入。
- 进程主动调用
LoadLibrary
加载,也就是动态加载。 - 通过系统机制要求,比如输入法为什么能在任意程序中使用?这就是系统机制导致的,它允许某些特性功能的模块直接加载到目标进程中。
1.输入表
首先是第一种方式,静态输入,通过修改输入表完成。
当一个进程被创建后,它并不会直接到exe本身去执行代码,首先被执行的是ntdll.dll
中的LdrInitializeThunk
函数。
这个ntdll.dll
是一个非常重要的基础模块,前面也提到过,它并不会依赖于你的可执行文件是否导入,它在进程的创建阶段就会被自动映射到新进程中。
而这个LdrInitializeThunk
函数又会调用LdrpInitializeProcess
函数对进出做一些必要的初始化功能。
LdrpInitializeProcess
函数又会调用LdrpWalkImportDescriptor
对输入表进行处理,也就是加载输入表中的模块。
所以只要我们在输入表被处理之前对其进行干预,为输入表增加一个项目,使其指向要加载的目标dll,或者替换原输入表中的DLL并对其函数调用进行转发,那么就能实现加载dll的功能了!
简单起见,这里我自己写一个dll,在vs自动生成的代码中添加一个导出函数、并在被注入的时候弹出一个提示框即可:
然后生成32位的,下面我们的目标就是将这个生成的32位dll注入到前面那个TraceMe32.exe
程序中去。
输入表中每一项的结构体为:IMAGE_IMPORT_DESCRIPTOR
,简称为IID
,一个输入表本质上就是IID数组并且以0作为数组的结尾。
所以想要添加添加一个项,就得在这个数组最后写入我们的dll信息才行。
但由于一般编译器生成的PE结构内存用的会很紧凑,前面我们也已经见识过了,PE头的后面都是紧邻其它结构的,这里也一样,一般IID数组后面紧邻的结构是OriginalFirstThunk
和FirstThunk
,我们不能直接将其覆盖掉。
所以此时我们的想法就是直接移动这整个输入表的位置到其它空间更大的地方。
一个项的大小为20字节:
换算为16
进制就是14
,然后我们再看看原表的大小:
为168
,所以总的加起来我们就需要新地方有182
的大小才行,可以从节中找找看:
由于是修改映射之前的文件中去,所以要找上面两边对其后剩余空间的最小值,而RAW中布局非常紧凑,偏移地址+大小都等于下一个节的偏移,也就是各个节之间没有空隙,所以这样不行。
更加通用的情况下,其实也不用分析这些,我们直接扩展最后一节的大小不就行了吗?
首先从其PE头中可以看到,其文件对齐是200
,后面的是块对齐,也就是内存对齐为1000
:
记录一下当前最末尾的地址:
0x339000+0x224F4=0x35B4F4
0x331600+0x22600=0x353C00
然后还要计算一下我们的输入表的位置,从输入表的地址可以看出,它在.data
段内:
所以计算公式就是:段RVA-导入表RVA+段RAW,因为段内的数据相对于段开头的距离是不会变的:0x1C8A94-0x17B000+0x179600=0x1C7094
大小为0x168
,所以结束位置为0x1C7094+0x168=0x1C71FC