介绍
远程桌面(RDP)是用于管理Windows Server的最广泛使用的工具之一,除了被管理员使用外,也容易成为攻击者的利用目标。登录到RDP会话的凭据通常用于是具有管理权限的,这也使得它们成为红队行动的一个理想目标。站在传统的角度看,许多人倾向于使用LSASS进行凭据盗窃,但是lsass.exe通常受到EDR和防病毒产品的监视,而且对LSASS的操作通常需要权限访问,于是我们自然就会考虑,有没有一种更容易的替代方案?
在本文中,我将描述我编写的一个工具,能使用API钩子从Microsoft RDP客户端提取明文凭据,而且如果是在已经受感染用户的权限下操作(比如网络钓鱼导致),并且该用户已打开RDP会话,则可以提取明文凭据而无需提权。
API钩子
简单来说,API钩子是通过将程序重定向到另一个函数来拦截程序中函数调用的过程。这是通过重新编写目标函数的内存代码来实现的。有几种API挂钩方法,技术都比较复杂,细述的话需要单独的篇章。
而就本文而言,我们将使用到的是Microsoft Detours库,该库是开源的,奇热并且支持32位和64位进程。其他框架(如Frida)也能提供类似的功能,但是Detours是非常轻量级的。为了演示这个库有多么强大,我们将使用它为MessageBox函数创建一个钩子。
钩住函数之前,我们需要做两件事,一个是包含原始函数地址和被钩住函数地址的目标指针,为了使钩子正常工作,目标函数和被钩住的函数都应具有相同数量的参数、参数类型和调用约定。
在下面的示例中,我们钩住了MessageBox调用并修改了传递给原始函数的参数。
#include "pch.h" #include#include #include static int(WINAPI * TrueMessageBox)(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) = MessageBox; int WINAPI _MessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) { return TrueMessageBox(NULL, L"Hooked", L"Hooked", 0); } int main() { // Hook MessageBox DetourRestoreAfterWith(); DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID&)TrueMessageBox, _MessageBox); // Two Arguments DetourTransactionCommit(); MessageBox(NULL, L"We can't be hooked", L"Hello", 0); // Detach Hooked Function DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourDetach(&(PVOID&)TrueMessageBox, _MessageBox); DetourTransactionCommit(); }
运行程序,第二个消息框应为Unhooked,但由于我们将其钩住并修改了参数,所以消息框如下图所示:
寻找要钩住的函数
在进行任何挂钩之前,我们需要确定感兴趣的函数。这些函数最好将我们感兴趣的数据作为参数;在本例中,是服务器主机名/IP、用户名和密码。对于这种情况,API监视器是一个非常强大的工具,它允许你附加到一个进程,记录所有API调用并浏览结果。
为此,我们将API监视器附加到mstsc.exe并启动一个示例连接:
现在,我们可以在所有API调用中搜索作为用户名提供的字符串。在本例中,xsie几个API调用都包含此字符串,但是最值得注意的一个是CredIsMarshaledCredentialW。
通过MSDN,我们可以看到它只使用一个指向C Unicode字符串的long指针类型的参数。
为了确保通过挂钩此函数获得正确的数据,我们将Windbg附加到mstsc.exe,并在CredIsMarshaledCredentialW处设置一个断点。尝试登录时,我们可以看到传递给该函数的第一个参数是Unicode字符串的地址。
按照相同的方法搜索密码字符串,然后可以看到对CryptProtectMemory的调用,并带有指向密码字符串的指针。
根据API监视器,CryptProtectMemory位于Crypt32.dll中,由于该函数是在最新版本的Win10上由dpapi.dll导出的,因此位置不正确。为了确保我们在此API调用上也具有正确的数据,我们将Windbg附加到进程,并为CryptProtectMemory函数设置一个断点。
通过检查内存,我们可以假定传递的参数是指向结构的指针。由于我们所需要的信息只是结构的开始,因此不必完全解析它。与前面的示例相反,该函数有多个调用,其中不包含我们所需要的信息。我们可以观察到前4个字节中包含了密码字符串的大小,从内存中读取大小,并比较它是否大于0x2,如果条件为true,则意味着结构中包含密码并可提取。
SspiPrepareForCredRead遵循相同的过程,该过程接收IP地址作为第二个参数。
RdpThief演示
现在,我们清楚地了解了需要挂钩哪些函数以提取信息,这些信息可以作为实现类似于RdpThief的功能的基础。
RdpThief本身是一个独立的DLL,当注入mstsc.exe进程时,它将执行API钩子,提取明文凭据并将其保存到文件中。它附带一个攻击者脚本,负责管理状态、监视新进程并将shellcode注入mstsc.exe。DLL已使用sRDI项目转换为shellcode。启用后,RdpThief将每5秒获取一次进程列表,搜索mstsc.exe后注入其中。
当将攻击者脚本加载到Cobalt Strike上时,将提供三个新命令:
rdpthief_enable:启用对新的mstsc.exe进程的心跳检查并将其注入。
rdpthief_disable:禁用新mstsc.exe的心跳检查,但不会卸载已加载的DLL。
rdpthief_dump:打印提取的凭据(如果有的话)。
可以在此视频中找到RdpThief的简单演示。
RdpThief工具源代码在此。