目录
前言
一、在 XP 系统上模拟 SAS
二、在不低于 Vista 的系统上模拟 SAS
2.1 一些细节
2.2 实现原理和应用
三、完整实现代码和测试
3.1 客户端控制台程序
3.2 服务程序
3.3 编译&测试程序
四、总结&更新
参考文献
前言
对于开启了安全登陆的窗口工作站 Server 系统,需要在登陆时按下 Ctrl + Alt + Del 才能成功登陆。此时,研究一种编程模拟发送组合热键以便于验证安全注意序列(SAS)是有必要的,通过研究发现发送这样子的快捷键往往需要一些特殊的技巧,因为他它和一般的快捷键不同(诸如,Win + D)。
本文属于拦截系统热键系列的补充文章。这篇文章的方法并不是本人提出的,而是参考了 Jackchenyj 的相关研究并验证所得。Jackchenyj 的原文是VNC源码研究(十)实现模拟发送 CAD。
本文转载请注明出处:https://blog.csdn.net/qq_59075481/article/details/136083634。
一、在 XP 系统上模拟 SAS
在 XP 系统模拟 SAS 需要具有 Local System 权限的进程来打开 Winlogon 桌面,并广播 Ctrl + Alt + Del 热键消息。虽然在 XP 上,系统热键通过 Winlogon 以 SAS 窗口快捷键的方式注册,我们只需要发送组合键即可模拟 SAS,但问题是 Local System 权限的进程一般为系统关键进程或者服务才可以具有。所以,只有两种方法:(1)注入 SYSTEM 权限的进程,依托已有进程执行代码;(2)创建一个以 Local System 身份登陆的服务进程,在自定义服务进程中执行代码。
为了稳定性,我们决定通过服务来实现。为了控制服务,我们通过命名管道在 Local System 身份登陆的服务和具有管理员权限的用户会话进程之间建立通信。
处理函数主要涉及打开窗口工作站、打开 winlogon 桌面、切换工作线程的桌面、发送广播消息、完成消息并恢复工作线程的桌面。
/**
* @brief 在 XP 系统下模拟 Ctrl + Alt + Delete 快捷键
*
* @param[out] PDWORD dwStatus // 传递操作失败的状态码
*
* @return TRUE 成功
* FALSE 失败
* @note
*/
BOOL SimulateSASInWinXP(PDWORD dwStatus)
{
HDESK hdeskCurrent;
HDESK hdesk;
HWINSTA hwinstaCurrent;
HWINSTA hwinsta;
DWORD_PTR dwResult;
BOOL bResult = FALSE;
LRESULT lResult = 0;
DWORD dwError;
// Save the current Window station
hwinstaCurrent = GetProcessWindowStation();
if (hwinstaCurrent == NULL)
{
*dwStatus = 0xC001;
return FALSE;
}
// Save the current desktop
hdeskCurrent = GetThreadDesktop(GetCurrentThreadId());
if (hdeskCurrent == NULL)
{
*dwStatus = 0xC002;
return FALSE;
}
// Obtain a handle to WinSta0 - service must be running
// in the LocalSystem account
hwinsta = OpenWindowStation(L"winsta0", FALSE,
WINSTA_ACCESSCLIPBOARD |
WINSTA_ACCESSGLOBALATOMS |
WINSTA_CREATEDESKTOP |
WINSTA_ENUMDESKTOPS |
WINSTA_ENUMERATE |
WINSTA_EXITWINDOWS |
WINSTA_READATTRIBUTES |
WINSTA_READSCREEN |
WINSTA_WRITEATTRIBUTES);
if (hwinsta == NULL)
{
*dwStatus = 0xC003;
return FALSE;
}
// Set the windowstation to be winsta0
if (!SetProcessWindowStation(hwinsta))
{
*dwStatus = 0xC004;
return FALSE;
}
// Get the default desktop on winsta0
hdesk = OpenDesktop(L"Winlogon", 0, FALSE,
DESKTOP_CREATEMENU |
DESKTOP_CREATEWINDOW |
DESKTOP_ENUMERATE |
DESKTOP_HOOKCONTROL |
DESKTOP_JOURNALPLAYBACK |
DESKTOP_JOURNALRECORD |
DESKTOP_READOBJECTS |
DESKTOP_SWITCHDESKTOP |
DESKTOP_WRITEOBJECTS);
if (hdesk == NULL)
{
*dwStatus = 0xC005;
return FALSE;
}
// Set the desktop to be "default"
if (!SetThreadDesktop(hdesk))
{
*dwStatus = 0xC006;
return FALSE;
}
// Use SendMessageTimeout to send msg and wait result event
SetLastError(0);
lResult = SendMessageTimeout(
HWND_BROADCAST, // 目标窗口句柄
WM_HOTKEY, // 消息类型
0, // wParam
MAKELONG(MOD_ALT | MOD_CONTROL, VK_DELETE), // lParam
SMTO_NORMAL, // 标志位
5000, // 超时时间(毫秒)
&dwResult // 接收消息结果的变量
);
bResult = (lResult > 0) ? 1 : 0;
if (!bResult) {
dwError = GetLastError();
if (dwError == ERROR_TIMEOUT) {
// 超时处理
*dwStatus = 0xC007;
goto endFunc;
}
else {
// 其他错误处理
*dwStatus = 0xC008;
goto endFunc;
}
}
else {
// 消息发送成功
*dwStatus = 0;
goto endFunc;
}
endFunc:
// Reset the Window station and desktop
if (!SetProcessWindowStation(hwinstaCurrent))
{
*dwStatus = 0xC010;
return FALSE;
}
if (!SetThreadDesktop(hdeskCurrent))
{
*dwStatus = 0xC011;
return FALSE;
}
// Close the windowstation and desktop handles
if (!CloseWindowStation(hwinsta))
{
*dwStatus = 0xC012;
return FALSE;
}
if (!CloseDesktop(hdesk))
{
*dwStatus = 0xC013;
return FALSE;
}
return bResult;
}
在 XP 上,默认情况下系统配置为使用“欢迎界面”,此时,按下 Ctrl + Alt + Del 不会进入 SecureDesktop,而是只启动任务管理器,此时,我们通过一些技巧来完成切换。
首先我们发现要想启用按下 Ctrl + Alt + Del 进入 SecureDesktop,则需要在 “控制面板 | 账户 | 更改登录和注销选项” 页面取消“使用欢迎屏幕”选项并应用,修改及时生效。
我们通过 Procexp 就可以定位控制面板中该 GUI 由名为 mshta.exe 的进程完成。
它的位置如下:
我们使用 procmgr 对进程的注册表操作进行监视,通过筛选事件,我们定位到了 LogonType 的注册表修改操作:
通过对比启用/禁用前后的设置:
我意识到, LogonType 为 1 时,对应于启用欢迎屏幕,此时 CAD 不激活安全桌面;
LogonType 为 0 时,对应于禁用欢迎屏幕,此时 CAD 会激活安全桌面。
于是,只要通过修改 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\LogonType 位置的键值为 0,就可以在模拟 SAS 时进入安全注意页面了。就不提供编程实现了,因为有些用户不喜欢传统的登陆页面。CAD 效果如下:
唯一的缺点就是,登陆界面变成了密码框:
二、在不低于 Vista 的系统上模拟 SAS
从 Vista 开始,发送消息的方法就失效了(因为微软改用更安全的 RPC 了),微软提供了一个接口函数 SendSAS,该函数由 sas.dll 导出。当系统配置允许服务程序模拟 SAS 时,可以在服务进程中使用该函数来发送 SAS 请求。
2.1 一些细节
在这里,我们将讨论一些 Jackchenyj 的原文未曾提到的细节。首先,模拟 SAS 的官方方法是调用 SendSAS 函数。但是根据微软文档介绍 sas.dll 这个链接库文件在原生的 Windows Server 2008 和 Windows Vista 是不可用的。由于早期发布,微软甚至未来得及包含相关文件,但是构建消息的 RPC 客户端的运行时库是早就集成到系统中的。
通过 IDA 分析多个版本系统的 sas.dll 库发现, sas.dll 似乎是一个封装,它将调用 RPC 客户端过程,这个库几乎未发生过变化。但微软似乎刻意隐藏 SAS 的细节而从未在文档中完整介绍,导致相关研究基本上都是建立在一般的方法角度上。
但是我也找到了一些提示,下文是从 Stack Overflow 摘取的关于模拟 SAS 第三方研究的描述片段:
- 许多论坛都说这是不可能的,但事实与他们相反。 :)
- SendSAS 功能看起来是最明显的答案,但这需要更改组策略,UIPI bypass 等等,所以这绝对不是只运行的 TeamViewer 所做的;
- 另一个常见的建议是使用商业化的 SasLibEx 第三方库,但该库已停产并且仅仅支持到 Windows Vista;
- 一篇旧的 Stackoverflow 回答似乎有点作用,但它不适用于较新的 Windows
- SendInput 对 Alt-Ctrl-Del 不起作用,如果它被发送到 winsta0\winlogon 桌面;
- PostMessage(HWND_BROADCAST, WM_HOTKEY, 0, MAKELONG(MOD_CONTROL | MOD_ALT, VK_DELETE)); —— 不,它不再起作用;
- WmsgSendMessage 尝试了下一个人的建议,结果证明它与 SendSAS 没有更好或不同。
- Keyboard filter drivers 可能是一个解决方案,但他们需要代码签名和特殊权限才能安装,实现它们既不适合胆小的人。我还使用 DriverQuery 验证了正在运行的 TeamViewer 不会安装驱动程序。
显然,这是一个很好的研究开端,下面将通过综合相关文献以及我自己的验证,展示部分关于模拟 SAS 的细节。
(1)对相关函数的分析
首先,函数声明如下:
void SendSAS(
[in] BOOL AsUser
);
参数
[in] AsUser:如果调用方以当前用户身份运行,则为 TRUE;否则为 FALSE。
返回值
无
SendSAS 成功的前提条件有两种,满足其中一种就可以成功调用:
(1)应用程序需要具有 TcbPrivilege,即作为操作系统的一部分执行的权限。LocalSystem 服务已具有该权限,并且默认情况下已启用该权限。此外,还需要配置计算机的本地安全策略,以允许服务生成 Ctrl-Alt-Del(或安全注意序列,SAS,Microsoft 称之为 Ctrl-Alt-Del),这可以通过修改注册表来实现。(此时使用 FALSE 参数)
(2)应用程序作为 AsUser 运行,具备下列各项条件的才允许模拟 SAS:
- 使用 AuthentiCode 签名,并且代码执行的所属上下文也必须是具有签名的
- 将 requestedExecutionLevel 元素的 uiAccess 属性设置为 true 的清单
- UAC 必须处于开启状态
- 需要存放在安全文件夹(如 Program Files 或 System32)中
- 必须将本地安全策略配置为允许应用程序模拟 SAS。
两种情况均说明了模拟 SAS 需要一些前置的额外操作。但本文为了方便,仅讨论第一种作为服务进程进行模拟的模式。
首先标准的模拟 SAS 模式需要开发人员在代码中引入 SAS 库:
#include <sas.h>
#pragma comment(lib, "sas.lib")
随后在正文中需要发起模拟的位置使用 SendSAS 函数:
SendSAS(FALSE);
但是我们并不能满足于直接使用 SendSAS,因为在部分机器上未配置 SAS 库。下面将谈谈我对该函数的一些较为深入的分析。
首先,通过 IDA 分析 SendSAS 函数,其伪代码如下:
通过这里,我们大概了解到,如果程序是在登陆会话 Winlogon 桌面下以 AsUser 身份运行的(已知即使是 SYSTEM 也不行),可以直接使用 SendSAS 并指定 TRUE,但此类型似乎要么是微软预留的测试模式要么就是特殊情况使用,甚至连系统的 OSK 组件在发送代码时候也只指定 FALSE 参数的模式。
SAS 模块首先获取当前进程的会话 ID,随后加载 wmsgapi.dll 并调用 WmsgSendMessage 函数,第一个参数是之前获取的会话 ID,第二个参数是固定值 0x208,第三个参数默认为 0 (有资料显示该参数可以传递 PID,但并未给出实际证明) ,最后一个参数传递 RPC 服务端传回的额外信息。
为什么说 SAS 模块是一个 RPC 客户端模块呢?
我们再来分析一下 wmsgapi.dll,如下图所示:
这个函数先后调用了两个内部函数:WmsgpConnect 和 WmsgpSendMessage 函数。
P.S.: WMsg 这个前缀似乎和 Winlogon 注册的 RPC Server 中部分未记录的函数前缀名称类似,比如 Winlogon 的 WmsgkMsgHandler 函数。
WmsgpConnect 函数:
WmsgpSendMessage 函数:
通过上文其实不难理解模拟 SAS 其实不是在调用方完成的,调用方只是将请求发送给了系统,然后由服务端代理完成,整个过程并没有涉及到键盘的输入信息。
SAS 模块在动态调用 wmsgapi 时,并没有注意返回值,这也许是微软的失误,或者是刻意为之,谁知道呢?为了进一步了解 WmsgSendMessage 函数调用返回机制,以了解如何判断消息发送成功与否,我们分析了 OSK 也就是屏幕键盘的导入表,因为该程序在特定条件下也能模拟 SAS,我想分析它是否使用了 WMsg 接口。
普通用户模式运行的 OSK 程序,在用户按下 Ctrl+Alt+Del 时,只会弹出一个毫无意义的窗口,他甚至不做任何事情:
随后,我们通过导入表快速定位到了 CtrlAltDelete 函数,它内部使用了 WMsg ,其代码如下:
__int64 __fastcall COSKKeyboardManager::CtrlAltDelete(DirectUI::NativeHWNDHost **this)
{
unsigned int v2; // ebx
DWORD CurrentProcessId; // eax
int v4; // eax
HWND HWND; // rax
DWORD pSessionId; // [rsp+38h] [rbp+10h] BYREF
char v8; // [rsp+40h] [rbp+18h] BYREF
v2 = 0;
if ( COSKUtils::ShouldRunSecure() ) // 检查是否以安全账户登陆运行
{
pSessionId = 0;
CurrentProcessId = GetCurrentProcessId();
if ( ProcessIdToSessionId(CurrentProcessId, &pSessionId) )
{
// 调用 WMsg
v4 = WmsgSendMessage(pSessionId, 0x208i64, 0i64, &v8);
if ( v4 ) // 返回值不为 0,表示调用失败
{
if ( v4 != 5 ) // 拒绝访问
{
if ( v4 > 0 ) // 其他错误
return (unsigned __int16)v4 | 0x80070000; // 区别高位和低位信息
else
return (unsigned int)v4;
}
}
}
}
else // 弹出默认窗口
{
HWND = DirectUI::NativeHWNDHost::GetHWND(this[16]);
PostMessageW(HWND, 0x111u, 0x13F0ui64, 0i64);
}
return v2;
}
这个代码告诉我们,OSK 会检查运行模式,符合条件的才发送 WMsg。有趣的是,这里的调用实际上绕开了 SAS 库,并且它会正常检查函数的返回值来确认调用是否成功。
相对来说,使用 WMsgAPI 库比使用 SAS 库具有版本优势,因为在早期的 Windows Server 2008 和 Windows Vista 上,只有安装 SAS 库的补丁,才可以使模拟成功完成。而 wmsgapi.dll 则早在一开始就存在于系统的 System32 目录下。
(2)未知而特殊的模拟模式
在 Win11 上的测试给我们全新的认识,Win11 还引入了一个 UWP 版本的触摸键盘(可以通过设置在任务栏右侧通知区域显示轻松使用应用图标),托盘进程为 TabTip,APPX Host 进程为 TextInputHost.exe,由 DcomLaunch 服务启动。
它的全键盘模式大概长这样(随系统配色可能有所不同):
通过该虚拟键盘可以不以提升权限或服务的方式通过 WMsg 发送 SAS 快捷键,这很让人困惑。
由于本人对 UWP 逆向的认识不多,所以暂时也没有做过多的挖掘研究,不过通过监视 API 我们发现似乎它通过 combase.dll 导出的的 I_Rpc 和 IPS_Factory 系列(继承于 IUnKnown)以及 MS-LRPC 接口工作,关键的模块是 TextInput.dll 和 InputApp.dll,它们是 ClientCBS 软件包的一部分。该程序原理需要未来去研究,现在暂且不谈。
(3)对于 CtrlAltDel 系统如何甄别虚拟按键
此外,我注意到目前文献有提到可以使用键盘按键按住 Ctrl + Alt ,由任何程序(原文是利用 osk.exe 虚拟键盘)发送的 Delete 将成功触发 SAS。也就是说 SAS 的模拟按键检测保护是有逻辑的。
经过我的试验,发现系统可能有两种检测模拟键盘输入的途径:
(1)当用户按下 CAD 三键组合时,实际上是习惯于先按下 Ctrl+Alt 最后才按下 Del,内核检测 Ctrl 和 Alt 组合是由键盘驱动发出时,进入警觉阶段,此时允许按下 Delete 键,但不检查 Del 键的发出是用户程序还是驱动程序;
(2)由于竞争条件,用户层和应用层只有先同时发出 Ctrl + Alt 的才能获得控制权(可以用户层一个键,另一个键由驱动发出,此时认为被用户占位),如果用户层使得检测序列进入了警觉阶段,内核层只检测三个键中是否至少有 Del 键是由键盘驱动发出的。
上面两种情况也可以成功激活 SAS,尽管标准的操作是三个键均由用户的键盘输入。这样的检测模式导致有一种极端情况,是即使三键都是用户键盘按下,但是先按住 Delete 不松开,随后一起按下 Ctrl 和 Alt 也不能激活 SAS 上下文,因为在非警觉阶段 Delete 键会被解释为删除。
(4)通过键盘过滤驱动模拟的潜在可能
理论上,我们还可以通过键盘过滤驱动来完成模拟按键,比如 VMware 虚拟机就是通过键盘过滤驱动来实现的。WinIO 是一种历史悠久的端口操作驱动框架,它通过一些技巧实现 DirectInput。如果你感兴趣,则可以参考本文最后列出的两篇比较经典的文章,以便于了解利用该驱动的实现。但是,目前没有找到公开的用于 Ctrl+Alt+Del 这种特殊的三键组合键上的实践。
2.2 实现原理和应用
我们的目标是编写服务程序,并在服务程序中调用 WmsgSendMessage 函数。
首先,通过逆向分析,我们发现 WmsgSendMessage 函数在 x86 和 x64 下的调用约定不同,这会导致执行函数时程序意外崩溃,下面是该函数正确的声明:
// x86 和 x64 函数的调用约定不同
#ifdef _WIN64
typedef RPC_STATUS(__fastcall* __WmsgSendMessage)(
DWORD dwClientId,
UINT uMachineState,
UINT uMsgWLGenericKey,
RPC_STATUS* pStatus
);
#else
typedef RPC_STATUS(__stdcall* __WmsgSendMessage)(
DWORD dwClientId,
UINT uMachineState,
UINT uMsgWLGenericKey,
RPC_STATUS* pStatus
);
#endif // _WIN64
随后,我们需要判断操作系统版本,Vista 及更高版本采用 Wmsg 模式,XP 及更早版本采用广播消息模式。通过下面的代码查询系统版本:
/**
* @brief 检查操作系统的版本是否高于 Vista
*
* @return TRUE 系统版本高于 Vista
* FALSE 系统版本低于 Vista
* @note
*/
BOOL IsVersionHigherVista()
{
typedef void(__stdcall* NTPROC)(DWORD*, DWORD*, DWORD*);
HINSTANCE hinst = GetModuleHandleW(L"ntdll.dll");// 加载DLL
NTPROC GetNtVersionNumbers = (NTPROC)
GetProcAddress(hinst, "RtlGetNtVersionNumbers");// 获取函数地址
if (!hinst || !GetNtVersionNumbers)
{
return FALSE;
}
DWORD dwMajor = 0, dwMinor = 0, dwBuildNumber = 0;
GetNtVersionNumbers(&dwMajor, &dwMinor, &dwBuildNumber);
// 判断大版本号
if (dwMajor >= 6)
return TRUE;
else
return FALSE;
}
随后,如果系统版本高于 Vista,执行 SAS 模拟时首先要检查注册表,确保系统安全策略允许模拟SAS,可以通过下面的代码实现维护注册表:
/**
* @brief Vista 及更高版本的操作系统在模拟 SAS 时,需要首先检查注册表配置
*
* @param[in] BOOL bEnable // 是否允许服务程序模拟 SAS
*
* @return TRUE 操作成功完成;
* FALSE 操作失败
* @note
*/
BOOL SetSASRegistryValue(BOOL bEnable)
{
HKEY hKey = NULL;
DWORD dwDisposition = 0;
DWORD dwData = bEnable ? 3 : 0; // 默认值为 3,bEnable 作为开关变量
// 尝试打开或创建注册表键
LONG lResult = RegCreateKeyEx(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System",
0,
NULL,
REG_OPTION_NON_VOLATILE,
KEY_READ | KEY_WRITE,
NULL,
&hKey,
&dwDisposition);
if (lResult == ERROR_SUCCESS) {
DWORD dwType = 0;
DWORD dwValue = 0;
DWORD dwSize = sizeof(DWORD);
// 检查值是否存在
lResult = RegQueryValueEx(hKey, L"SoftwareSASGeneration",
NULL, &dwType, (LPBYTE)&dwValue, &dwSize);
if (lResult == ERROR_SUCCESS) {
if (dwType == REG_DWORD && dwValue == dwData) {
// 值已存在且为 dwData,无需更新
RegCloseKey(hKey);
return TRUE;
}
else {
// 值存在但不为 dwData,更新值为 dwData
lResult = RegSetValueEx(hKey, L"SoftwareSASGeneration",
0, REG_DWORD, (BYTE*)&dwData, sizeof(DWORD));
RegCloseKey(hKey);
return (lResult == ERROR_SUCCESS);
}
}
else if (lResult == ERROR_FILE_NOT_FOUND) {
// 值不存在,创建并设置为 dwData
lResult = RegSetValueEx(hKey, L"SoftwareSASGeneration",
0, REG_DWORD, (BYTE*)&dwData, sizeof(DWORD));
RegCloseKey(hKey);
return (lResult == ERROR_SUCCESS);
}
else {
// 其他错误
RegCloseKey(hKey);
return FALSE;
}
}
else {
// 打开或创建键失败
return FALSE;
}
}
这其实是对应于组策略的:计算机配置 |管理模板 |Windows 组件 |Windows 登录选项 |禁用或启用软件安全注意序列。
由于 WmsgSendMessage 第一个参数需要传递活动登陆用户的会话 ID。而我们又是在服务程序中,所以不能使用 GetCurrentProcesssId() -> ProcessIdToSessionId() 的方式,因为“ Session 0 隔离”将导致我们获取到错误的会话 ID。这里有两种解决方案,第一种是使用 WTS 接口:
#include <wtsapi32.h>
#pragma comment(lib, "wtsapi32.lib")
DWORD GetCurrentActiveSessionID()
{
DWORD dwSessionID = 0;
PWTS_SESSION_INFO pSessionInfo = NULL;
DWORD dwCount = 0;
if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSessionInfo, &dwCount))
{
for (DWORD i = 0; i < dwCount; i++)
{
if (pSessionInfo[i].State == WTSActive)
{
dwSessionID = pSessionInfo[i].SessionId;
break;
}
}
WTSFreeMemory(pSessionInfo);
}
return dwSessionID;
}
上面的代码使用 WTS 直接枚举活动会话的 ID,请求的操作需要至少具有 SeDebugPrivilege 权限,并以管理员身份启动进程的控制台,我们以 LocalSystem 身份登陆的服务显然已经具有了更高级别的 SeTcbPrivilege 权限。
P.S. : 相关函数文档详情参考 MSDN:WTSEnumerateSessionsW 函数。
第二种是通过客户端传递其进程 ID,由 ProcessIdToSessionId 函数转换为会话 ID,该数据一并以结构体信息经由管道通信传递给服务进程。(两者均在我的代码中实现)
接下来就是通过 WmsgSendMessage 来完成 RPC 消息发送了,伪代码如下:
// 变量初始化
LONG bResponse = 0;
DWORD dwSessionId = 0; /* 活动会话 ID */
FARPROC lpWmsgSendMessage = NULL;
RPC_STATUS Ret = 0;
// 加载 WMsg LRPC 客户端接口
HMODULE hWMsgLib = LoadLibraryW(L"wmsgapi.dll");
if (hWMsgLib != NULL)
{
// 定位 WmsgSendMessage 函数,用于向特定服务终结点发送请求
lpWmsgSendMessage = GetProcAddress(hWMsgLib, "WmsgSendMessage");
if (lpWmsgSendMessage != NULL)
{
// 发送 0x208 消息,模拟 SAS 按键
bResponse = ((__WmsgSendMessage)lpWmsgSendMessage)(dwSessionId, 0x208, 0, &Ret);
FreeLibrary(hWMsgLib);
}
else { // 获取函数失败
bResponse = 50029;
LogEvent(50029, L"Get WmsgSendMessage Address Failed.");
FreeLibrary(hWMsgLib);
}
}
else { // 加载模块失败
bResponse = 50028;
LogEvent(50028, L"LoadLibrary Failed: wmsgapi.dll.");
}
由于我们考虑到服务和用户层交互以及实现复杂度等问题,最终还是选用了命名管道进行进程间通信。我们通过下面的代码来完成服务器对管理员用户进程(客户端)的应答:
/**
* @brief 管道通信时,服务器端处理消息的函数
*
* @param[in, out] LPPIPEINST pipe // 传递管道通信的结构体指针
*
* @return TRUE 允许回复客户端消息
* FALSE 不允许回复客户端消息
* @note
*/
BOOL GetAnswerToRequest(LPPIPEINST pipe)
{
// 检查空指针
if (pipe == nullptr)
{
LogEvent(50026, TEXT("NullPointerException Activate"));
return FALSE;
}
if (pipe->message.username == nullptr ||
pipe->message.password == nullptr ||
pipe->message.request == nullptr)
{
LogEvent(50026, TEXT("NullPointerException Activate"));
return FALSE;
}
// 验证客户端身份
if (!AuthenticateClient(pipe))
{
pipe->cbToWrite = 0;
DisconnectAndClose(pipe);
LogEvent(50031, TEXT("Canceling pipe link with client"));
return FALSE;
}
PWCHAR buffer = (WCHAR*)malloc(100 * sizeof(WCHAR));
memset(buffer, 0, 100 * sizeof(WCHAR));
// 根据客户端发送的不同消息生成不同的回复
if (_tcscmp(pipe->message.request, TEXT("SendSASMsg")) == 0)
{
LONG bResponse = 0;
// 如果是 XP 系统,则选择发送快捷键的模式
if (IsVersionHigherVista() == FALSE)
{
DWORD dwStatus = 0;
if (!SimulateSASInWinXP(&dwStatus))
{
bResponse = dwStatus; // 如果调用失败,返回非零错误码
}
}
else {
// 将客户端的 PID 转为十进制数字
DWORD clientPID = wcstoul(pipe->message.clientPID, NULL, 10);
DWORD dwSessionId = 0;
if(clientPID != 0) // 获取客户端进程的会话 ID
ProcessIdToSessionId(clientPID, &dwSessionId);
if (dwSessionId == 0) // 获取当前活动会话 ID
{
dwSessionId = GetCurrentActiveSessionID();
}
if (dwSessionId != 0)
{
HMODULE hWMsgLib = NULL;
FARPROC lpWmsgSendMessage = NULL;
RPC_STATUS Ret = 0;
// 尝试设置注册表以允许服务程序模拟 SAS
if (SetSASRegistryValue(TRUE)) {
LogEvent(50035, TEXT("SoftwareSASGeneration value is successfully updated.\n"));
// 加载 WMsg LRPC 客户端接口
hWMsgLib = LoadLibraryW(L"wmsgapi.dll");
if (hWMsgLib != NULL)
{
// 定位 WmsgSendMessage 函数,用于向特定服务终结点发送请求
lpWmsgSendMessage = GetProcAddress(hWMsgLib, "WmsgSendMessage");
if (lpWmsgSendMessage != NULL)
{
// 发送 0x208 消息,模拟 SAS 按键
bResponse = ((__WmsgSendMessage)lpWmsgSendMessage)(dwSessionId, 0x208, 0, &Ret);
FreeLibrary(hWMsgLib);
}
else { // 获取函数失败
bResponse = 50029;
LogEvent(50029, L"Get WmsgSendMessage Address Failed.");
FreeLibrary(hWMsgLib);
}
}
else { // 加载模块失败
bResponse = 50028;
LogEvent(50028, L"LoadLibrary Failed: wmsgapi.dll.");
}
}
else { // 设置注册表失败
bResponse = 50036;
LogEvent(50036, TEXT("Failed to update SoftwareSASGeneration value.\n"));
}
}
else {
// 获取 SessionId 失败
bResponse = 50027;
LogEvent(50027, L"GetSessionId Failed.");
}
// Vista 及以上系统的消息发送完成后,恢复注册表键值
::SetSASRegistryValue(FALSE);
}
// 根据消息的处理结果答复客户端
if (!bResponse)
{
swprintf_s(buffer, 100, L"The requested operation has been completed");
LogEvent(50024, L"The requested operation has been completed.");
}
else {
swprintf_s(buffer, 100, L"The requested operation failed (%u).", bResponse);
LogEvent(50025, L"The requested operation failed.");
}
}
else
{
swprintf_s(buffer, 100, L"Default Response from Server");
}
StringCchCopy(pipe->chReply, BUFSIZE, buffer);
pipe->cbToWrite = (lstrlen(pipe->chReply) + 1) * sizeof(TCHAR);
free(buffer);
return TRUE;
}
三、完整实现代码和测试
最后,我依然分享一下完整的实现代码。先说一下基本框架:工具包含两部分,分别是服务程序和客户端控制台程序,客户端程序用于请求服务发送 SAS 消息,也就是所谓的 Ctrl + Alt + Del ,或者请求关闭服务程序;服务程序内嵌了命令行终端,允许通过执行参数安装和卸载服务本体。客户端程序和服务程序通过命名管道通信,服务的管道通信是基于完成事件的队列机制,且由独立的线程管理。此外服务还通过创建消息线程允许接受服务控制器 / 管理器 (SC/SCM) 的关闭请求。
3.1 客户端控制台程序
由于代码写的比较随意,可能没全部补充注释,代码如下:
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <tchar.h>
#include <locale.h>
#define BUFSIZE 512
#define PIPE_NAME_STRING L"\\\\.\\pipe\\WMsgSimulate"
#define ServiceName L"WMsgSimulateServer"
typedef struct
{
TCHAR username[100];
TCHAR password[100];
TCHAR request[BUFSIZE];
TCHAR clientPID[15];
} Message;
BOOL CheckInstallAndEnableService(BOOL bEnable);
int _tmain(int argc, TCHAR* argv[])
{
setlocale(LC_ALL, ".utf8");
HANDLE hPipe;
Message message = { 0 };
BOOL fSuccess = FALSE;
DWORD cbRead, cbToWrite, cbWritten, dwMode;
LPCTSTR lpszPipename = PIPE_NAME_STRING;
// 获取并转换客户端进程ID
DWORD clientPID = GetCurrentProcessId();
// 初始化结构体信息
_tcscpy_s(message.username, _T("WMsgClientName"));
_tcscpy_s(message.password, _T("WMsgClientPassword"));
if (argc == 1)
{
_tcscpy_s(message.request, _T("Default request from client."));
}
else if (argc == 2) {
if (!_tcsicmp(argv[1], L"/shutdown")) // 关闭服务(再次确认)
{
_tprintf(TEXT("The current operation is terminating the service. Press \'y\' to continue?\n"));
int ch = _getch();
if (ch == 'y')
{
if (CheckInstallAndEnableService(FALSE) == TRUE)
{
_tprintf(TEXT("Service is closed.\n"));
}
}
_getch();
return 0;
}
else if (!_tcsicmp(argv[1], L"/shutdownnoreply")) // 关闭服务(强制)
{
CheckInstallAndEnableService(FALSE);
return 0;
}
else {
_tcscpy_s(message.request, argv[1]);
_stprintf_s(message.clientPID, _T("%lu"), clientPID);
_tprintf(TEXT("SendSASMsg with PID: %s\n"), message.clientPID);
}
}
else if (argc == 3)
{
_tcscpy_s(message.request, argv[1]);
if (!_tcscmp(argv[2], L"/nonpid"))
{
_stprintf_s(message.clientPID, _T("%lu"), 0);
_tprintf(TEXT("SendSASMsg without PID\n"));
}
else if(!_tcscmp(argv[2], L"/usepid")){
_stprintf_s(message.clientPID, _T("%lu"), clientPID);
_tprintf(TEXT("SendSASMsg with PID: %s\n"), message.clientPID);
}
else {
_tprintf(TEXT("Error invalid parameters.\n"));
return -1;
}
}
else {
_tprintf(TEXT("Error invalid parameters.\n"));
return -1;
}
// 检查服务状态并启动服务
if (!CheckInstallAndEnableService(TRUE))
{
_tprintf(TEXT("Service status failed general check.\n"));
return -1;
}
// 尝试打开命名管道
while (1)
{
hPipe = CreateFile(
lpszPipename, // pipe name
GENERIC_READ | // read and write access
GENERIC_WRITE,
0, // no sharing
NULL, // default security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
NULL); // no template file
// 如果管道句柄有效,则退出循环
if (hPipe != INVALID_HANDLE_VALUE)
break;
// 如果错误不是 ERROR_PIPE_BUSY,则退出程序
if (GetLastError() != ERROR_PIPE_BUSY)
{
_tprintf(TEXT("Could not open pipe. GLE=%d\n"), GetLastError());
return -1;
}
// 如果所有管道实例都在忙,则等待 20 秒
if (!WaitNamedPipe(lpszPipename, 20000))
{
printf("Could not open pipe: 20 second wait timed out.");
return -1;
}
}
// 设置管道模式为消息读取模式
dwMode = PIPE_READMODE_MESSAGE;
fSuccess = SetNamedPipeHandleState(
hPipe, // 管道句柄
&dwMode, // 新的管道模式
NULL, // 不设置最大字节数
NULL); // 不设置最大时间
if (!fSuccess)
{
_tprintf(TEXT("SetNamedPipeHandleState failed. GLE=%d\n"), GetLastError());
return -1;
}
// 向管道服务器发送消息
cbToWrite = sizeof(message);
_tprintf(TEXT("Sending %d byte message: \"%s\"\n"), cbToWrite, message.request);
fSuccess = WriteFile(
hPipe, // 管道句柄
&message, // 消息
cbToWrite, // 消息长度
&cbWritten, // 写入的字节数
NULL); // 不使用重叠
if (!fSuccess)
{
_tprintf(TEXT("WriteFile to pipe failed. GLE=%d\n"), GetLastError());
return -1;
}
printf("\nMessage sent to server, receiving reply as follows:\n");
// 从管道读取服务器的回复
fSuccess = ReadFile(
hPipe, // 管道句柄
message.request, // 用于接收回复的缓冲区
BUFSIZE * sizeof(TCHAR), // 缓冲区大小
&cbRead, // 读取的字节数
NULL); // 不使用重叠
if (!fSuccess)
{
_tprintf(TEXT("ReadFile from pipe failed. GLE=%d\n"), GetLastError());
return -1;
}
if (_tcscmp(message.request, TEXT("The requested operation has been completed")) == 0)
{
_tprintf(TEXT("The server returns a successful result.\n"));
}
else if (_tcscmp(message.request, TEXT("The requested operation failed")) == 0)
{
_tprintf(TEXT("The server notified us that the requested operation failed.\n"));
}
else {
_tprintf(TEXT("Server's reply: \"%s\"\n"), message.request);
}
_tprintf(TEXT("\n<End of message, press ENTER to exit without close service, press \'y\' to close service>\n"));
int ch = _getch();
if (ch == 'y')
{
CloseHandle(hPipe);
if (CheckInstallAndEnableService(FALSE) == TRUE)
{
_tprintf(TEXT("Service is closed. WindowsTerminal is closing.\n"));
Sleep(3000);
}
}
else {
CloseHandle(hPipe);
}
return 0;
}
BOOL CheckInstallAndEnableService(BOOL bEnable)
{
BOOL bResult = FALSE;
// 打开服务控制管理器
SC_HANDLE hSCM = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (hSCM != NULL)
{
// 打开服务
SC_HANDLE hService = OpenServiceW(hSCM, ServiceName,
SERVICE_QUERY_CONFIG | SERVICE_START | SERVICE_STOP | SERVICE_QUERY_STATUS);
if (hService != NULL)
{
SERVICE_STATUS status;
BOOL tagStatus = QueryServiceStatus(hService, &status);
if (tagStatus != FALSE)
{
if (bEnable)
{
if (status.dwCurrentState == SERVICE_STOPPED)
{
// 启动服务
if (StartServiceW(hService, NULL, NULL) == FALSE)
{
_tprintf(TEXT("Start Service failed.\n"));
bResult = FALSE;
}
else {
// 等待服务启动
while (QueryServiceStatus(hService, &status) == TRUE)
{
if (status.dwCurrentState == SERVICE_RUNNING)
{
bResult = TRUE;
break;
}
}
}
}
else if (status.dwCurrentState == SERVICE_RUNNING)
{
bResult = TRUE;
}
else {
_tprintf(TEXT("Service status unknown.\n"));
bResult = FALSE;
}
}
else {
if (status.dwCurrentState == SERVICE_RUNNING)
{
// 停止服务
if (ControlService(hService,
SERVICE_CONTROL_STOP, &status) == FALSE)
{
_tprintf(TEXT("Send Service Control Msg failed.\n"));
bResult = FALSE;
}
else {
// 等待服务停止
while (QueryServiceStatus(hService, &status) == TRUE)
{
Sleep(status.dwWaitHint);
if (status.dwCurrentState == SERVICE_STOPPED)
{
bResult = TRUE;
break;
}
}
}
}
else
{
bResult = TRUE;
}
}
}
else {
_tprintf(TEXT("Query Service status failed. Reinstalling the application may resolve this issue.\n"));
}
CloseServiceHandle(hService);
}
else {
_tprintf(TEXT("The service was not installed correctly.\n"));
}
CloseServiceHandle(hSCM);
}
else {
_tprintf(TEXT("Open SC Manager failed, Please run this program as an administrator.\n"));
}
return bResult;
}
3.2 服务程序
服务程序也没有单独整理代码,看完文章的你应该可以动手实现更好的代码,难道不是吗?
// SimulSASService.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <stdio.h>
#include <windows.h>
#include <tchar.h>
#include <strsafe.h>
#include <process.h>
#include <wtsapi32.h>
#pragma comment(lib, "wtsapi32.lib")
//#include <sas.h>
//#pragma comment(lib, "sas.lib")
#define PIPE_TIMEOUT 5000
#define BUFSIZE 512
#define MAX_USERNAME_LEN 100
#define MAX_PASSWORD_LEN 100
#define ServiceMachineState L"I_Event_ServiceMachineState"
#define ServicePipeEvent L"I_Event_ServicePipe"
#define PIPE_NAME_STRING L"\\\\.\\pipe\\WMsgSimulate"
#define SERVICE_DESCRIPTION_STRING \
L"This service is used to simulate users sending security attention sequences (SAS).\
Shutting down this service will result in client programs\
that rely on it being unable to successfully simulate events."
// 定义全局函数变量
TCHAR szServiceName[] = L"WMsgSimulateServer";
BOOL bInstall; // IsService Installed
SERVICE_STATUS_HANDLE hServiceStatus;
SERVICE_STATUS status;
UINT dwMsgThreadID; // msg thread TID
HANDLE hStartEvent; // msg thread start event
HANDLE hConnectEvent; // Pipe connect event
HANDLE hPipe = NULL; // pipe handle
// User authentication and request
TCHAR username[MAX_USERNAME_LEN] = { 0 };
TCHAR password[MAX_PASSWORD_LEN] = { 0 };
TCHAR request[BUFSIZE] = { 0 };
// 服务管道通信结构体
typedef struct
{
OVERLAPPED oOverlap;
HANDLE hPipeInst;
struct Message {
TCHAR username[MAX_USERNAME_LEN];
TCHAR password[MAX_PASSWORD_LEN];
TCHAR request[BUFSIZE];
TCHAR clientPID[15];
} message;
DWORD cbRead;
TCHAR chReply[BUFSIZE];
DWORD cbToWrite;
} PIPEINST, * LPPIPEINST;
// 定义函数声明
BOOL IsVersionHigherVista();
VOID Init();
BOOL IsInstalled();
BOOL Install();
BOOL Uninstall();
void LogEvent(DWORD EventType, LPCTSTR pszFormat, ...);
UINT WINAPI ServiceMsgHandlerThreadFunc(void* param);
UINT WINAPI ServerMainPipeHandler(void* param);
void WINAPI ServiceStrl(DWORD dwOpcode);
void WINAPI ServiceMain();
VOID DisconnectAndClose(LPPIPEINST);
BOOL CreateAndConnectInstance(LPOVERLAPPED);
BOOL ConnectToNewClient(HANDLE, LPOVERLAPPED);
BOOL GetAnswerToRequest(LPPIPEINST);
VOID WINAPI CompletedWriteRoutine(DWORD, DWORD, LPOVERLAPPED);
VOID WINAPI CompletedReadRoutine(DWORD, DWORD, LPOVERLAPPED);
// x86 和 x64 函数的调用约定不同
#ifdef _WIN64
typedef RPC_STATUS(__fastcall* __WmsgSendMessage)(
DWORD dwClientId,
UINT uMachineState,
UINT uMsgWLGenericKey,
RPC_STATUS* pStatus
);
#else
typedef RPC_STATUS(__stdcall* __WmsgSendMessage)(
DWORD dwClientId,
UINT uMachineState,
UINT uMsgWLGenericKey,
RPC_STATUS* pStatus
);
#endif // _WIN64
int _tmain(int argc, TCHAR* argv[])
{
// 初始化环境
Init();
SERVICE_TABLE_ENTRY st[] =
{
{ szServiceName, (LPSERVICE_MAIN_FUNCTION)ServiceMain },
{ NULL, NULL }
};
if (argc == 2)
{
if (_wcsicmp(argv[1], L"/install") == 0)
{
if (Install() == TRUE)
{
_tprintf(TEXT("The service has been successfully installed.\n"));
}
else {
_tprintf(TEXT("Install service failed.\n"));
}
}
else if (_wcsicmp(argv[1], L"/uninstall") == 0)
{
if (Uninstall() == TRUE)
{
_tprintf(TEXT("The service has been successfully uninstalled.\n"));
}
else {
_tprintf(TEXT("Uninstall service failed.\n"));
}
}
else
{
if (!StartServiceCtrlDispatcherW(st))
{
LogEvent(50000, L"Register Service Main Function Error!");
}
LogEvent(50023, L"Service stopped.\n");
}
}
else
{
if (!StartServiceCtrlDispatcherW(st))
{
LogEvent(50000, L"Register Service Main Function Error!");
}
LogEvent(50023, L"Service stopped.\n");
}
return 0;
}
/**
* @brief 服务程序初始化例程
*
* @note
*/
VOID Init()
{
hServiceStatus = NULL;
status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
status.dwCurrentState = SERVICE_STOPPED;
status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
status.dwWin32ExitCode = 0;
status.dwServiceSpecificExitCode = 0;
status.dwCheckPoint = 0;
status.dwWaitHint = 0;
}
/**
* @brief 服务主函数,这在里进行控制对服务控制的注册
*
* @note
*/
void WINAPI ServiceMain()
{
// Register the control request handler
status.dwCurrentState = SERVICE_START_PENDING;
status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
// 注册服务控制
hServiceStatus = RegisterServiceCtrlHandlerW(szServiceName, ServiceStrl);
if (hServiceStatus == NULL)
{
LogEvent(50009, L"Handler not installed");
return;
}
SetServiceStatus(hServiceStatus, &status);
// 服务运行
hStartEvent = CreateEventW(0, FALSE, FALSE, ServiceMachineState); //create thread start event
if (hStartEvent == 0)
{
LogEvent(50010, L"Create start event failed,errno:%d\n", ::GetLastError());
return;
}
// 运行服务消息处理线程
HANDLE hThread = NULL;
hThread = (HANDLE)_beginthreadex(NULL, 0, &ServiceMsgHandlerThreadFunc,
NULL, 0, &dwMsgThreadID);
if (hThread == 0)
{
LogEvent(50011, L"Start thread failed,errno:%d\n", GetLastError());
CloseHandle(hStartEvent);
return;
}
// wait thread start event to avoid PostThreadMessage return errno:1444
WaitForSingleObject(hStartEvent, INFINITE);
CloseHandle(hStartEvent);
// 运行管道通信线程
hThread = (HANDLE)_beginthreadex(NULL, 0, &ServerMainPipeHandler, NULL, 0, NULL);
if (hThread == 0)
{
LogEvent(50012, L"Start thread failed,errno:%d\n", GetLastError());
CloseHandle(hStartEvent);
return;
}
// 等待管道加载完成
while (WaitNamedPipeW(PIPE_NAME_STRING, 1000) == 0)
{
Sleep(300);
}
// 通知服务控制器服务启动完成
status.dwWin32ExitCode = S_OK;
status.dwCheckPoint = 0;
status.dwWaitHint = 0;
status.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(hServiceStatus, &status);
LogEvent(50001, L"Service is running!");
// 在服务消息线程关闭前保持主线程的运行状态
while (WaitForSingleObject(hThread, INFINITE) != FALSE)
{
Sleep(5000);
}
LogEvent(50031, L"Service main thread is exiting.");
// 通知服务控制器,服务例程即将结束
status.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus(hServiceStatus, &status);
}
/**
* @brief 服务控制主函数,这里实现对服务的控制,
* 当在服务管理器上停止或其它操作时,将会运行此处代码
*
* @param[in] DWORD dwOpcode
*
* @note
*/
void WINAPI ServiceStrl(DWORD dwOpcode)
{
switch (dwOpcode)
{
case SERVICE_CONTROL_STOP:
status.dwCurrentState = SERVICE_STOP_PENDING;
SetServiceStatus(hServiceStatus, &status);
PostThreadMessageW(dwMsgThreadID, WM_CLOSE, 0, 0);
break;
case SERVICE_CONTROL_PAUSE:
break;
case SERVICE_CONTROL_CONTINUE:
break;
case SERVICE_CONTROL_INTERROGATE:
break;
case SERVICE_CONTROL_SHUTDOWN:
break;
default:
LogEvent(50013, L"Bad service request");
}
}
/**
* @brief 服务消息处理线程,在该线程响应服务主线程回调函数发送的消息
*
* @param[in] void* param // 此参数暂未使用
*
* @return UINT 消息线程函数的返回值
* @note
*/
UINT WINAPI ServiceMsgHandlerThreadFunc(void* param)
{
LogEvent(50002, L"Service Message Thread start.\n");
MSG msg = { 0 };
PeekMessageW(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);
if (!SetEvent(hStartEvent)) //set thread start event
{
LogEvent(50003, L"Set start event failed,errno:%d\n", ::GetLastError());
return 1;
}
while (true)
{
if (GetMessageW(&msg, 0, 0, 0)) //get msg from message queue
{
switch (msg.message)
{
case WM_CLOSE:
LogEvent(50022, L"Service is stopping.\n");
ResetEvent(hConnectEvent);
CloseHandle(hConnectEvent);
hConnectEvent = NULL;
// Disconnect the pipe instance.
if (!DisconnectNamedPipe(hPipe))
{
LogEvent(50004, L"DisconnectNamedPipe failed with %d.\n", GetLastError());
}
// Close the handle to the pipe instance.
CloseHandle(hPipe);
hPipe = NULL;
// Check pipe is already Closed
while (WaitNamedPipeW(PIPE_NAME_STRING, 1000) != 0)
{
Sleep(300);
}
break;
}
}
}
return 0;
}
/**
* @brief 服务管道通信的主要例程
*
* @param[in] void* param // 此参数暂未使用
*
* @return UINT 管道线程函数的返回值
*
* @note
*/
UINT WINAPI ServerMainPipeHandler(void* param)
{
LogEvent(50008, L"Service Pipe is running.");
OVERLAPPED oConnect = { 0 };
LPPIPEINST lpPipeInst;
DWORD dwWait, cbRet;
BOOL fSuccess, fPendingIO;
// Create one event object for the connect operation.
hConnectEvent = CreateEventW(
NULL, // default security attribute
TRUE, // manual reset event
TRUE, // initial state = signaled
ServicePipeEvent); // unnamed event object
if (hConnectEvent == NULL)
{
LogEvent(50005, L"CreateEvent failed with %d.\n", GetLastError());
return 0;
}
oConnect.hEvent = hConnectEvent;
// Call a subroutine to create one instance, and wait for
// the client to connect.
fPendingIO = CreateAndConnectInstance(&oConnect);
while (1)
{
// Wait for a client to connect, or for a read or write
// operation to be completed, which causes a completion
// routine to be queued for execution.
if (hConnectEvent == NULL)
{
_endthreadex(0);
return 0;
}
dwWait = WaitForSingleObjectEx(
hConnectEvent, // event object to wait for
5000, // waits indefinitely
TRUE); // alertable wait enabled
switch (dwWait)
{
// The wait conditions are satisfied by a completed connect
// operation.
case 0:
// If an operation is pending, get the result of the
// connect operation.
if (fPendingIO)
{
fSuccess = GetOverlappedResult(
hPipe, // pipe handle
&oConnect, // OVERLAPPED structure
&cbRet, // bytes transferred
FALSE); // does not wait
if (!fSuccess)
{
LogEvent(50006, L"ConnectNamedPipe (%d)\n", GetLastError());
_endthreadex(0);
return 0;
}
}
// Allocate storage for this instance.
lpPipeInst = (LPPIPEINST)GlobalAlloc(
GPTR, sizeof(PIPEINST));
if (lpPipeInst == NULL)
{
LogEvent(50007, L"GlobalAlloc failed (%d)\n", GetLastError());
_endthreadex(0);
return 0;
}
lpPipeInst->hPipeInst = hPipe;
// Start the read operation for this client.
// Note that this same routine is later used as a
// completion routine after a write operation.
lpPipeInst->cbToWrite = 0;
CompletedWriteRoutine(0, 0, (LPOVERLAPPED)lpPipeInst);
// Create new pipe instance for the next client.
fPendingIO = CreateAndConnectInstance(
&oConnect);
break;
// The wait is satisfied by a completed read or write
// operation. This allows the system to execute the
// completion routine.
case WAIT_IO_COMPLETION:
break;
// An error occurred in the wait function.
default:
break;
}
}
return 0;
}
/**
* @brief 检查操作系统的版本是否高于 Vista
*
* @return TRUE 系统版本高于 Vista
* FALSE 系统版本低于 Vista
* @note
*/
BOOL IsVersionHigherVista()
{
typedef void(__stdcall* NTPROC)(DWORD*, DWORD*, DWORD*);
HINSTANCE hinst = GetModuleHandleW(L"ntdll.dll");// 加载DLL
NTPROC GetNtVersionNumbers = (NTPROC)
GetProcAddress(hinst, "RtlGetNtVersionNumbers");// 获取函数地址
if (!hinst || !GetNtVersionNumbers)
{
return FALSE;
}
DWORD dwMajor = 0, dwMinor = 0, dwBuildNumber = 0;
GetNtVersionNumbers(&dwMajor, &dwMinor, &dwBuildNumber);
// 判断大版本号
if (dwMajor >= 6)
return TRUE;
else
return FALSE;
}
/**
* @brief 检查系统中是否已经存在服务应用程序的配置
*
* @return TRUE 服务程序配置已经就绪;
* FALSE 没有检测到服务程序配置文件
* @note
*/
BOOL IsInstalled()
{
BOOL bResult = FALSE;
//打开服务控制管理器
SC_HANDLE hSCM = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (hSCM != NULL)
{
//打开服务
SC_HANDLE hService = OpenServiceW(hSCM, szServiceName, SERVICE_QUERY_CONFIG);
if (hService != NULL)
{
bResult = TRUE;
CloseServiceHandle(hService);
}
CloseServiceHandle(hSCM);
}
return bResult;
}
/**
* @brief 检查用于安装服务应用程序
*
* @return TRUE 成功安装服务程序;
* FALSE 安装服务程序失败
* @note
*/
BOOL Install()
{
if (IsInstalled())
return TRUE;
// 打开服务控制管理器
SC_HANDLE hSCM = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (hSCM == NULL)
{
MessageBoxW(NULL, L"Couldn't open service manager", szServiceName, MB_OK);
return FALSE;
}
// Get the executable file path
TCHAR szFilePath[MAX_PATH] = { 0 };
GetModuleFileNameW(NULL, szFilePath, MAX_PATH);
// 创建服务
SC_HANDLE hService = CreateServiceW(
hSCM, szServiceName, szServiceName,
SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
szFilePath, NULL, NULL, L"", NULL, NULL);
if (hService == NULL)
{
CloseServiceHandle(hSCM);
MessageBoxW(NULL, L"Couldn't create service", szServiceName, MB_OK);
return FALSE;
}
// Change service configuration to add description
SERVICE_DESCRIPTION serviceDescription = { 0 };
WCHAR lpDescription[] = SERVICE_DESCRIPTION_STRING;
serviceDescription.lpDescription = (LPWSTR)lpDescription;
if (!ChangeServiceConfig2W(hService, SERVICE_CONFIG_DESCRIPTION, &serviceDescription))
{
printf("ChangeServiceConfig2 failed (%d)\n", GetLastError());
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
return FALSE;
}
// printf("Service description added successfully.\n");
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
return TRUE;
}
/**
* @brief 检查用于卸载服务应用程序
*
* @return TRUE 成功卸载服务程序;
* FALSE 卸载服务程序失败
* @note
*/
BOOL Uninstall()
{
if (!IsInstalled())
return TRUE;
SC_HANDLE hSCM = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (hSCM == NULL)
{
MessageBox(NULL, L"Couldn't open service manager", szServiceName, MB_OK);
return FALSE;
}
SC_HANDLE hService = OpenServiceW(hSCM, szServiceName, SERVICE_STOP | DELETE);
if (hService == NULL)
{
CloseServiceHandle(hSCM);
MessageBox(NULL, L"Couldn't open service", szServiceName, MB_OK);
return FALSE;
}
SERVICE_STATUS status;
ControlService(hService, SERVICE_CONTROL_STOP, &status);
// 删除服务
BOOL bDelete = DeleteService(hService);
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
if (bDelete)
return TRUE;
LogEvent(50014, L"Service could not be deleted");
return FALSE;
}
/**
* @brief This function is used by the service program to send event log
records to the system, and important information of the service
will be recorded in the event log by
the operating system's event logging service.
*
* @param[in] DWORD EventType
* @param[in] LPCTSTR pFormat
* @param[in] ...
*
* @note
*/
void LogEvent(DWORD EventType, LPCTSTR pFormat, ...)
{
WCHAR chMsg[256] = { 0 };
HANDLE hEventSource = NULL;
LPTSTR lpszStrings[1] = { 0 };
va_list pArg = NULL;
va_start(pArg, pFormat);
vswprintf_s(chMsg, pFormat, pArg);
va_end(pArg);
lpszStrings[0] = chMsg;
hEventSource = RegisterEventSourceW(NULL, szServiceName);
if (hEventSource != NULL)
{
ReportEventW(hEventSource, EVENTLOG_INFORMATION_TYPE, 0, EventType, NULL, 1, 0, (LPCTSTR*)&lpszStrings[0], NULL);
DeregisterEventSource(hEventSource);
}
}
/**
* @brief This routine is called as a completion routine after writing to
the pipe, or when a new client has connected to a pipe instance.
It starts another read operation.
*
* @param[in] DWORD dwErr
* @param[in] DWORD cbWritten
* @param[in, out] LPOVERLAPPED lpOverLap
*
* @note
*/
VOID WINAPI CompletedWriteRoutine(DWORD dwErr, DWORD cbWritten,
LPOVERLAPPED lpOverLap)
{
LPPIPEINST lpPipeInst;
BOOL fRead = FALSE;
// lpOverlap points to storage for this instance.
lpPipeInst = (LPPIPEINST)lpOverLap;
// The write operation has finished, so read the next request (if
// there is no error).
if ((dwErr == 0) && (cbWritten == lpPipeInst->cbToWrite))
fRead = ReadFileEx(
lpPipeInst->hPipeInst,
&lpPipeInst->message,
sizeof(lpPipeInst->message),
(LPOVERLAPPED)lpPipeInst,
(LPOVERLAPPED_COMPLETION_ROUTINE)CompletedReadRoutine);
// Disconnect if an error occurred.
if (!fRead)
DisconnectAndClose(lpPipeInst);
}
/**
* @brief This routine is called as an I/O completion routine after reading
a request from the client. It gets data and writes it to the pipe.
*
* @param[in] DWORD dwErr
* @param[in] DWORD cbBytesRead
* @param[in, out] LPOVERLAPPED lpOverLap
*
* @note
*/
VOID WINAPI CompletedReadRoutine(DWORD dwErr, DWORD cbBytesRead,
LPOVERLAPPED lpOverLap)
{
LPPIPEINST lpPipeInst;
BOOL fWrite = FALSE;
BOOL fGetMsgRet = FALSE;
// lpOverlap points to storage for this instance.
lpPipeInst = (LPPIPEINST)lpOverLap;
// The read operation has finished, so write a response (if no
// error occurred).
if ((dwErr == 0) && (cbBytesRead != 0))
{
fGetMsgRet = GetAnswerToRequest(lpPipeInst);
// cbToWrite 为 0 时,表示拒绝处理客户端的请求,并且不返回消息
if (lpPipeInst->cbToWrite == 0 || fGetMsgRet == FALSE)
{
return;
}
fWrite = WriteFileEx(
lpPipeInst->hPipeInst,
lpPipeInst->chReply,
lpPipeInst->cbToWrite,
(LPOVERLAPPED)lpPipeInst,
(LPOVERLAPPED_COMPLETION_ROUTINE)CompletedWriteRoutine);
}
// Disconnect if an error occurred.
if (!fWrite)
DisconnectAndClose(lpPipeInst);
}
/**
* @brief This routine is called when an error occurs or the client closes
its handle to the pipe.
*
* @param[in, out] LPPIPEINST lpPipeInst
*
* @note
*/
VOID DisconnectAndClose(LPPIPEINST lpPipeInst)
{
if (lpPipeInst != NULL)
{
if (lpPipeInst->hPipeInst != NULL)
{
// Disconnect the pipe instance.
if (!DisconnectNamedPipe(lpPipeInst->hPipeInst))
{
printf("DisconnectNamedPipe failed with %d.\n", GetLastError());
}
// Close the handle to the pipe instance.
CloseHandle(lpPipeInst->hPipeInst);
}
// Release the storage for the pipe instance.
GlobalFree(lpPipeInst);
}
}
/**
* @brief This function creates a pipe instance and connects to the client.
It returns TRUE if the connect operation is pending, and FALSE if
the connection has been completed.
*
* @param[in, out] LPOVERLAPPED lpo
*
* @return TRUE connect operation is pending
* FALSE connection has been completed
* @note
*/
BOOL CreateAndConnectInstance(LPOVERLAPPED lpoOverlap)
{
LPCTSTR lpszPipename = PIPE_NAME_STRING;
hPipe = CreateNamedPipeW(
lpszPipename, // pipe name
PIPE_ACCESS_DUPLEX | // read/write access
FILE_FLAG_OVERLAPPED, // overlapped mode
PIPE_TYPE_MESSAGE | // message-type pipe
PIPE_READMODE_MESSAGE | // message read mode
PIPE_WAIT, // blocking mode
PIPE_UNLIMITED_INSTANCES, // unlimited instances
BUFSIZE * sizeof(TCHAR), // output buffer size
BUFSIZE * sizeof(TCHAR), // input buffer size
PIPE_TIMEOUT, // client time-out
NULL); // default security attributes
if (hPipe == INVALID_HANDLE_VALUE)
{
printf("CreateNamedPipe failed with %d.\n", GetLastError());
return 0;
}
// Call a subroutine to connect to the new client.
return ConnectToNewClient(hPipe, lpoOverlap);
}
/**
* @brief 管道通信时,服务器端建立新的管道连接的函数
*
* @param[in] HANDLE hPipe // 传递管道事件对象的句柄
* @param[in, out] LPOVERLAPPED lpo // 异步操作所需的结构体信息
*
* @return TRUE 连接成功
* FALSE 连接失败
* @note
*/
BOOL ConnectToNewClient(HANDLE hPipe, LPOVERLAPPED lpo)
{
BOOL fConnected, fPendingIO = FALSE;
// 检查空指针
if (lpo == nullptr)
{
LogEvent(50026, TEXT("NullPointerException Activate"));
return FALSE;
}
// Start an overlapped connection for this pipe instance.
fConnected = ConnectNamedPipe(hPipe, lpo);
// Overlapped ConnectNamedPipe should return zero.
if (fConnected)
{
printf("ConnectNamedPipe failed with %d.\n", GetLastError());
return FALSE;
}
switch (GetLastError())
{
// The overlapped connection in progress.
case ERROR_IO_PENDING:
fPendingIO = TRUE;
break;
// Client is already connected, so signal an event.
case ERROR_PIPE_CONNECTED:
if (SetEvent(lpo->hEvent))
break;
// If an error occurs during the connect operation...
default:
{
printf("ConnectNamedPipe failed with %d.\n", GetLastError());
return FALSE;
}
}
return fPendingIO;
}
/**
* @brief 管道通信时,服务器端验证客户端身份的函数
*
* @param[in, out] LPPIPEINST lpPipeInst // 传递管道通信的结构体指针
*
* @return TRUE 验证身份成功
* FALSE 验证身份失败
* @note
*/
BOOL AuthenticateClient(LPPIPEINST lpPipeInst) {
// 硬编码的用户名和密码列表
const TCHAR* validUsernames[] = { TEXT("WMsgClientName") };
const TCHAR* validPasswords[] = { TEXT("WMsgClientPassword") };
// 从 lpPipeInst 中获取客户端发送的身份验证信息(例如,用户名和密码)
memset(username, 0, sizeof(username));
memset(password, 0, sizeof(password));
memset(request, 0, sizeof(request));
// 添加末尾零字节,防止缓冲区溢出
lpPipeInst->message.username[MAX_USERNAME_LEN - 1] = 0;
lpPipeInst->message.password[MAX_PASSWORD_LEN - 1] = 0;
lpPipeInst->message.request[BUFSIZE - 1] = 0;
// 解析客户端发送的身份验证信息
_tcscpy_s(username, lpPipeInst->message.username);
_tcscpy_s(password, lpPipeInst->message.password);
_tcscpy_s(request, lpPipeInst->message.request);
// 检查用户名和密码是否在有效的用户名和密码列表中
for (int i = 0; i < sizeof(validUsernames) / sizeof(validUsernames[0]); i++) {
if (_tcscmp(username, validUsernames[i]) == 0 && _tcscmp(password, validPasswords[i]) == 0) {
// 用户名和密码匹配,身份验证成功
LogEvent(50016, TEXT("Client authentication is legal.\n"));
return TRUE;
}
}
// 如果用户名和密码不匹配任何有效的用户名和密码,身份验证失败
LogEvent(50015, TEXT("Invalid authentication.\n"));
return FALSE;
}
/**
* @brief 在 XP 系统下模拟 Ctrl + Alt + Delete 快捷键
*
* @param[out] PDWORD dwStatus // 传递操作失败的状态码
*
* @return TRUE 成功
* FALSE 失败
* @note
*/
BOOL SimulateSASInWinXP(PDWORD dwStatus)
{
HDESK hdeskCurrent;
HDESK hdesk;
HWINSTA hwinstaCurrent;
HWINSTA hwinsta;
DWORD_PTR dwResult;
BOOL bResult = FALSE;
LRESULT lResult = 0;
DWORD dwError;
// Save the current Window station
hwinstaCurrent = GetProcessWindowStation();
if (hwinstaCurrent == NULL)
{
*dwStatus = 0xC001;
return FALSE;
}
// Save the current desktop
hdeskCurrent = GetThreadDesktop(GetCurrentThreadId());
if (hdeskCurrent == NULL)
{
*dwStatus = 0xC002;
return FALSE;
}
// Obtain a handle to WinSta0 - service must be running
// in the LocalSystem account
hwinsta = OpenWindowStation(L"winsta0", FALSE,
WINSTA_ACCESSCLIPBOARD |
WINSTA_ACCESSGLOBALATOMS |
WINSTA_CREATEDESKTOP |
WINSTA_ENUMDESKTOPS |
WINSTA_ENUMERATE |
WINSTA_EXITWINDOWS |
WINSTA_READATTRIBUTES |
WINSTA_READSCREEN |
WINSTA_WRITEATTRIBUTES);
if (hwinsta == NULL)
{
*dwStatus = 0xC003;
return FALSE;
}
// Set the windowstation to be winsta0
if (!SetProcessWindowStation(hwinsta))
{
*dwStatus = 0xC004;
return FALSE;
}
// Get the default desktop on winsta0
hdesk = OpenDesktop(L"Winlogon", 0, FALSE,
DESKTOP_CREATEMENU |
DESKTOP_CREATEWINDOW |
DESKTOP_ENUMERATE |
DESKTOP_HOOKCONTROL |
DESKTOP_JOURNALPLAYBACK |
DESKTOP_JOURNALRECORD |
DESKTOP_READOBJECTS |
DESKTOP_SWITCHDESKTOP |
DESKTOP_WRITEOBJECTS);
if (hdesk == NULL)
{
*dwStatus = 0xC005;
return FALSE;
}
// Set the desktop to be "default"
if (!SetThreadDesktop(hdesk))
{
*dwStatus = 0xC006;
return FALSE;
}
// Use SendMessageTimeout to send msg and wait result event
SetLastError(0);
lResult = SendMessageTimeout(
HWND_BROADCAST, // 目标窗口句柄
WM_HOTKEY, // 消息类型
0, // wParam
MAKELONG(MOD_ALT | MOD_CONTROL, VK_DELETE), // lParam
SMTO_NORMAL, // 标志位
5000, // 超时时间(毫秒)
&dwResult // 接收消息结果的变量
);
bResult = (lResult > 0) ? 1 : 0;
if (!bResult) {
dwError = GetLastError();
if (dwError == ERROR_TIMEOUT) {
// 超时处理
LogEvent(50019, L"SendMessageTimeout: Timeout occurred.\n");
*dwStatus = 0xC007;
goto endFunc;
}
else {
// 其他错误处理
LogEvent(50020, L"SendMessageTimeout: Error occurred. Error code: %lu\n", dwError);
*dwStatus = 0xC008;
goto endFunc;
}
}
else {
// 消息发送成功
LogEvent(50021, L"SendMessageTimeout: Message sent successfully.\n");
*dwStatus = 0;
goto endFunc;
}
endFunc:
// Reset the Window station and desktop
if (!SetProcessWindowStation(hwinstaCurrent))
{
*dwStatus = 0xC010;
return FALSE;
}
if (!SetThreadDesktop(hdeskCurrent))
{
*dwStatus = 0xC011;
return FALSE;
}
// Close the windowstation and desktop handles
if (!CloseWindowStation(hwinsta))
{
*dwStatus = 0xC012;
return FALSE;
}
if (!CloseDesktop(hdesk))
{
*dwStatus = 0xC013;
return FALSE;
}
return bResult;
}
/**
* @brief Vista 及更高版本的操作系统在模拟 SAS 时,需要首先检查注册表配置
*
* @param[in] BOOL bEnable // 是否允许服务程序模拟 SAS
*
* @return TRUE 操作成功完成;
* FALSE 操作失败
* @note
*/
BOOL SetSASRegistryValue(BOOL bEnable)
{
HKEY hKey = NULL;
DWORD dwDisposition = 0;
DWORD dwData = bEnable ? 3 : 0; // 默认值为 3,bEnable 作为开关变量
// 尝试打开或创建注册表键
LONG lResult = RegCreateKeyEx(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System",
0,
NULL,
REG_OPTION_NON_VOLATILE,
KEY_READ | KEY_WRITE,
NULL,
&hKey,
&dwDisposition);
if (lResult == ERROR_SUCCESS) {
DWORD dwType = 0;
DWORD dwValue = 0;
DWORD dwSize = sizeof(DWORD);
// 检查值是否存在
lResult = RegQueryValueEx(hKey, L"SoftwareSASGeneration",
NULL, &dwType, (LPBYTE)&dwValue, &dwSize);
if (lResult == ERROR_SUCCESS) {
if (dwType == REG_DWORD && dwValue == dwData) {
// 值已存在且为 dwData,无需更新
RegCloseKey(hKey);
return TRUE;
}
else {
// 值存在但不为 dwData,更新值为 dwData
lResult = RegSetValueEx(hKey, L"SoftwareSASGeneration",
0, REG_DWORD, (BYTE*)&dwData, sizeof(DWORD));
RegCloseKey(hKey);
return (lResult == ERROR_SUCCESS);
}
}
else if (lResult == ERROR_FILE_NOT_FOUND) {
// 值不存在,创建并设置为 dwData
lResult = RegSetValueEx(hKey, L"SoftwareSASGeneration",
0, REG_DWORD, (BYTE*)&dwData, sizeof(DWORD));
RegCloseKey(hKey);
return (lResult == ERROR_SUCCESS);
}
else {
// 其他错误
RegCloseKey(hKey);
return FALSE;
}
}
else {
// 打开或创建键失败
return FALSE;
}
}
/**
* @brief 服务器枚举当前活动登陆用户的会话 ID
*
* @return DWORD 活动登陆用户的会话 ID
*
* @note
*/
DWORD GetCurrentActiveSessionID()
{
DWORD dwSessionID = 0;
PWTS_SESSION_INFO pSessionInfo = NULL;
DWORD dwCount = 0;
if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSessionInfo, &dwCount))
{
for (DWORD i = 0; i < dwCount; i++)
{
if (pSessionInfo[i].State == WTSActive)
{
dwSessionID = pSessionInfo[i].SessionId;
break;
}
}
WTSFreeMemory(pSessionInfo);
}
return dwSessionID;
}
/**
* @brief 管道通信时,服务器端处理消息的函数
*
* @param[in, out] LPPIPEINST pipe // 传递管道通信的结构体指针
*
* @return TRUE 允许回复客户端消息
* FALSE 不允许回复客户端消息
* @note
*/
BOOL GetAnswerToRequest(LPPIPEINST pipe)
{
// 检查空指针
if (pipe == nullptr)
{
LogEvent(50026, TEXT("NullPointerException Activate"));
return FALSE;
}
if (pipe->message.username == nullptr ||
pipe->message.password == nullptr ||
pipe->message.request == nullptr)
{
LogEvent(50026, TEXT("NullPointerException Activate"));
return FALSE;
}
// 验证客户端身份
if (!AuthenticateClient(pipe))
{
pipe->cbToWrite = 0;
DisconnectAndClose(pipe);
LogEvent(50031, TEXT("Canceling pipe link with client"));
return FALSE;
}
PWCHAR buffer = (WCHAR*)malloc(100 * sizeof(WCHAR));
memset(buffer, 0, 100 * sizeof(WCHAR));
// 根据客户端发送的不同消息生成不同的回复
if (_tcscmp(pipe->message.request, TEXT("SendSASMsg")) == 0)
{
LONG bResponse = 0;
// 如果是 XP 系统,则选择发送快捷键的模式
if (IsVersionHigherVista() == FALSE)
{
DWORD dwStatus = 0;
if (!SimulateSASInWinXP(&dwStatus))
{
bResponse = dwStatus; // 如果调用失败,返回非零错误码
}
}
else {
// 将客户端的 PID 转为十进制数字
DWORD clientPID = wcstoul(pipe->message.clientPID, NULL, 10);
DWORD dwSessionId = 0;
if(clientPID != 0) // 获取客户端进程的会话 ID
ProcessIdToSessionId(clientPID, &dwSessionId);
if (dwSessionId == 0) // 获取当前活动会话 ID
{
dwSessionId = GetCurrentActiveSessionID();
}
if (dwSessionId != 0)
{
HMODULE hWMsgLib = NULL;
FARPROC lpWmsgSendMessage = NULL;
RPC_STATUS Ret = 0;
// 尝试设置注册表以允许服务程序模拟 SAS
if (SetSASRegistryValue(TRUE)) {
LogEvent(50035, TEXT("SoftwareSASGeneration value is successfully updated.\n"));
// 加载 WMsg LRPC 客户端接口
hWMsgLib = LoadLibraryW(L"wmsgapi.dll");
if (hWMsgLib != NULL)
{
// 定位 WmsgSendMessage 函数,用于向特定服务终结点发送请求
lpWmsgSendMessage = GetProcAddress(hWMsgLib, "WmsgSendMessage");
if (lpWmsgSendMessage != NULL)
{
// 发送 0x208 消息,模拟 SAS 按键
bResponse = ((__WmsgSendMessage)lpWmsgSendMessage)(dwSessionId, 0x208, 0, &Ret);
FreeLibrary(hWMsgLib);
}
else { // 获取函数失败
bResponse = 50029;
LogEvent(50029, L"Get WmsgSendMessage Address Failed.");
FreeLibrary(hWMsgLib);
}
}
else { // 加载模块失败
bResponse = 50028;
LogEvent(50028, L"LoadLibrary Failed: wmsgapi.dll.");
}
}
else { // 设置注册表失败
bResponse = 50036;
LogEvent(50036, TEXT("Failed to update SoftwareSASGeneration value.\n"));
}
}
else {
// 获取 SessionId 失败
bResponse = 50027;
LogEvent(50027, L"GetSessionId Failed.");
}
// Vista 及以上系统的消息发送完成后,恢复注册表键值
::SetSASRegistryValue(FALSE);
}
// 根据消息的处理结果答复客户端
if (!bResponse)
{
swprintf_s(buffer, 100, L"The requested operation has been completed");
LogEvent(50024, L"The requested operation has been completed.");
}
else {
swprintf_s(buffer, 100, L"The requested operation failed (%u).", bResponse);
LogEvent(50025, L"The requested operation failed.");
}
}
else
{
swprintf_s(buffer, 100, L"Default Response from Server");
}
StringCchCopy(pipe->chReply, BUFSIZE, buffer);
pipe->cbToWrite = (lstrlen(pipe->chReply) + 1) * sizeof(TCHAR);
free(buffer);
return TRUE;
}
3.3 编译&测试程序
编译程序时需要勾选清单文件中的 UAC 执行级别,以便于程序(包括两者)拥有足够的令牌权限:
P.S.:
- 如果代码使用受信任机构签名,则可以考虑选择勾选 /uiAccess 为True。
- XP 系统需要在登录用户身份下运行命令行(右键菜单),Vista 及更高版本则需要在提升的命令提示符下运行程序。
- 通过提升的系统 sc.exe 工具安装/卸载服务,但我们推荐使用服务程序支持的 /install 或 /uninstall 参数进行标准安装/卸载服务。
- 服务的默认名称为:WMsgSimulateServer,管道的名称为“\\\\.\\pipe\\WMsgSimulate”。而这通过修改代码并重新编译实现修改(需要注意客户端程序的宏也需要同步修改)。
- 客户端支持的参数包括:(1)无参数:测试服务程序连接;(2)包含第一个参数如果为 “SendSASMsg”,表示发送 SAS;(2)第一个参数如果为 “SendSASMsg” 时,第二个参数为可选参数,如果填写 “/usepid” 或者不写入可选参数,表示使用当前客户端进程所在的会话发起调用,如果客户端不运行在活动的交互式会话下,则调用将失败;如果填写 “/nonpid” 则由服务器决定活动会话,客户端只发送 clientPID 字段表示数字 0 的请求;(3)如果第一个参数为 “/shutdown” 或者 “/shutdownNoReply” 则客户端将发送服务控制代码尝试终止服务;(4)如果包含三个及三个以上的参数,则不执行任何操作,并返回错误代码。
- 服务端会检查客户端请求的参数,必须包含 "SendSASMsg" (严格区分大小写)才模拟 SAS。
编译程序后测试如下:
首先是基本的安装卸载以及连接测试:
测试终止服务(命令 “/shutdown” 需要二次确认):
测试 ClientPID 模式:
测试 NonPID 模式:
测试意外参数:
四、总结&更新
关于 Ctrl + Alt + Del 快捷键的模拟,目前在 Vista 及更高版本上,成熟的方案就是使用 LocalSystem 服务进程,使用 WMsgAPI 库来完成,WMsgAPI 库是一个 RPC 客户端运行时模块,通过它的导出函数 WmsgSendMessage 发送魔数 0x208(十进制 520),来模拟 SAS,同时需要设置安全组策略。通过相关的分析研究,我总结了实现该方案的更多细节,总结了大量零碎的文献资料。我旨在进一步弄清楚 SAS 的细节,以及为远程软件需要的模拟 SAS 序列权衡找到一个更佳合适的解决方案。在这过程中,受限于目前掌握的思路,仍然有很多疑问未能够解决,希望读者有更好的解决方案。
参考文献
- SendSAS 函数 (sas.h) - Win32 apps | Microsoft Learn
- Simulate Ctrl-Alt-Del in Vista and above | Atelier Web
- XP、Wn7模拟发送ctrl+alt+delete组合键
- How TeamViewer simulates Ctrl-Alt-Del on Windows programmatically?
- Microsoft Windows Security | Microsoft Press Store
- SasLibEx Archives | Remko Weijnen's Blog (Remko's Blog)
- 安全性 - 为什么任何应用程序都无法拦截 control + alt + delete
- Simulate Control-Alt-Delete key sequence in Vista and XP
- Simulate Ctrl-Alt-Del | Jose's Blog (wordpress.com)[.Net 实现]
- Simulate Ctrl+Alt+Del Sendkeys、Send Ctrl Alt Del through INPUT Structure doesn't work?
- 奇特的Local System权限
- WTSEnumerateSessionsW 函数 (wtsapi32.h)
- 会话(Session)、窗口站(WindowsStation)、桌面、窗口
- WinIO3.0调用键盘实践-winxp、win7、win10下32位64位都适用
- 最近在玩WinIo遇到的问题 - trance (gitbook.io)
- 使用完成例程的命名管道服务器 - Win32 apps | Microsoft Learn
可以参考上面文章加深对本文的理解。
发布于:2024.02.10;更新于:2024.02.11。