DLL注入新姿式:反射式DLL注入研究

在分析koadic渗透利器时,发现它有一个注入模块,其DLL注入实现方式和通常的注入方式不同。搜索了一下发现是由HarmanySecurity的Stephen Fewer提出的ReflectiveDLL Injection. 因为目前互联网上有关这个反射式DLL注入的分析并很少,也没有人分析其核心的ReflectiveLoader具体是怎么实现的,所以我就在这抛砖引玉了。数组

0×00 引言

常规的DLL注入方式相信你们都很熟悉了,利用CreateRemoteThread这一函数在目标进程中开始一个新的线程,这个线程执行系统的API函数LoadLibrary,以后DLL就被装载到目标进程中了。然而,因为这一技术被大量的恶意软件利用,各类安全对DLL注入这一块天然是严加看守,而常规的注入方式太过于套路化(CreateRemoteThread+ LoadLibrary),致使它十分容易被检测出来。同时,常规的DLL注入方式还须要目标DLL必须存在磁盘上,而文件一旦“落地”就也存在着被杀毒软件查杀的风险。缓存

所以我在这里介绍一种新的DLL注入方式,它不须要在文件系统存放目标DLL,减小了文件“落地”被删的风险。同时它不须要像常规的DLL注入方式那么套路,所以更容易经过杀软的行为检测。因为反射式注入方式并无经过LoadLibrary等API来完成DLL的装载,DLL并无在操做系统中”注册”本身的存在,所以用ProcessExplorer等软件也没法检测出进程加载了该DLL。安全

0×01 核心思路

咱们不想让DLL文件“落地”, 那咱们能够在磁盘上存放一份DLL的加密后的版本,而后将其解密以后储存在内存里。咱们而后能够用VirtualAlloc和WriteProcessMemory将DLL文件写入目标进程的虚拟空间中。然而,要”加载”一个DLL,咱们使用的LoadLibrary函数要求该DLL必须存在于文件系统中。这可怎么办呢。markdown

没错,咱们须要抛弃LoadLibrary,本身来实现整个装载过程!咱们能够为待注入的DLL添加一个导出函数,ReflectiveLoader,这个函数实现的功能就是装载它自身。那么咱们只须要将这个DLL文件写入目标进程的虚拟空间中,而后经过DLL的导出表找到这个ReflectiveLoader并调用它,咱们的任务就完成了。网络

因而,咱们的任务就转到了编写这个ReflectiveLoader上。因为ReflectiveLoader运行时所在的DLL尚未被装载,它在运行时会受到诸多的限制,例如没法正常使用全局变量等。并且,因为咱们没法确认咱们究竟将DLL文件写到目标进程哪一处虚拟空间上,因此咱们编写的ReflectiveLoader必须是地址无关的。也就是说,ReflectiveLoader中的代码不管处于虚拟空间的哪一个位置,它都必须能正确运行。这样的代码被咱们称为“地址无关代码”(position-independent code, PIC)。数据结构

0×02 注射器实现

要实现反射式注入DLL咱们须要两个部分,注射器和被注入的DLL。其中,被注入的DLL除了须要导出一个函数ReflectiveLoader来实现对自身的加载以外,其他部分能够正常编写源代码以及编译。而注射器部分只须要将被注入的DLL文件写入到目标进程,而后将控制权转交给这个ReflectiveLoader便可。所以,注射器的执行流程以下:koa

1. 将待注入DLL读入自身内存(利用解密磁盘上加密的文件、网络传输等方式避免文件落地)ide

2. 利用VirtualAlloc和WriteProcessMemory在目标进程中写入待注入的DLL文件函数

3. 利用CreateRemoteThread等函数启动位于目标进程中的ReflectiveLoaderui

至此,咱们注射器的任务就已经完成了。下一步就是ReflectiveLoader的实现了。

0×03 ReflectiveLoader的实现

ReflectiveLoader要完成的任务是对自身的装载。所谓的“装载”具体而言是什么意义呢?

所谓“装载”,最重要的一点就是要将自身合适地展开到虚拟空间中。咱们都知道在PE文件包含了许多节,而为了节省存储空间,这些节在PE文件中比较紧密地凑在一块儿的。而在广阔虚拟空间中,这些节就能够映射到更大的空间中去。更不用说还存在着.bss这样的在PE文件中不占空间,而要在虚拟空间中占据位置的节了。ReflectiveLoader须要作的一件很重要的事就是按照规则去将这些节映射到对应的地址去。

同时,因为DLL中可能会用到其余DLL的函数,装载一个DLL还须要将这个DLL依赖的其余动态库装入内存,并修改DLL的IAT指向到合适的位置,这样对其余DLL函数的引用才能正确运做。

虽然咱们上文提到,ReflectiveLoader的代码是地址无关的,可是该DLL的其余部分的代码却并非这样的。在一份源代码编译、连接成为DLL时,编译器都是假设该DLL会加载到一个固定的位置,生成的代码也是基于这一个假设。在反射式注入DLL的时候,咱们不太可能申请到这个预先设定好的地址,因此咱们须要面对一个重定位(Rebasing)的问题。

以上就是ReflectiveLoader所面对的问题。接下来咱们看看它是如何解决这些问题的。

1) 定位DLL文件在内存中的基址

ReflectiveLoader作的第一件事就是查找自身所在的DLL具体被写入了哪一个位置。

ReflectiveLoader首先利用一个重定位技巧找到自身所在的大体位置:

ULONG_PTR caller( VOID ) { return(ULONG_PTR)_ReturnAddress(); }

其中函数_ReturnAddress()返回的是当前调用函数的返回地址,也就是caller()的下一条指令的地址。这个地址位于ReflectiveLoader的内部,而ReflectiveLoader位于被注入的DLL文件内部,所以这个地址离DLL文件的头部不远了。

借助上文找到的地址,咱们逐字节的向上遍历,当查找到符合PE格式的文件头以后,就能够认为找到了DLL文件在内存中的地址了。

2)获取所需的系统API。

ReflectiveLoader启动时,目标进程已在正常的运行状态中了,此时目标进程已经装载了一些核心的DLL文件。咱们能够搜索这些DLL文件,查找须要的API函数,为后续操做提供方便。具体地,咱们须要的函数是kernel32.dll中的LoadLibraryA(), GetProcAddress(), VirtualAlloc()以及ntdll.dll中的NtFlushInstructionCache()函数。

ReflectiveLoader借助PEB (ProcessEnvironment Block)来查找kernel32.dll和ntdll.dll在内存中的位置。这一部分须要对TEB (ThreadEnvironment Block)和PEB (Process Environment Block)有一个基本的了解,我在此简略介绍一下。

每个线程都具备一个TEB结构,其中记录了相关线程的一些基本信息。线程运行时,其FS段寄存器记录了其TEB的位置。而在TEB结构的0×30偏移处记录了PEB结构的指针,所以能够经过以下代码访问PEB:

mov EAX, FS:[0x30]       //EAX指向了PEB结构。

PEB结构包含有65个成员,大小达到0×210个字节,在此就不细致介绍了。须要注意的是,在PEB结构的0x0C偏移处,是一个指向PEB_LDR_DATA结构体的指针,其结构以下:

5.png

 

其中的三个LIST_ENTRY是三个链表,按照不一样的顺序规则将当前进程加载的全部模块连接起来。经过遍历其中的任意一个LIST_ENTRY,咱们就能够得到全部模块的基地址,具体方法就不细致阐述了。

在获取了模块基地址以后,经过对PE文件的解析,找到DLL文件的导出表,再根据导出表就能够找到任一导出函数的地址了。对PE文件的解析有太多文章,这里也不细致阐述了。

在此,咱们获得了函数LoadLibraryA(), GetProcAddress(), VirtualAlloc()以及NtFlushInstructionCache()。它们将在以后被用到。

 

3) 分配一片用来装载DLL的空间。

虽然在ReflectiveLoader运行时,DLL文件已经在进程内存中了,可是要装载这个DLL,咱们还须要更大的空间。借助在第2)步获得的函数VirtualAlloc(),咱们能够分配一片更大的内存空间用于加载DLL。在PE头中的IMAGE_OPTIONAL_HEADER结构体中的SizeOfImage成员记载DLL被装载后的大小,咱们按照这个大小分配内存便可。

 

uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );

uiBaseAddress记录了VirtualAlloc的返回值,也就是分配内存空间的起始地址。因而uiBaseAddress就成为了DLL被装载后的基地址。

4) 复制PE文件头和各个节

分配了用于装载的空间后,ReflectiveLoader将DLL文件的头部(也就是DOS文件头、DOS插桩代码和PE文件头)复制到新的空间的首部。再根据PE文件的节表将各个节复制到相应的位置中.

5) 处理DLL的引入表

被注入的DLL可能还依赖于其余的DLL,所以咱们还须要装载这些被依赖的DLL,并修改本DLL的引入表,使这些被引入的函数能正常运行。

PE文件的引入表是一个元素为IMAGE_IMPORT_DESCRIPTOR的数组。每个被依赖的DLL都对应着数组中的一个元素。下图表示了IMAGE_IMPORT_DESCRIPTOR结构以及咱们须要进行的处理。

3.png

咱们要作的就是根据IMAGE_IMPORT_DESCRIPTOR中的NAME成员找到DLL的名称,根据名称装载这些被依赖的DLL。 IMAGE_IMPORT_DESCRIPTOR中的OriginalFirstThunk指示了要从该DLL中引入哪些函数。有的函数是由名称导入的,此时IMAGE_THUNK_DATA会指向这个函数名;有的函数是由函数序号导入,此时分析IMAGE_THUNK_DATA咱们会获得这个序号。不管是以什么方式导入,咱们都要须要找到对应的函数,而后将其地址填入FirstThunk指向的IMAGE_THUNK_DATA数组中。装载这些被依赖的DLL就不须要咱们手工操做了,咱们直接利用步骤2)中得到的LoadLibraryA()来装载它们。对于那些经过函数名导入的函数来讲,咱们能够直接用GetProcAddress()来获得它们的地址;而对于经过序数导入的函数来讲,则须要咱们再次手工分析PE文件的导出表来找到它们的位置。

在获得所需的函数的地址后,将它们填入上图的相应位置,这样咱们就完成了对引入表的处理了。

 

 

 

 

6) 对DLL进行重定位

 

 

被注入的DLL只有其ReflectiveLoader中的代码是故意写成地址无关、不须要重定位的,其余部分的代码则须要通过重定位才能正确运行。幸运的是DLL文件提供了咱们进行重定位所需的全部信息,这是由于每个DLL都具备加载不到预约基地址的可能性,因此每个DLL都对自身的重定位作好了准备。

 

 

PE可选印象头的DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]就指向了重定位表。重定位表的数据结构以下:

 

2.png

 

 

 

 

从定义上看,IMAGE_BASE_RELOCATION只包含了两个DWORD,其实在内存中它以后还跟了若干个大小为两个字节的元素,就是定义中被注释掉的“WORD Typeoffset[1]“。IMAGE_BASE_RELOCATION结构和后面紧跟的若干个Typeoffset组成了一个块,其大小为结构体中的SizeOfBlock。所以,Typeoffset的数量能够根据SizeofBlock算出。当一个块结束时,后面紧跟的就是下一个块。若SizeofBlock为0则标志着重定位表结束了。

 

1.png

 

 

 

 

Typeoffset的高4位表明重定位类型,通常为3,低12位则表示重定位地址。这个地址和IMAGE_BASE_RELOCATION中的VirtualAddress加起来则指向一个须要重定位的指令。

 

 

找到须要重定位的地点以后,怎么重定位呢?前文说到Typeoffset指示了多种重定位类型,其中最多见的为3,在此我只介绍这种状况。其余重定位类型的主体思想基本是类似的,只有细微的不一样。

 

 

咱们首先计算获得基地址的偏移量,也就是实际的DLL加载地址减去DLL的推荐加载地址。DLL推荐加载地址保存在NT可选印象头中的ImageBase成员中,而实际DLL加载地址则是咱们在第3)步中函数VirtualAlloc()的返回值。而后咱们将VirtualAddress和Typeoffset协力组成的地址所指向的双字加上这个偏移量,重定位就完成了。

 

 

 

 

*(DWORD*)(VirtualAddress + Typeoffset的低12位) += (实际DLL加载地址 – 推荐DLL加载地址)

 

 

 

 

 

在完成全部的重定位后,咱们最后调用第2)步获得的NtFlushInstructionCache()清除指令缓存以免问题。

 

 

 

 

7) 调用DLL入口点

 

 

至此,ReflectiveLoader的任务所有完成,最后它将控制权转交给DLL文件的入口点,这个入口点能够经过NT可选印象头中的AddressOfEntryPoint找到。通常地,它会完成C运行库的初始化,执行一系列安全检查并调用dllmain。

 

 

0×04 总结

 

 

反射式DLL注入是一种新型的DLL注入方式,它不须要像传统的注入方式同样须要DLL落地存储,避免了注入DLL被安全软件删除的危险。因为它没有经过系统API对DLL进行装载,操做系统无从得知被注入进程装载了该DLL,因此检测软件也没法检测它。同时,因为操做流程和通常的注入方式不一样,反射式DLL注入被安全软件拦截的几率也会比通常的注入方式低。

 

 

反射式DLL注入的实现中运用了大量对PE文件结构的解析。了解,以及动手实践这个注入方式会让您对PE文件格式,PE文件加载的理解更加深入。

相关文章
相关标签/搜索