最近发现某Desk远程桌面产品有一个很不错的功能,在软件上保存好目标计算机的密码之后,下次远程可以实现自动激活锁屏界面-自动输入密码-实现自动化登录,用起来很是舒畅,于是学习了一下相关的资料,实现了关键功能的模拟,这里为大家介绍一下这个能力实现的核心路径,附带的代码也是本地登录的核心代码,大家可以根据自己的需要进行改造,比如改成云端实时动态传输密码进行自动登录等,都可以,下面进入正题。
Windows 的锁定状态属于“会话”,因此首先要明确目标会话。本项目通过 WTSGetActiveConsoleSessionId() 获取当前物理控制台会话,再查询该会话的 WTSSessionInfoEx 信息。
识别时检查三件事:会话为 WTSActive、已经存在登录用户,以及 SessionFlags 明确为 WTS_SESSIONSTATE_LOCK 或 WTS_SESSIONSTATE_UNLOCK。WTSActive 只表示会话连接状态,不能单独说明已经解锁。查询失败、未登录或断开连接应作为“未知/不适用”处理,不能直接当作解锁。接口定义参见 [WTSINFOEX_LEVEL1_W][1] 和 [WTSQuerySessionInformationW][2]。
在服务监视循环中,只有查询成功时才更新 LockSchedule。调度器在首次观察到锁屏时设置 10 秒后的截止时间,后续轮询不推迟截止时间;到期领取一次任务后标记为已尝试;观察到解锁才清除该轮状态。服务每 500ms 轮询一次,会话通知同时用于唤醒轮询。具体实现见 main.cpp 的 Monitor() 和 schedule.h。
这使“等待期间手动解锁”能够取消任务,也使“密码错误”不会触发同一轮的持续重试。服务刚启动时若发现已经锁屏,10 秒从服务第一次观察到该状态开始计算,并非追溯用户实际锁屏的时间。
本文限定 Windows 10/11。若扩展到 Windows 7/Server 2008 R2,需注意微软文档记录的锁定/解锁标记反转问题,不能直接复用当前判断。[WTSINFOEX_LEVEL1_W][1]
这里使用的是软件安全注意序列 SAS(Secure Attention Sequence)。项目调用系统 sas.dll 导出的 SendSAS,由 Windows 处理登录界面的激活流程。它和普通窗口消息、拼接 Ctrl+Alt+Del 键盘事件具有不同的调用条件。[SendSAS][3]
首先要准备正确的执行上下文。服务本体留在 Session 0;worker 则通过复制服务主令牌、设置目标 TokenSessionId,再调用 CreateProcessAsUserW 进入目标会话。其 STARTUPINFO.lpDesktop 设为 winsta0\Winlogon。项目中这部分由 SpawnWorker() 完成,服务启动时还会检查 LocalSystem 身份并启用所需特权。
worker 在初始化 COM 和创建窗口之前调用 OpenDesktopW、SetThreadDesktop。已有窗口或钩子的线程切换桌面可能失败,因此该操作应尽早完成。桌面句柄在 worker 生命周期内保持有效。[SetThreadDesktop][4]
下面将项目 Worker() 的起始部分整理为一个辅助函数,突出“身份、会话、桌面”的准备顺序:
其中 SecureDesktopActive() 通过 OpenInputDesktop 打开当前输入桌面,再用 GetUserObjectInformationW(..., UOI_NAME, ...) 确认名称为 Winlogon。这一步只确认桌面条件,密码控件是否已出现仍要在 Step 3 中检查。
SendSAS 是受限制的接口;以管理员身份启动一个普通程序,并不能直接推导它具备相同能力。微软文档要求调用应用以服务方式运行,或者满足 UIAccess 条件;非服务调用还涉及调用者身份、UAC、签名与受保护安装路径等要求。项目在 LocalSystem worker 中执行该调用,但 worker 本身并不是由 SCM 注册运行的服务,因此“由服务启动且为 SYSTEM”不应直接视为满足文档中所有服务调用条件,实际目标系统上的行为仍需验证。[SendSAS][3]
项目安装脚本对 SoftwareSASGeneration 采用保留原能力的方式:未配置/0 设置为 1,2 设置为 3,已有 1/3 保持。它保存原值供卸载恢复。该策略允许服务生成 SAS,但不保证后续 UI Automation 或 SendInput 一定被允许。
激活后,项目还会发送一次空格以推进界面,再进入密码框识别循环;空格必须经过 Step 4 所示的输入桌面校验函数。SAS、桌面切换、密码框出现是连续的异步过程,需要逐步观察状态。
这里的“光标定位”是让密码编辑框获得键盘焦点。项目使用 UI Automation 从当前 LogonUI 窗口查找密码元素,再调用 IUIAutomationElement::SetFocus(),随后重新读取当前焦点进行复核。[SetFocus][5]
UI Automation 初始化在桌面绑定之后进行:调用 CoInitializeEx(nullptr, COINIT_MULTITHREADED),然后以 CLSID_CUIAutomation 创建 IUIAutomation。项目的 Com<T> 负责接口释放,成功初始化 COM 后还应对应调用 CoUninitialize()。
项目组合了以下条件:目标会话仍锁定、当前输入桌面为 Winlogon、前台进程属于同一会话且路径为系统目录中的 LogonUI.exe,焦点元素同时满足编辑框类型、密码属性、可用状态和允许的字段标签。
IsLogonUIProcess() 会通过 ProcessIdToSessionId 检查会话,再通过 QueryFullProcessImageNameW 比较系统目录中的完整可执行文件路径。它是一组降低误操作概率的检查,不等同于对进程文件的签名验证,也无法把“检查焦点”和“发送输入”变成原子操作。
如果界面当前选择 PIN,项目还会用 ClickNamedButton() 查找“登录选项 / Sign-in options”和“密码 / Password”按钮,并通过 UI Automation 的 Invoke 模式切换。切换后继续轮询并验证密码框;12 秒内仍未确认就结束本轮,且不读取密码文件。该逻辑依赖目标系统和凭据提供程序暴露的 UI Automation 元素,需要针对实际 Windows 界面验证。