0x00 背景
在实际渗透过程中,各种口令凭据的窃取是一种美妙的艺术。而我们发现,登录 RDP 会话的凭据权限都比较高,除却通过 lsass.exe 来窃取口令外,RdpThief 应用而生。本文主要结合工具 CobaltStrike 以及 RdpThief 进行测试实验,以及些许原理的说明。
0x01 测试实验
测试环境:Win7(172.16.203.131)、Win10(172.16.203.136)
测试工具:https://github.com/0x09AL/RdpThief
测试过程:
首先,我们假设目标已成功上线,以 Win10 为例,执行命令 rdpthief_enable ,开启对进程 mstsc 的检测。
然后在目标机开启 RDP 客户端,输入登录需要的 Server 地址,用户名及口令。
下图红框处,可以看到当 CobaltStrike 检测到目标进程 mstsc 时,就会将 shellcode 注入到进程,通过 Hook Windows 系统 API 获取登录凭证,并将凭证写入到环境变量 temp 目录下的 data.bin 文件。
之后执行命令 rdpthief_disable ,停止对进程 mstsc 的检测;执行命令 rdpthief_dump,读取储存凭证的文件。
无论用户登录成功与否,凭证信息都会被记录下来。
0x02 原理说明
Windows API
因为我们整个过程的核心内容就是通过 Hook Windows 系统 API 来窃取 RDP 凭证。那么接下来,就利用工具 API monitor 监控进程 mstsc 来大概看一下,RDP 登录过程中涉及的几个比较重要的系统 API。
下图红框标出的是 Win10 监控进程 mstsc ,完整进行 RDP 成功登录过程中涉及到的 4 个主要的 API 函数。
首先是 CredReadW 函数,可以看到这个函数的第一次调用,就能获得 Server 地址 172.16.203.131。
然后是 SspiPrepareForCredRead 函数,可以看到这个函数的第二个参数 pszTargetName 也能获得 Server 地址。
接下来就是 CryptProtectMemory 函数,这个函数主要用来加密内存中的敏感信息。可以看到第一次调用的这个函数,其中包含明文用户名 Test 。
接下来第二次调用的时候,可以获得纯粹的密码明文 123456 。
接着是 CredIsMarshaledCredentialW 函数,可以看到这个函数只有一个参数,就是我们的用户名明文 Test 。
工具 RdpThief 主要利用的是以下 3 个 API 来拿到对应的信息:
CredIsMarshaledCredentialW --> Username
CryptProtectMemory --> Password
SspiPrepareForCredRead --> ServerIP
但是在 Win7 中,这个过程并没有对 SspiPrepareForCredRead 函数的调用,而且整个调用过程看起来也没有 Win10 那么清晰明了。首先,仍然是 CredReadW 函数可以拿到 Server 地址 172.16.203.136 。接下来其实通过 CredPackAuthenticationBufferW 函数也能拿到用户名明文 Test 信息。
然后也可以通过 CredIsProtectedW 函数直接拿到明文密码 654321 信息。这个过程跟 Win10 是有区别的。
利用工具 RdpThief 在 Win7 进行测试实验的时候,没办法获取到 Server 地址,就是因为没有 SspiPrepareForCredRead 这个 API 。只需要修改工具代码,使其通过 CredReadW 函数来获取 Sever 地址就可以了。当然,根据上述监控 API 的调用过程,Win7 其实也能利用其他 API 函数来获取相应凭证。
Hook API
RdpThief 工具编写 hook API 的 DLL 文件,使用的是开源库 Detours ,操作和原理都很简单。下面笔者提供一个简单的 Hook MessageBox 的示例,已经写明注释,便于参考理解。
#include <Windows.h>
#include <iostream>
#include <detours.h>
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()
{
DetourRestoreAfterWith(); // 恢复之前的状态
DetourTransactionBegin(); // 开始一个事务
DetourUpdateThread(GetCurrentThread()); // 更新当前线程信
DetourAttach(&(PVOID&)TrueMessageBox, _MessageBox); // 将拦截的函数 _MessageBox 附加到原函数 TrueMessageBox 的地址上
DetourTransactionCommit(); // 提交事务
MessageBox(NULL, L"We can't be hooked", L"Hello", 0); // Hooked --> _MessageBox
DetourTransactionBegin(); // 开始一个事务
DetourUpdateThread(GetCurrentThread()); // 更新当前线程信息
DetourDetach(&(PVOID&)TrueMessageBox, _MessageBox); // 解除 Hook ,将拦截的函数从原函数的地址解除
DetourTransactionCommit(); // 提交事务
}
RdpThief 工具的作者有提供编译 DLL 的源码。所以这里笔者建议,还是自己重新建个新项目,将代码修改后编译成 DLL 。然后通过 sRDI 工具将 DLL 转换成 shellcode ,使用作者提供的 cna 脚本。结合 CobaltStrike 工具,将 shellcode 注入到 mstsc 进程中,进行 RDP 登录凭证的窃取。
踩坑指南
因为笔者复现的过程,并没有像作者演示视频的那样顺利,所以特此将碰到的坑点记录如下:
首先最重要的,RdpThief 工具是使用 VS2015 编译成的(笔者猜测,并不确定是不是这个原因),所以测试环境需要有 Visual C++ 2015 Redistributable 或更高版本的支持。否则你永远也不知道,为什么你能注入进程,但就是获取不到数据。
然后,经过修改后的代码编译 DLL ,需要 Release 版。否则你一执行命令 rdpthief_enable ,就会看到目标 mstsc 进程瞬间被杀死。(依然不清楚根本原因。)
最后,其实是编码的问题,如果你不想最后 type 读取凭证文件看到整齐划一的空格或方框,那么建议在写文件的时候,使用 WideCharToMultiByte 函数将 Unicode 变成 ANSI。可能,强迫症患者还需要注意不同操作系统换行符的问题。(不追求完美主义者,这条可忽略。)
0x03 参考链接
https://github.com/monoxgas/sRDI
https://github.com/0x09AL/RdpThief