第43章 内核6中的DLL注入
本文最后更新于:2022年5月27日 下午
第43章内核6中的DLL注入
本章将讲解在Windows OS Kernel 6 (Vista、7、8等)中实施DLL注入的方法。由于从Kernel 6开始采用了新的会话管理机制,这使得通过CreateRemoteThread() API注入DLL的旧方法对某些进程(服务进程)不再适用。本章将调试相关API,分析注入失败的原因,然后寻求解决之道。原有的DLL注入技术是通过调用CreateRemoteThread() API进行的,在Windows XP、2000中能够准确完成DLL注入操作。但Windows 7中该方法不太奏效,准确地说就是,在Windows 7中使用CreateRemoteThread() API无法完成对服务(Service)进程的DLL注入操作。原因在于,Windows7中的会话管理机制已经发生了变化。下面通过一个简单的练习示例再现DLL注入失败的情形,并分析失败原因,进而查找解决之策。
本示例文件在Windows7 32位系统中通过测试。
43.1 再现DLL注入失败
尝试将Dummy.dll文件注入Windows的系统进程时,会出现注入失败。本节中再现这种注入失败的情形(注入程序是之前用过的InjectDll.exe)。
43.1.1 源代码
先简单看一下相关源代码。
InjectDll.cpp
源代码中的核心部分是InjectDll()函数
1 |
|
代码43-1是典型的DLL注入代码,前面我们已经多次分析过,相信大家已经非常熟悉了(更多说明请参考第23章)。
Dummy.cpp
接下来查看Dummy.dll文件的源代码(dummy.cpp)。
1 |
|
DllMain()函数代码非常简单,若dummy.dll被成功注入指定进程,就输出相关调试信息(进程名称、进程ID)。
43.1.2注入测试
首先运行Process Explorer工具,查看目标进程的PID (svhost.exe (属于会话0,PID为3300)、notepad.exe(属于会话1,PID为3964)),然后再使用InjectDll.exe分别向它们注入dummy.dll文件,如图43-1所示。
注入前先把lnjectDll.exe与dummy.dll文件复制到工作文件夹,然后运行InjectDll.exe命令实施注入,如图43-2所示。
dummy.dll文件成功注入notepad.exe进程(属于会话1,PID为3964),但向svchost.exe (属于会话0,PID为3300)注入时却发生了失败(error code=8)。在Process Explorer中搜索dummy.dll模块。
就像在图43-3中看到的一样,dummy.dll文件只成功注入notepad.exe进程(属于会话1,PID为3964)。
43.2原因分析
43.2.1 调试 #1
如图43-2所示,向svchost.exe进程(属于会话0,PID为3300)注入的过程中,调用CreateRemoteThread() API 函数时发生了失败,错误代码为8 (ERROR_NOT_ENOUGH_MEMORY)。下面使用OllyDbg工具调试InjectDll.exe文件。在Open对话框中选择InjectDll.exe文件,输入相应参数后单击“打开”按钮,如图43-4所示。
我们已经知道调用CreateRemoteThread() API时会发生错误,所以使用鼠标右键菜单中的Search for All intermodular calls菜单,直接在API的调用代码处设置断点,如图43-5所示。
InjectDll.exe进程中并未应用ASLR技术。
按F9运行程序,调试器将在断点处暂停,如图43-6所示。
然后按F8键(StepOver)执行调用指令,在OllyDbg的寄存器窗口中可以看到“LastErr=ERROR_NOT_ENOUGH_MEMORY(8)” 字样,如图43-7所示。
以上通过OllyDbg工具再现了注入失败的情形,但仍未能找到确切原因。只有直接调试kernel32!CreateRemoteThread() API才能准确把握失败原因。
43.2.2调试 #2
重新运行OllyDbg调试器,暂停在InjectDll.exe调用CreateRemoteThread()的代码处(参考图43-8)。
查看存储在栈中的CreateRemoteThread() API的参数,如图43-9所示
对上图中的重要参数说明如下:
① svchost.exe (PID: 3300)的进程句柄。
② kernel32!LoadLibraryA() API地址。
③ svchost.exe的进程内存中分配的缓冲区地址。
在图43-8中使用StepIn(F7)命令,进入kernel32!CreateRemoteThreacl()API,如图43-10所示。
kernelbase.dll 是从 Vista 开始新增的 DLL 文件,负责包装(wrapper) kernel32.dll。
继续按F7键运行到kernelbase!CreateRemoteThreadEx()调用前,查看栈中存储的参数,如图
43-11所示。
kernelbase! CreateRemoteThreadEx()的参数与kernel32!CreateRemoteThread()的参数几乎一样,
只多了 1 个 IpAttributeList 参数(Arg8)。继续进人 kernelbase!CreateRemoteThreadEx()代码
(StepInto(F7)),在代码窗口中向下拖动滚动条,可以看到调用ntdll!ZwCreateThreadEx() API的代码,如图43-12所示。
运行到ZwCreateThreadEx()调用前,查看栈中存储的参数,如图43-13所示。
从栈中可以看到,ZwCreateThreadEx()拥有很多个参数。比较图43-9与图43-13可以发现,重
要的参数①~③都被原样传递过来。继续跟踪进人ntdll!ZwCreateThreadEx()API,可以看到它最终
通过SYSENTER指令进人内核模式,无法继续用户模式调试。
实际上,kernelbase!CreateRemoteThreadEx()与ntdll!ZwCreateThreadEx()都是从Vista开始新增的API (XP之前的版本中不存在)。在XP操作系统中,kernel32!CreateRemoteThread()内部会直接调用ZwCreateThreadEx()函数。在Windows XP与Windows 7中调用kernel32!CreateRemoteThread()的流程分别如图43-14所示。
到此我们可以推测岀,DLL注入失败的原因在于系统中这些新增的API,正是它们导致了向运行在会话0中的服务进程注入DLL操作失败。
Ntdll!ZwCreateThreadEx()
由于kernelbase!CreateRemoteThreadEx()只是kernel32!CreateRemoteThread()的包装器(wrapper),所以问题的原因可能在ntdll!ZwCreateThreadEx()中。ntdll!ZwCreateThreadEx()是一个尚未公开的API, MSDN中查不到函数的定义,使用Google搜索查找。
1 |
|
通过Google搜索发现,在Windows Vista以后的OS中进行DLL注入操作时,直接调用ZwCreateThreadEx()而非CreateRemoteThread()就能成功注入DLL。从我的测试结果看,这样做非常成功,且不受所在会话的影响。
比较该方法中使用的参数与图43-13中的参数可以发现,它们的不同在于第七个参数(CreateSuspended)。直接调用ZwCreateThreadEx()成功注入DLL时,CreateSuspended参数值为FALSE(0),而在CreateRemoteThread() API内部调用ZwCreateThreadEx()时,该CreateSuspended参数值为TRUE(1)。这就是DLL注入失败的原因。
从Windows XP开始,CreateRemoteThread() API内部的实现算法采用了挂起模式,即先创建出线程,再使用“恢复运行”方法继续执行(CreateSuspended=1)。
43.3练习:使 CreateRemoteThread()正常工作
我们现在已经知道了DLL注入失败的原因,也知道了解决方法,下面使用调试器直接修改测试,使调用CreateRemoteThread() API能够成功完成注入操作。
43.3.1 方法 #1:修改 CreateSuspended 参数值
修改 ZwCreateThreadEx() API 的 CreateSuspended参数值,就可在 Windows 7中成功调用CreateRemoteThread()API。重启调试器,运行到图43-12中调用ntdll.ZwCreateThreadEx()函数的位置,然后将存储在栈中的CreateSuspended参数值由1修改为0,如图43-15所示。
接下来使用StepOver(F8)命令,运行到ZwCreateThreadEx()调用后,dummy.dll成功注入指定服务进程,如图43-16所示。
使用DebugView工具可以查看dummy.dll的DllMain()函数中输出的调试日志,如图43-17所示。
在Windows 7中修改CreateSuspended参数值,借助CreateRemoteThread()API将指定DLL文件成功注入svchost.exe服务进程。
43.3.2方法#2:操纵条件分支
进一步调试kernelbase!CreateRemoteThreadEx()函数可以发现更多内容。在图43-12中,直接使用StepOver(F8)命令,执行到调用ZwCreateThreadEx()函数(CreateSuspended=TRUE)后,第一个参数pThread Handle被赋值,如图43-18所示。
创建岀线程句柄就意味着线程正常创建,也就是说,调用CreateRemoteThread()的过程中成功创建了远程线程。
这是一个非常重要的发现。虽然远程线程已被成功创建,但它无法正常工作,原因可能是后面调用ntdlllZwResumeThread()API时发生了失败,或者干脆无法调用(由于线程是以挂起模式创建的,必须“恢复运行”才能正常执行)。继续跟踪,查看调用ZwResumeThread() API的部分。 图43-19中是kernelbase!CreateRemoteThread() API代码的结束部分。
从图43-19中可以发现,调用758EBD33地址处的ntdll!CsrClientCallServer() API后,其下的条件分支指令(CMP/JL)使ZwResumeThread() API未被调用而直接跳转到后面。在调试器中调用ntdll!CsrClientCallServer() API后,操纵下面的条件分支指令使ZwResumeThread()得以调用执行,从而将DLL文件成功注入指定进程。根据Intel IA-32 Reference可知,SF!=OF时JL指令就会执行发生跳转,如图43-20所示,使用鼠标双击S Flag修改其值。
继续执行后,检查DLL文件是否成功注入指定进程。
通过上面的调试,我们掌握了在Windows 7中调用kernel32!CreateRemoteThread() API向服务进程注入DLL文件时失败的原因。并通过操纵kernel32!CreateRemoteThread() API的参数与代码,成功地将DLL文件注入指定服务进程(该方法借助调试器实现,不便推广通用)。接下来,我们 要根据上面的方法编写一个新的InjectDll.exe程序,该程序具有较好的通用性,在Windows7与XP中都能顺利完成DLL注入。
所有源代码均使用MS Visual C++ 2010 Express Edition编写而成,并在Windows 7 &XPSP3中通过测试。
43.4 稍作整理
正式编写新的DLL注入程序前,先简单整理前面学过的内容。由于Windows 7的会话管理机制发生了变化,kernel32!CreateRemoteThread() API的内部实现代码也发生了变化,最终使借助CreateRemoteThread()进行DLL注入的技术在向Windows7的服务进程(会话0)注入DLL文件时无法正常发挥作用。从调试kernel32!CreateRemoteThread()的结果看,原因在于,在API内部创建远程线程时采用了挂起模式,若远程进程属于会话0,则不会“恢复运行”,而是直接返回错误。
创建远程线程时,先采用挂起模式创建,再“恢复运行”,这是从XP就开始使用的一种实现方法
在kernel32!CreateRemoteThread() API内部调用ntdll!ZwCreateThreadEx() API,操作它的参数,或者强制改变错误条件分支语句,就可以正常创建远程线程,并成功实现DLL文件注入。
43.5 lnjectDII_new.exe
从前面的学习中我们知道,在Windows 7中实施DLL注入时直接调用ntdll!ZwCreateThreadEx() API要比调用kernel32!CreateRemoteThread()好得多。下面以此为基础编写一个新的InjectDll_new.exe程序,使之能够在Windows Kernel 6 (Vista、7、8等)中顺利完成DLL注入。
43.5.1 lnjectDII_new.cpp
首先看新编写的InjectDll()函数。
1 |
|
InjectDll()函数中变动的部分是,函数内部并未直接调用kernel32!CreateRemoteThread(),而是调用了名为MyCreateRemoteThread()的用户函数。在MyCreateRemoteThread()函数内部先获取OS的版本,若为Vista以上版本,则调用ntdll!NtCreateThreadEx()函数;若为XP以下版本,则调用kernel32!CreateRemoteThread()。整个代码比较简单,很容易理解。
用户模式下,ntdll.dll 库中的 NtCreateThreadEx()与 ZwCreateThreadEx() API 其实是同一函数(二者起始地址是一样的)。而内核模式(ntoskml.exe)中,二者是不同的。请记住,用户模式下NtXXX()与ZwXXX()是一样的。
43.5.2注入练习
首先选择一个合适的服务进程(会话0)进行DLL文件注入练习,如图43-21所示。
然后运行InjectDll_new.exe命令,输入相关参数进行注入操作,如图43-22所示。
最后使用Process Explorer工具查看svchost.exe (PID: 2096),可以看到dummy.dll文件成功注入,如图43-23所示。
这样就可以在Windows Kernel 6 (Vista、7、8等)中向服务进程(会话0)顺利注入指定DLL文件。
ntdll!NtCreateThreadEx() API是一个尚未公开的API,所以微软不建议直接调用它,否则将导致系统稳定性失去保障。就我的测试结果来看,调用它之后工作非常正常,但微软可能在以后某个时候修改它。在某个项目中使用该方法时,一定要注意这一点。