大家好,又见面了,我是你们的朋友全栈君。
前不久实现了经典dll注入exe,实现注入到用户层面进程,比如notepad.exe和wps.exe。(像金山毒霸kxetray.exe有自我保护机制也注入不进去)。特此记录一下。
首先什么是反射式注入?它和传统经典注入有什么区别呢?我这里作个比喻。
经典式:
在目标进程申请一段空间并放入恶意dll,创建远线程让目标进程调用LoadLiabrary函数加载这段内存
先把病毒扔进邻居家,告诉邻居这是个炮仗(病毒dll),邻居误以为真,点燃导火索(创建线程执行),显然邻居被骗了
反射式:
在别人的内存里调用自己编写的dll导出函数 ,自己dll导出函数里实现自我加载(加载PE的整个过程)
先把病毒扔进邻居家,然后自己把它直接点燃(调用dll导出函数),病毒自己爆炸(dll内部实现自己从底层dll遍历加载函数),邻居浑然不知(没有通过系统LoadLibrary调用dll,没有记录)
区别是啥
少了使用LoadLibrary的过程。
反射式注入dll的优点
反射式注入方式并没有通过LoadLibrary等API来完成DLL的装载,DLL并没有在操作系统中”注册”自己的存在,因此ProcessExplorer等软件也无法检测出进程加载了该DLL。利用解密磁盘上加密的文件、网络传输等方式避免文件落地,DLL文件可以不一定是本地文件,可来自网络等,总之将数据写到缓冲区即可。
由于它没有通过系统API对DLL进行装载,操作系统无从得知被注入进程装载了该DLL,所以检测软件也无法检测它。同时,由于操作流程和一般的注入方式不同,反射式DLL注入被安全软件拦截的概率也会比一般的注入方式低。
实现需求
一、注射器
(将这个DLL文件写入目标进程的虚拟空间中)实现大部分和传统注入一样。但它要做一件很重要的事情,就是获得病毒dll里的一个最重要的导出函数,也即是我们要编写的反射加载自己的函数ReflectiveLoader的文件偏移。(如果你知道自己dll这个导出函数文件偏移多少其实也没必要再用程序读取)
二、编写一个邪恶的dll以及它的核心函数ReflectiveLoader
要知道ReflectiveLoader函数运行时所在的DLL还没有被装载,它在运行时会受到诸多的限制,例如无法正常使用全局变量等。而且,由于我们无法确认我们究竟将DLL文件写到目标进程哪一处虚拟空间上,所以我们编写的ReflectiveLoader必须是地址无关的。
注射器实现
1. 将待注入DLL读入自身内存(可以在磁盘上存放一份DLL的加密后的版本,然后将其解密之后储存在内存里)
2. 利用VirtualAlloc和WriteProcessMemory在目标进程中写入待注入的DLL文件
3.利用CreateRemoteThread等函数启动位于目标进程中的ReflectiveLoader(要首先获得ReflectiveLoader的文件偏移地址),获取的是DLL中反射加载函数在文件中的偏移,由于这种注入没有使用LoadLibraryA函数,所以DLL在内存中的状态和在磁盘中的状态是相同的,要想调用函数,我们就需要找到文件偏移。
线程函数的地址=缓冲区的基地址+文件偏移
图中1264十六进制就是4F0
4.通过DLL的导出表找到这个ReflectiveLoader并调用它
将自身合适地展开到虚拟空间中。我们都知道在PE文件包含了许多节,而为了节省存储空间,这些节在PE文件中比较紧密地凑在一起的。而在广阔虚拟空间中,这些节就可以映射到更大的空间中去。(更不用说还存在着.bss这样的在PE文件中不占空间,而要在虚拟空间中占据位置的节)
ReflectiveLoader函数实现
实现中的问题:
DLL中可能会用到其他DLL的函数,装载一个DLL还需要将这个DLL依赖的其他动态库装入内存,并修改DLL的IAT指向到合适的位置,这样对其他DLL函数的引用才能正确运作ReflectiveLoader的代码是地址无关的,但是该DLL的其他部分的代码却并不是这样的。在一份源代码编译、链接成为DLL时,编译器都是假设该DLL会加载到一个固定的位置,生成的代码也是基于这一个假设。在反射式注入DLL的时候,我们不太可能申请到这个预先设定好的地址,所以我们需要面对一个重定位(Rebasing)的问题。
解决方案
1.ReflectiveLoader做的第一件事就是查找自身所在的DLL具体被写入了哪个位置
ULONG_PTR caller( VOID ) { return(ULONG_PTR)_ReturnAddress(); }
借助上文找到的地址,我们逐字节的向上遍历,当查找到符合PE格式的文件头之后,就可以认为找到了DLL文件在内存中的地址了。
Caller()函数:获得当前指令的下条指令的地址。
2.我们需要的函数是kernel32.dll中的LoadLibraryA(), GetProcAddress(),VirtualAlloc()以及ntdll.dll中的NtFlushInstructionCache()函数。我们需要遍历已经加载的模块,从中找到我们需要的模块,获得以上几个函数的地址。
#define KERNEL32DLL_HASH 0x6A4ABC5B
#define NTDLLDLL_HASH 0x3CFA685D
#define LOADLIBRARYA_HASH 0xEC0E4E8E
#define GETPROCADDRESS_HASH 0x7C0DFCAA
#define VIRTUALALLOC_HASH 0x91AFCA54
#define NTFLUSHINSTRUCTIONCACHE_HASH 0x534C0AB8
// compute the hash values for this function name
dwHashValue = hash((char *)(uiBaseAddress + DEREF_32(uiNameArray)));
每一个线程都具有一个TEB结构,记录了相关线程的一些基本信息。线程运行时,其FS段寄存器记录了其TEB的位置。
获取PEB的方法:FS:[0x30]和GS:[0x60],前者为32位系统,后者为64位系统。
这里需要用到之前TEB定位与导出函数定位的知识。
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.根据节表加载节
6. 处理DLL的导入表
被注入的DLL可能还依赖于其他的DLL,因此我们还需要装载这些被依赖的DLL,并修改本DLL的引入表,使这些被引入的函数能正常运行。
无论是以什么方式导入,我们都要需要找到对应的函数,然后将其地址填入FirstThunk指向的IMAGE_THUNK_DATA数组中。这个跑起来是IAT
7.对DLL进行重定位
由于基址改变,所以程序中的一些直接寻址等会出问题,所以要更改重定向表。
被注入的DLL只有其ReflectiveLoader中的代码是故意写成地址无关、不需要重定位的,其他部分的代码则需要经过重定位才能正确运行。
我们首先计算得到基地址的偏移量,也就是实际的DLL加载地址减去DLL的推荐加载地址。DLL推荐加载地址保存在NT可选印象头中的ImageBase成员中,而实际DLL加载地址则是我们在第3)步中函数VirtualAlloc()的返回值。然后我们将VirtualAddress和Typeoffset合力组成的地址所指向的双字加上这个偏移量,重定位就完成了。
(DWORD)(VirtualAddress + Typeoffset的低12位) += (实际DLL加载地址 – 推荐DLL加载地址)
在完成所有的重定位后,我们最后调用第2)步得到的NtFlushInstructionCache()清除指令缓存以避免问题。
8. 调用DLL入口点
至此,ReflectiveLoader的任务全部完成,最后它将控制权转交给DLL文件的入口点,这个入口点可以通过NT可选印象头中的AddressOfEntryPoint找到。一般地,它会完成C运行库的初始化,执行一系列安全检查并调用dllmain。
用到的底层C++语言编程
dll遍历kernel32与ntdll的导出函数部分用汇编语言更加直接简短,这部分代码同样是大部分加壳程序的壳内核心代码,有时间一定会细心研究一下。
_rotr函数,功能是value右循环移动shift位( unsigned int value, int shift );
register 关键字请求“编译器”将局部变量存储于寄存器中——最快的关键字
register 这个关键字请求编译器尽可能的将变量存在CPU内部寄存器,而不是通过内存寻址访问,以提高效率。注意是尽可能,不是绝对。
数据从内存里拿出来先放到寄存器,然后CPU 再从寄存器里读取数据来处理,处理完后同样把数据通过寄存器存放到内存里,CPU 不直接和内存打交道。
内存 寄存器 CPU
寄存器其实就是一块一块小的存储空间,只不过其存取速度要比内存快得多。CPU从寄存器里拿数据比在大内存里去寻找某个地址上的数据快多了。
__readgsqword(0x60) 从GS寄存器里读取数值
查杀建议
回想我们注射器实现的过程中所调用的函数,与正常的注入似乎没有太大的区别,像CreateRemoteProcess这种危险函数杀软抓的很严,可以被替换掉。整个过程没有发现LoadLibraryA函数。但这个样本有明显的特征:解析PE结构,所以当我们遇到这种样本的时候,可以考虑为反射式DLL注入。
用IDA逆向程序时,解析PE结构的过程,编程中会出现很多+0xnumber 这里number代表十六进制数字。
在磁盘发现某dll很可疑,如果该dll被注入到进程中已经在运行,删除或移动dll时会弹窗正在某某程序中使用,可以看到它是不是注入的dll。
另外注入到进程的恶意Dll程序执行错误时崩溃并不会导致被注入程序实际的崩溃。
参考
来源https://bbs.pediy.com/thread-224241.htm
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/145399.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...