导语:enSilo的一名安全研究员发现了一种新的进程注入技术,恶意行为者可能会滥用此技术来隐藏基于Windows CLI应用程序的恶意软件。
一、概述
本文将推出一个新的进程注入技术,我们称之为“Ctrl-Inject”,它利用了控制台应用程序中处理Ctrl信号的机制。作为研究的一部分,在浏览MSDN时,我们看到了关于Ctrl信号处理的以下评论:
与SetConsoleCtrlHandler函数一起使用的应用程序定义函数。控制台进程使用此功能来处理进程收到的控制信号。当收到信号时,系统会在过程中创建一个新线程来执行该功能。
这意味着每触发一个信号到一个基于控制台的进程时,系统就会在一个新线程中调用处理函数。看到这一点,我们认为可以利用此功能执行一个不同的进程注入。
二、控制信号处理
每当用户(或进程)向基于控制台的进程(如cmd.exe或powershell.exe)发送Ctrl+C(或Break)信号时,系统进程csrss.exe就会在目标进程中调用新的函数CtrlRoutine 。
CtrlRoutine函数负责包装使用SetConsoleCtrlHandler设置的hander。深入探究CtrlRoutine,我们注意到下面的一段代码:
图1:在运行和CFG检查之前解码指针
该函数使用名为HandlerList的全局变量来存储回调函数列表,在该函数中迭代它,直到其中一个hander返回TRUE通知该信号已被处理。
为了使hander成功执行,它必须满足以下条件:
· 函数指针必须正确编码——hander列表中的每个指针都使用RtlEncodePointer进行编码,并在执行之前使用RtlDecodePointer API进行解码。因此,未编码的指针很容易使程序崩溃。
· 指向有效的CFG(控制流防护)目标。 CFG通过验证间接调用的目标是否为有效函数来保护间接调用。
我们来看一下SetConsoleCtrlHandle ,看看它如何设置一个Ctrl hander,以便稍后可以复制。在图2中,可以看到每个指针在添加到HandlerList之前是如何编码的。
图2:在保存之前对指针进行编码
继续,我们看到一个名为SetCtrlHandler的内部函数的调用。这个函数更新了两个变量,HandlerList为它添加一个新的指针,另一个全局变量叫做HandlerListLength,增加了它的长度以适应新的列表大小。
图3:更新HandlerList并增加HandlerListLength
由于HandlerList和HandlerListLength变量驻留在kernelbase.dll模块中,并且由于此模块映射到所有进程的相同地址,所以可以在我们的进程中找到它们的地址,然后使用WriteProcessMemory在远程进程中更新它们的值。我们的工作还没有完成,因为CFG和指针编码已经到位,我们需要找到一种方法来绕过它们。
三、绕过指针编码
在Windows 10之前,需要了解指针编码/解码如何工作以避开指针编码保护。那么,让我们来深入了解EncodePointer的工作原理。
图4:RtlEncodePointer的内部工作原理
最初,有一个对NtQueryInformationProcess的调用。让我们来看看它的定义:
NTSTATUS WINAPI NtQueryInformationProcess( _In_ HANDLE ProcessHandle, _In_ PROCESSINFOCLASS ProcessInformationClass, _Out_ PVOID ProcessInformation, _In_ ULONG ProcessInformationLength, _Out_opt_ PULONG ReturnLength );
根据定义,可以做出以下假设:
· ProcessHandle: 当传递值-1时,它告诉函数我们要调用进程。
· ProcessInformationClass: 此参数的值为0x24,这是一个未公开的值,要求内核获取进程secretcookie。cookie本身驻留在EPROCESS结构中。
在获取secretcookie后,我们可以看到几个涉及输入指针和secretcookie的操作。这与以下等式等价:
EncodedPointer = (OriginalPointer ^ SecretCookie) >> (SecretCookie & 0x1F)
绕过此方法的一种方式是使用CreateRemoteThread 执行RtlEncodePointer 并将NULL作为参数传递给它,如下所示:
1) EncodedPointer = (0 ^ SecretCookie) >> (SecretCookie & 0x1F) 2) EncodedPointer = SecretCookie >> (SecretCookie & 0x1F)
可以看出,cookie循环31次(在Windows 10 此64比特值为63,0x3f)得到返回值。如果我们在目标进程上使用已知的编码地址,我们将能够暴力破解原始cookie值。下述代码演示了如何对cookie执行这种暴力攻击:
自Windows 10以来,微软非常慷慨地提供了一组新的API,称为:RtlEncodeRemotePointer 和RtlDecodeRemotePointer。
顾名思义,传递一个进程句柄和指针,它将为目标进程返回一个有效的编码指针。
另一种值得提及的提取cookie的技术可以在这里找到:here。
四、绕过控制流监控(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 );
简而言之,传递进程句柄和指针,它就将其设置为有效的调用目标。使用我们在之前的博文(previous blog posts)中介绍的未记录在案的API也可以做到这一点。
五、触发CTRL-C事件
现在一切就绪,所需要做的就是在目标进程上触发Ctrl + C以调用我们的代码。有几种方法可以触发它。在这里,我们使用SendInput的组合来触发系统范围的Ctrl键按键,以及用于发送C键的PostMessage。这也适用于隐藏/不可见的控制台窗口。下面是触发Ctrl-C信号的函数:
六、幕后
实际上,在这个进程注入技术中,我们将代码注入到目标进程中,但是我们并没有直接调用它,也就是说,我们从来没有使用SetThreadContext调用CreateRemoteThread或更改执行流。相反,我们正在使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为恶意payload分配一个新的缓冲区。
3. 使用WriteProcessMemory将数据写入分配的缓冲区。
4. 使用目标进程cookie将指针指向缓冲区。通过调用带有空指针的RtlEncodePointer并手动编码指针或通过调用RtlEncodeRemotePointer来实现。
5. 使用SetProcessValidCallTargets让远程进程知道新指针是有效指针。
6. 最后,使用PostMessage和SendInput的组合触发Ctrl+C信号。
7. 恢复原始处理程序列表。