什么是 qwinsta?
qwinsta:显示有关远程桌面会话主机服务器上的会话的信息。该列表不仅包含有关活动会话的信息,还包含有关服务器运行的其他会话的信息。(参考:MSFT 文档)
还可以通过参数远程枚举用户会话/server:{hostname}。尽管 Microsoft 文档指定此二进制文件与远程桌面会话相关,但无需启用远程桌面即可使二进制文件和枚举成功:
RDP 已禁用
“远程”枚举
Windows API 实现
与大多数本机 Windows 二进制文件一样,qwinsta利用 Windows API 函数从主机检索会话信息。
Disassembly
qwinsta专门利用了 Windows Station ( WinStation qwinsta.exe ) 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包含任何等效的头文件。在同一个反汇编器中,我们可以看到这些函数导入:
-
WinStationOpenServerW:打开指定终端服务器的句柄。
-
WinStationEnumerateW:枚举服务器上的所有会话。
-
WinStationQueryInformationW:检索有关指定会话的信息。
-
WinStationFreeMemory:释放为会话信息分配的内存。
-
WinStationCloseServer:关闭服务器句柄。
这也是调用这些 API 的逻辑顺序。
指定目标主机
WinStationOpenServerW
winsta.h
在 Microsoft 中没有记录,但我在 Process Hacker 文档 ( https://processhacker.sourceforge.io/doc/winsta_8h_source.html )中找到了它的实现。
HANDLE
WINAPI
WinStationOpenServerW(
_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:
敏锐的眼睛会发现 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 表示形式,我得到的结果是有希望的!
Final Touches
所以这给我带来了一个好地方,我知道我至少能够从这些字节数组中检索用户名和会话状态。我向 Grzegorz Tworek 求助,了解他的想法。利用他的一些见解,我能够想出一种可靠的方法来提取我感兴趣的信息。
最终的输出是我非常高兴能开始工作的东西:
源代码可以在我的 github 上找到https://github.com/0xv1n/RemoteSessionEnum/blob/main/main.cpp
Remote Session Enumeration via Undocumented Windows APIs
https://0xv1n.github.io/posts/sessionenumeration/
原文始发于微信公众号(Ots安全):通过未记录的 Windows API 进行远程会话枚举
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论