首页
社区
课程
招聘
[原创]如何让Apollo(Sunshine)串流时自动“熄屏”,屏蔽物理键鼠;串流结束后自动锁屏
发表于: 19小时前 227

[原创]如何让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配置

配置无头模式,修改显示器输出

image

image

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

image

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

image

添加暂停/恢复命令

image

如图所示,添加前置命令和后置命令。注意替换实际目录。

非编译版本

前置命令

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内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回