通过未记录的 Windows API 进行远程会话枚举

admin 2024年7月15日12:39:10评论193 views字数 4654阅读15分30秒阅读模式

通过未记录的 Windows API 进行远程会话枚举

什么是 qwinsta?

qwinsta:显示有关远程桌面会话主机服务器上的会话的信息。该列表不仅包含有关活动会话的信息,还包含有关服务器运行的其他会话的信息。(参考:MSFT 文档)

还可以通过参数远程枚举用户会话/server:{hostname}。尽管 Microsoft 文档指定此二进制文件与远程桌面会话相关,但无需启用远程桌面即可使二进制文件和枚举成功:

RDP 已禁用

通过未记录的 Windows API 进行远程会话枚举

“远程”枚举

通过未记录的 Windows API 进行远程会话枚举

Windows API 实现

与大多数本机 Windows 二进制文件一样,qwinsta利用 Windows API 函数从主机检索会话信息。

Disassembly

qwinsta专门利用了 Windows Station ( WinStation  qwinsta.exe ) API。我们可以通过插入反汇编程序来发现这一点:

通过未记录的 Windows API 进行远程会话枚举

但是,对于那些知识渊博的人来说,您可能还知道,我们可以利用另一个 API 来完成相同的任务 - 终端服务 API(也称为远程桌面服务API)。harmj0y写了一篇博客,介绍如何使用PowerShell实现我将要介绍的功能。我上面链接的 harmj0y 撰写的博客利用了 RDS/WTS API,因此出于教育目的而不是重复这项工作 - 我将使用其他文档较少的 API。

关键 API

有趣的是,与可以通过 Windows 头文件直接在 C/C++ 程序中使用的终端服务 API 不同,它wtsapi32.h依赖qwinsta 于动态链接库的导入,winsta.dll而 Windows SDK 中却没有winsta32.h包含任何等效的头文件。在同一个反汇编器中,我们可以看到这些函数导入:

通过未记录的 Windows API 进行远程会话枚举

  • WinStationOpenServerW:打开指定终端服务器的句柄。

  • WinStationEnumerateW:枚举服务器上的所有会话。

  • WinStationQueryInformationW:检索有关指定会话的信息。

  • WinStationFreeMemory:释放为会话信息分配的内存。

  • WinStationCloseServer:关闭服务器句柄。

这也是调用这些 API 的逻辑顺序。

指定目标主机

WinStationOpenServerWwinsta.h在 Microsoft 中没有记录,但我在 Process Hacker 文档 ( https://processhacker.sourceforge.io/doc/winsta_8h_source.html )中找到了它的实现。

HANDLEWINAPIWinStationOpenServerW(  _In_ PWSTR ServerName);

通过这种方式,我们可以验证我们需要传入一个wchar_t(主机名)。但是,由于我们没有函数定义,我们无法真正使用此标头的当前状态。函数定义实际上在内winsta.dll。因此,我们需要采取一些额外的步骤:

定义所有函数原型,例如:

  • WinStationOpenServerW(`typedef HANDLE(WINAPI* LPFN_WinStationOpenServerW)(PWSTR);)

  • 使用LoadLibrary(https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya)获取 WinSta DLL 的句柄,然后获取我们需要调用的每个函数的内存地址。

  • 完成后关闭所有Handles。

打开远程主机Handle的示例代码

以下代码应完成四项任务:

  • 将 WinSta DLL 加载到内存中

  • 检索 WinStationOpenServerW 和 WinStationCloseServer 的指针

  • 打开和关闭服务器Handle

  • 关闭 DLL Handle

#include <Windows.h>#include <iostream>typedef HANDLE(WINAPI* LPFN_WinStationOpenServerW)(PWSTR);typedef BOOLEAN(WINAPI* LPFN_WinStationCloseServer)(HANDLE);int main() {    HINSTANCE hDLL = LoadLibrary(TEXT("winsta.dll"));    if (hDLL == NULL) {        std::cerr << "Failed to load winsta.dlln";        return 1;    }    // Find Address of WinStationOpenServerW    LPFN_WinStationOpenServerW pfnWinStationOpenServerW =        (LPFN_WinStationOpenServerW)GetProcAddress(hDLL, "WinStationOpenServerW");    if (pfnWinStationOpenServerW == NULL) {        std::cerr << "Failed to find WinStationOpenServerW functionn";        FreeLibrary(hDLL);        return 1;    }    // Find Address of WinStationCloseServer    LPFN_WinStationCloseServer pfnWinStationCloseServer =        (LPFN_WinStationCloseServer)GetProcAddress(hDLL, "WinStationCloseServer");    if (pfnWinStationCloseServer == NULL) {        std::cerr << "Failed to find WinStationCloseServer functionn";        FreeLibrary(hDLL);        return 1;    }    wchar_t serverName[] = L"TARGET_HOST"; // Target Hostname    HANDLE hServer = pfnWinStationOpenServerW(serverName);    if (hServer == NULL) {        std::cerr << "Failed to open servern";        FreeLibrary(hDLL);        return 1;    }    else {        std::wcout << L"Server Handle: " << hServer << std::endl;    }    // Close Server Handle    BOOLEAN result = pfnWinStationCloseServer(hServer);    // Close DLL Handle    FreeLibrary(hDLL);    return 0;}

查询会话

与上面一样,我们需要导入查询目标上所有会话所需的函数。我们通过 WinStationEnumerateW. 实现此目的。同样,该 API 的官方文档不存在,但我们可以再次查看 Process Hacker 代码以获得适当的结构:

typedef BOOLEAN(WINAPI* LPFN_WinStationEnumerateW)(HANDLE, PSESSIONIDW*, PULONG);

这将介绍一些我们目前没有的类型。然而,我们可以再次引入 PH 源中定义的内容,例如:

typedef struct _SESSIONIDW {    union {        ULONG SessionId;        ULONG LogonId;    };    WINSTATIONNAME WinStationName;    WINSTATIONSTATECLASS State;} SESSIONIDW, * PSESSIONIDW;

当我们引入每个所需的结构时,您会发现我们需要引入其他未定义的所需结构 - 这是一个乏味的过程,但如果您一个接一个地进行,就不会太困难。

API调用

现在所有必需的结构和函数都可用,我们可以调用 WinStationEnumerateW :

// Enumerate Server for Active Sessions, store IDs in PSESSIONIDW (SESSIONIDW array)PSESSIONIDW pSessionIds = NULL;ULONG count = 0;BOOLEAN enumResult = pfnWinStationEnumerateW(hServer, &pSessionIds, &count);

我们对 pSessionIds 感兴趣,它包含 PSESSIONIDW 数组。这个结构体包含我们每个会话关心的 4 条信息:

  • 会话 ID / 登录 ID

  • WinStation名称

  • 状态

在此 API 调用的上下文中,WinStationName 只是 qwinsta 输出中的 SessionName:

通过未记录的 Windows API 进行远程会话枚举

敏锐的眼睛会发现 qwinsta 还具有 PSESSIONIDW 结构中未见的变量。现在,我决定不去深入那个兔子洞,因为目标只是枚举用户名,以及它们是否有活动的(状态 0)会话。

枚举会话信息

WinStationEnumerateW 返回一个布尔值,因此如果我们成功调用函数,我们就可以继续执行最后一步 - 枚举单个会话信息。为此,我们对 WinStationQueryInformationW 进行最后一次 API 调用。幸运的是,这个记录在这里。

实现这个很简单,因为 pSessionIds 是一个数组或 SESSIONIDW ,我们可以迭代它,然后访问其中的结构变量。

遇到障碍

根据函数原型

typedef BOOLEAN(WINAPI* LPFN_WinStationQueryInformationW)(HANDLE, ULONG, WINSTATIONINFOCLASS, PVOID, ULONG, PULONG);

我们将为每个会话返回一个 WINSTATIONINFOCLASS ,这就是事情对我来说变得奇怪的地方。检查 wintern.h 显示以下定义:

typedef enum _WINSTATIONINFOCLASS {    WinStationInformation = 8} WINSTATIONINFOCLASS;typedef struct _WINSTATIONINFORMATIONW {    BYTE Reserved2[70];    ULONG LogonId;    BYTE Reserved3[1140];} WINSTATIONINFORMATIONW, * PWINSTATIONINFORMATIONW;

Reserve2 和 Reserve3 包含一个字节数组,我不知道它代表什么,但因为我们知道每个会话都会有一个用户名、会话名称,也许还有一些其他信息,我们可以只输出字节字符串,然后尝试推导出含义。为此,我编写了一个简单的例程来将字节数组转换为 ASCII 表示形式,我得到的结果是有希望的!

通过未记录的 Windows API 进行远程会话枚举

Final Touches

所以这给我带来了一个好地方,我知道我至少能够从这些字节数组中检索用户名和会话状态。我向 Grzegorz Tworek 求助,了解他的想法。利用他的一些见解,我能够想出一种可靠的方法来提取我感兴趣的信息。

最终的输出是我非常高兴能开始工作的东西:

通过未记录的 Windows API 进行远程会话枚举

源代码可以在我的 github 上找到https://github.com/0xv1n/RemoteSessionEnum/blob/main/main.cpp

Remote Session Enumeration via Undocumented Windows APIshttps://0xv1n.github.io/posts/sessionenumeration/

原文始发于微信公众号(Ots安全):通过未记录的 Windows API 进行远程会话枚举

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月15日12:39:10
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   通过未记录的 Windows API 进行远程会话枚举https://cn-sec.com/archives/2955587.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息