针对新型进程注入技术Ctrl-Inject的原理分析

阅读量    35233 |   稿费 200

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

概述

在本文中,我们将主要介绍一种新型的进程注入方法,我们称之为“Ctrl-Inject”,它利用控制台应用程序中处理Ctrl信号的机制实现注入。在研究的过程中,我们在浏览MSDN时发现有一条关于Ctrl信号处理的相关评论:
“这是一个与SetConsoleCtrlHandler函数( https://docs.microsoft.com/en-us/windows/console/setconsolectrlhandler )一起使用的函数,由应用程序定义。控制台进程使用此函数来处理进程收到的控制信号。当收到信号后,系统会在进程中启动一个新的线程来执行该函数。”
这也就意味着,每次我们触发一个信号到一个基于控制台的进程时,系统都会调用一个在新线程中调用的处理函数。正因如此,我们可以借助这一特点,来实现一个不同于以往的进程注入。

 

控制信号处理

当用户或进程向基于控制台的进程(例如cmd.exe或powershell.exe)发送Ctrl + C(或Break)信号时,系统进程csrss.exe将会在目标进程中创建一个新的线程来调用函数CtrlRoutine。
CtrlRoutine函数负责包装使用SetConsoleCtrlHandler的处理程序。接下来,我们深入研究一下CtrlRoutine,首先注意到了下面这段代码:

该函数使用名为HandlerList的全局变量来存储回调函数列表,在该函数中会循环执行,直到其中一个处理程序返回TRUE(通知该信号已被处理)为止。
为了使处理程序成功执行,它必须满足以下条件:
1、函数指针必须正确编码。处理程序列表中的每个指针都使用RtlEncodePointer进行编码,并在执行之前使用RtlDecodePointer API进行解码。因此,未经编码的指针很有可能会导致程序崩溃。
2、指向有效的CFG(Control Flow Guard,控制流防护)目标。CFG通过验证间接调用的目标是否为有效函数,来尝试对间接调用进行保护。
我们来看一下SetConsoleCtrlHandle,看看它如何设置一个Ctrl处理程序,以便我们以后可以模仿其方式。在下图中,我们可以看到各个指针在添加到HandlerList之前是如何编码的。

接下来,我们看到了一个名为SetCtrlHandler的内部函数调用。该函数更新了两个变量:一个是HandlerList,用于添加一个新的指针;另一个全局变量是HandlerListLength,增加了它的长度以适应新的列表大小。

现在,由于HandlerList和HandlerListLength变量驻留在kernelbase.dll模块中,并且该模块会映射到所有进程的相同地址,所以我们可以在进程中找到它们的地址,然后使用WriteProcessMemory在远程进程中更新它们的值。
我们的工作还没有完成,考虑到CFG和指针编码的存在,我们需要找到一种方法来绕过它们。

 

绕过指针编码

在Windows 10之前的版本中,我们需要理解指针编码、解码的工作原理,从而应对指针编码保护。接下来,我们一起深入了解一下EncodePointer的工作原理。

最开始,存在一个对NtQueryInformationProcess的调用,其定义如下:

NTSTATUS WINAPI NtQueryInformationProcess(
_In_      HANDLE           ProcessHandle,
_In_      PROCESSINFOCLASS ProcessInformationClass,
_Out_     PVOID            ProcessInformation,
_In_      ULONG            ProcessInformationLength,
_Out_opt_ PULONG           ReturnLength
);

根据上述定义,我们可以做出以下假设:
1、ProcessHandle:当传递-1的值时,它代表引用调用进程的函数。
2、ProcessInformationClass:该参数的值为0x24,这是一个未公开的值,要求内核检索进程加密Cookie。Cookie本身驻留在EPROCESS结构中。
在检索加密Cookie后,我们可以看到几个涉及输入指针和加密Cookie的操作。具体为:

 EncodedPointer = (OriginalPointer ^ SecretCookie) >> (SecretCookie & 0x1F)

一种绕过的方法,是使用CreateRemoteThread执行RtlEncodePointer,并将NULL作为参数传递给它,如下所示:
1) EncodedPointer = (0 ^ SecretCookie) >> (SecretCookie & 0x1F)
2) EncodedPointer = SecretCookie >> (SecretCookie & 0x1F)
这样一来,返回值将被Cookie旋转的值增加到31倍(在64位Windows 10环境上该值为63,即0x3f)。如果我们在目标进程上使用已知的编码地址,就能够暴力猜测出原始Cookie值。以下代码展示了如何对Cookie进行暴力猜测:

在Windows 10及以上版本中,微软非常慷慨地为我们提供了一组新的API,称为RtlEncodeRemotePointer和RtlDecodeRemotePointer。
顾名思义,我们传递一个进程句柄和一个指针,该API将会为目标进程返回一个有效的编码后指针。
此外,还有另一种提取Cookie的技术,请参考: https://github.com/changeofpace/Remote-Process-Cookie-for-Windows-7/blob/master/Remote%20Process%20Cookie%20for%20Windows%207/main.cpp

 

绕过CFG

到目前为止,我们已经将我们的代码注入到目标进程,并修改了HandlerList和HandlerListLength的值。如果我们现在尝试发送Ctrl+C信号来触发代码,该进程会引发异常,最终自行终止。其原因在于,CFG会注意到我们正在尝试跳转到一个非有效调用目标的指针。
幸运的是,微软对我们一直非常友善,他们发布了另外一个有用的API,名为SetProcessValidCallTargets。

WINAPI SetProcessValidCallTargets(
_In_    HANDLE                hProcess,
_In_    PVOID                 VirtualAddress,
_In_    SIZE_T                RegionSize,
_In_    ULONG                 NumberOfOffsets,
_Inout_ PCFG_CALL_TARGET_INFO OffsetInformation

);

简而言之,我们传递进程句柄和指针后,该API会将其设置为有效的调用目标。此外,如果使用我们此前介绍过的( https://blog.ensilo.com/documenting-the-undocumented-adding-cfg-exceptions )未记录的API也可以实现这一点。

 

触发Ctrl+C事件

现在一切准备就绪,我们需要做的就是在目标进程上触发Ctrl + C,以调用我们的代码。有几种方法可以触发它。在这种情况下,我们可以使用SendInput的组合,来触发系统范围的Ctrl键按键,以及用于发送C键的PostMessage。同样,也适用于隐藏或不可见的控制台窗口。以下是触发Ctrl-C信号的函数:

 

揭秘底层

从实质上来说,在这个进程注入技术中,我们将代码注入到目标进程中,但是我们从不直接调用它。也就是说,我们从来没有自己调用CreateRemoteThread或使用SetThreadContext改变执行流。相反,我们正在让csrss.exe为我们调用它,这样一来就显得是一个正常的行为,不会被怀疑。
其原因在于,每次将Ctrl + C信号发送到基于控制台的应用程序时,conhost.exe会调用类似于调用堆栈的内容,如下所示:

其中,CsrClientCallServer会传递一个唯一索引标识符(0x30401),然后将其传递给csrss.exe服务。
在其中,会从调度表中调用一个名为SrvEndTask的函数。调用链具体如下:

在这个调用链的最后,我们看到了RtlCreateUserThread,它负责在目标进程上执行我们的线程。
注意:尽管Ctrl-Inject技术仅针对于控制台应用程序,但也可能会在很多控制台应用程序上被滥用,最值得注意的就是cmd.exe。

 

总结

现在,我们已经了解了这个新型的进程注入方法,掌握了该方法的工作原理以及其背后到底发生了什么。在最后,我们可以总结一下Ctrl-Inject技术。这种技术与传统线程注入技术相比,主要优点是远程线程是由可信的Windows进程csrss.exe创建,这使得它得隐蔽性更强。但同样存在缺点,就是这种方法仅适用于控制台应用程序。

要进行这种进程注入技术,所需的步骤如下:
1、将OpenProcess附加到控制台进程。
2、通过调用VirtualAllocEx,为恶意负载分配一个新的缓冲区。
3、使用WriteProcessMemory将数据写入分配的缓冲区。
4、使用目标进程cookie将指针指向指定的缓冲区。通过调用带有空指针的RtlEncodePointer并手动编码指针或通过调用RtlEncodeRemotePointer来实现。
5、通知远程进程,新指针是可以使用SetProcessValidCallTargets的有效指针。
6、最后,使用PostMessage和SendInput的组合触发Ctrl + C信号。
7、恢复原始处理程序列表。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多