-
-
[原创]如何让Apollo(Sunshine)串流时自动“熄屏”,屏蔽物理键鼠;串流结束后自动锁屏
-
发表于: 19小时前 227
-
如何让Sunshine串流时自动“熄屏”,屏蔽物理键鼠;串流结束后自动锁屏
前言
远程串流搞完了,觉得串流时电脑键鼠仍然有输入,屏幕亮着,结束后不会锁屏不安全(也不想让别人看见)。于是自己搜,再把要求给AI写了一些东西。经过实际测试使用是没有问题的。
说明
屏蔽键鼠使用
C#程序,屏蔽物理键盘/鼠标/触控板输入,客户端串流设备的键盘/鼠标/触控板可以操作电脑。如果需要应急解除屏蔽,快速按3次esc。使用的是Sunshine的改版Apollo,原版Sunshine未经过测试。如需要安装Apollo,需要先卸载Sunshine,重启后再安装。
==声明==
==以下程序或脚本均由AI生成,如果使用过程中造成财产损失,本人概不负责。==
编译C#程序
主程序
input-blocker.cs
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
class InputBlocker {
// ── Win32 imports ──────────────────────────────
const int WH_KEYBOARD_LL = 13;
const int WH_MOUSE_LL = 14;
const uint LLKHF_INJECTED = 0x10;
const uint LLMHF_INJECTED = 0x01;
const uint PM_REMOVE = 1;
const int VK_ESCAPE = 0x1B;
delegate IntPtr LowLevelHookProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
static extern IntPtr SetWindowsHookEx(int idHook, LowLevelHookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll")]
static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll")]
static extern bool PeekMessage(out MSG msg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg);
[DllImport("user32.dll")]
static extern bool TranslateMessage(ref MSG msg);
[DllImport("user32.dll")]
static extern IntPtr DispatchMessage(ref MSG msg);
[StructLayout(LayoutKind.Sequential)]
struct MSG {
public IntPtr hwnd;
public uint message;
public IntPtr wParam;
public IntPtr lParam;
public uint time;
public int pt_x;
public int pt_y;
}
// ── 静态字段 ───────────────────────────────────
static LowLevelHookProc kbdProc;
static LowLevelHookProc mouseProc;
static IntPtr hHookKbd = IntPtr.Zero;
static IntPtr hHookMouse = IntPtr.Zero;
static EventWaitHandle stopEvent;
// ── 紧急逃生:连按 3 次 Esc(1.5 秒内)→ 解除 ──
static int escCount = 0;
static Stopwatch escWatch = Stopwatch.StartNew();
static long lastEscMs = 0;
// ── 拦截判定:是否为物理按键按下事件 ──
static bool IsPhysicalKeyDown(IntPtr wParam, IntPtr lParam, out int vkCode) {
int msg = (int)wParam;
// WM_KEYDOWN=0x0100, WM_SYSKEYDOWN=0x0104
if (msg != 0x0100 && msg != 0x0104) {
vkCode = 0;
return false;
}
uint flags = (uint)Marshal.ReadInt32(lParam, 8);
vkCode = Marshal.ReadInt32(lParam, 0);
return (flags & LLKHF_INJECTED) == 0;
}
// ── 钩子回调 ───────────────────────────────────
static IntPtr KbdHook(int nCode, IntPtr wParam, IntPtr lParam) {
if (nCode >= 0) {
int vkCode;
if (!IsPhysicalKeyDown(wParam, lParam, out vkCode))
return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
long now = escWatch.ElapsedMilliseconds;
if (vkCode == VK_ESCAPE) {
if (now - lastEscMs > 1500) escCount = 0;
lastEscMs = now;
escCount++;
if (escCount >= 3) {
stopEvent.Set();
return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
}
} else {
escCount = 0;
}
return (IntPtr)1; // 拦截所有物理按键(含 Alt/Win 系统组合键)
}
return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
}
static IntPtr MouseHook(int nCode, IntPtr wParam, IntPtr lParam) {
if (nCode >= 0) {
uint flags = (uint)Marshal.ReadInt32(lParam, 12);
if ((flags & LLMHF_INJECTED) == 0) return (IntPtr)1;
}
return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
}
// ── 消息循环 ───────────────────────────────────
static void MessageLoop(int timeoutSec) {
Stopwatch loopWatch = Stopwatch.StartNew();
MSG msg;
while (true) {
if (timeoutSec > 0) {
if (loopWatch.ElapsedMilliseconds / 1000 >= timeoutSec) break;
}
if (stopEvent.WaitOne(0)) break;
while (PeekMessage(out msg, IntPtr.Zero, 0, 0, PM_REMOVE)) {
TranslateMessage(ref msg);
DispatchMessage(ref msg);
}
Thread.Sleep(10);
}
}
// ── Main ───────────────────────────────────────
static void Main(string[] args) {
string eventName = @"Global\ApolloInputBlock";
int timeout = 0;
bool blocking = true;
for (int i = 0; i < args.Length; i++) {
switch (args[i]) {
case "-e": case "--event": eventName = args[++i]; break;
case "-t": case "--timeout": timeout = int.Parse(args[++i]); break;
case "-u": case "--unblock": blocking = false; break;
case "-b": case "--block": blocking = true; break;
}
}
if (!blocking) {
try {
var evt = EventWaitHandle.OpenExisting(eventName);
evt.Set();
Console.WriteLine("Signal sent. Input should be restored.");
} catch {
Console.WriteLine("No active blocking session found.");
}
return;
}
bool createdNew;
using (var mutex = new Mutex(true, eventName + ".Mutex", out createdNew)) {
if (!createdNew) {
Console.WriteLine("Another instance is already running.");
return;
}
try { stopEvent = EventWaitHandle.OpenExisting(eventName); stopEvent.Reset(); }
catch { stopEvent = new EventWaitHandle(false, EventResetMode.ManualReset, eventName); }
kbdProc = KbdHook;
mouseProc = MouseHook;
IntPtr hMod = GetModuleHandle(null);
hHookKbd = SetWindowsHookEx(WH_KEYBOARD_LL, kbdProc, hMod, 0);
hHookMouse = SetWindowsHookEx(WH_MOUSE_LL, mouseProc, hMod, 0);
if (hHookKbd == IntPtr.Zero || hHookMouse == IntPtr.Zero) {
Console.WriteLine(string.Format("Hook install failed. Kbd=0x{0:X} Mouse=0x{1:X}", hHookKbd, hHookMouse));
return;
}
Console.WriteLine("Hooks installed. Physical input BLOCKED. Timeout=" + timeout + "s");
Console.WriteLine("Emergency: press Esc 3 times within 1.5s to restore.");
MessageLoop(timeout);
UnhookWindowsHookEx(hHookKbd);
UnhookWindowsHookEx(hHookMouse);
Console.WriteLine("Hooks released. Input restored.");
}
}
}
编译
替换实际目录(比如替换成E:\User\Scripts)。如果需要不带控制台的,把/target:exe改为/target:winexe
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /out:"实际目录\input-blocker.exe" /target:exe "实际目录\input-blocker.cs"
使用Powershell脚本
创建以下两个Powershell脚本,存储到相应文件中。
需要把所有脚本中的$ScriptDir对应目录替换为实际目录。
该方案会在连接/断开的前后看到一个闪烁的控制台窗口,如果介意选择编译版本的。
apollo-disable-input.ps1
$ScriptDir = "E:\User\Scripts"
# 解除旧实例(带轮询)
& "$ScriptDir\input-blocker.exe" -u 2>$null
$retries = 0
while ((Get-Process -Name "input-blocker" -ErrorAction SilentlyContinue) -and $retries -lt 30) {
Start-Sleep -Milliseconds 100
$retries++
}
# 刷新壁纸
$RegPath = "HKCU:\Control Panel\Desktop"
$wallpaper = (Get-ItemProperty -Path $RegPath -Name WallPaper -ErrorAction SilentlyContinue).WallPaper
if ($wallpaper) {
if (-not ([System.Management.Automation.PSTypeName]'Wallpaper').Type) {
Add-Type -TypeDefinition @"
using System.Runtime.InteropServices;
public class Wallpaper {
[DllImport("user32.dll", SetLastError = true)]
public static extern bool SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni);
}
"@
}
[Wallpaper]::SystemParametersInfo(0x0014, 0, $wallpaper, 0x0002)
}
# 启动拦截
Start-Process "$ScriptDir\input-blocker.exe" -ArgumentList "-b" -WindowStyle Hidden
Write-Host "Input blocked."
apollo-enable-input.ps1
$ScriptDir = "E:\User\Scripts"
# 锁屏
rundll32.exe user32.dll, LockWorkStation
# 轮询检测锁屏是否已生效(LogonUI.exe 出现即表示锁屏界面已接管)
$maxWait = 10 # 最多等 10 秒,避免死循环
$waited = 0
while ($waited -lt $maxWait) {
if (Get-Process -Name "LogonUI" -ErrorAction SilentlyContinue) {
# 锁屏界面已出现,再等 500ms 确保完全接管输入
Start-Sleep -Milliseconds 500
break
}
Start-Sleep -Milliseconds 200
$waited += 0.2
}
# 恢复物理输入
& "$ScriptDir\input-blocker.exe" -u 2>$null
(可选版本)编译Powershell脚本为exe
需要替换以下两个脚本并编译。这样就不会产生可见控制台窗口。
apollo-disable-input.ps1
$ScriptDir = "E:\User\Scripts"
function Invoke-Silent($exe, $argStr, [switch]$NoWait) {
$p = New-Object System.Diagnostics.Process
$p.StartInfo.FileName = $exe
$p.StartInfo.Arguments = $argStr
$p.StartInfo.UseShellExecute = $false
$p.StartInfo.RedirectStandardOutput = $true
$p.StartInfo.RedirectStandardError = $true
$p.StartInfo.CreateNoWindow = $true
[void]$p.Start()
if (-not $NoWait) {
# 必须读完输出再等待,否则缓冲区满会死锁
[void]$p.StandardOutput.ReadToEnd()
[void]$p.StandardError.ReadToEnd()
$p.WaitForExit()
}
$p.Dispose()
}
# 解除旧实例
Invoke-Silent "$ScriptDir\input-blocker.exe" "-u"
$retries = 0
while ((Get-Process -Name "input-blocker" -ErrorAction SilentlyContinue) -and $retries -lt 30) {
Start-Sleep -Milliseconds 100
$retries++
}
# 刷新壁纸
$RegPath = "HKCU:\Control Panel\Desktop"
$wallpaper = (Get-ItemProperty -Path $RegPath -Name WallPaper -ErrorAction SilentlyContinue).WallPaper
if ($wallpaper) {
if (-not ([System.Management.Automation.PSTypeName]'Wallpaper').Type) {
Add-Type -TypeDefinition @"
using System.Runtime.InteropServices;
public class Wallpaper {
[DllImport("user32.dll", SetLastError = true)]
public static extern bool SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni);
}
"@ 2>$null
}
[void][Wallpaper]::SystemParametersInfo(0x0014, 0, $wallpaper, 0x0002)
}
# 启动拦截(不等待)
Invoke-Silent "$ScriptDir\input-blocker.exe" "-b" -NoWait
apollo-enable-input.ps1
$ScriptDir = "E:\User\Scripts"
function Invoke-Silent($exe, $argStr) {
$p = New-Object System.Diagnostics.Process
$p.StartInfo.FileName = $exe
$p.StartInfo.Arguments = $argStr
$p.StartInfo.UseShellExecute = $false
$p.StartInfo.RedirectStandardOutput = $true
$p.StartInfo.RedirectStandardError = $true
$p.StartInfo.CreateNoWindow = $true
[void]$p.Start()
[void]$p.StandardOutput.ReadToEnd()
[void]$p.StandardError.ReadToEnd()
$p.WaitForExit()
$p.Dispose()
}
# 锁屏
rundll32.exe user32.dll, LockWorkStation
# 轮询检测锁屏是否已生效(LogonUI.exe 出现即表示锁屏界面已接管)
$maxWait = 10
$waited = 0
while ($waited -lt $maxWait) {
if (Get-Process -Name "LogonUI" -ErrorAction SilentlyContinue) {
Start-Sleep -Milliseconds 500
break
}
Start-Sleep -Milliseconds 200
$waited += 0.2
}
# 恢复物理输入(静默)
Invoke-Silent "$ScriptDir\input-blocker.exe" "-u"
编译
Powershell安装ps2exe(开源)
Install-Module -Name ps2exe -Scope CurrentUser
启动管理员powershell,设置Set-ExecutionPolicy -Scope CurrentUser RemoteSigned。
PS C:\Windows\system32> Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
执行策略更改
执行策略可帮助你防止执行不信任的脚本。更改执行策略可能会产生安全风险,如 https:/go.microsoft.com/fwlink/?LinkID=135170
中的 about_Execution_Policies 帮助主题所述。是否要更改执行策略?
[Y] 是(Y) [A] 全是(A) [N] 否(N) [L] 全否(L) [S] 暂停(S) [?] 帮助 (默认值为“N”): Y
回车。然后执行Import-Module ps2exe(仅需一次)。再去编译。替换成实际目录。
ps2exe "实际目录\apollo-disable-input.ps1" "实际目录\apollo-disable-input.exe" -noConsole
ps2exe "实际目录\apollo-enable-input.ps1" "实际目录\apollo-enable-input.exe" -noConsole
结束后可以重新设置Set-ExecutionPolicy -Scope CurrentUser RemoteSigned为否。
修改Apollo配置
配置无头模式,修改显示器输出


需要开启无头模式。之后远程串流,将显示器输出修改为另一个显示器。

按需调整分辨率和缩放大小。

添加暂停/恢复命令

如图所示,添加前置命令和后置命令。注意替换实际目录。
非编译版本
前置命令
powershell -ExecutionPolicy Bypass -File "实际目录\apollo-disable-input.ps1"
后置命令
powershell -ExecutionPolicy Bypass -File "实际目录\apollo-enable-input.ps1"
编译版本
前置命令
"实际目录\apollo-disable-input.exe"
后置命令
"实际目录\apollo-enable-input.exe"
最后
你可以自行测试效果。如果遇到物理键鼠完全无响应的情况,可以 按住电源键强制重启。
注:Ctrl + Alt + Del没有被屏蔽。
[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。