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

如何捕获各类Shell窗口中用户执行的命令(支持C#/C/C++)

解决方案:捕获控制台Shell窗口的输入命令

针对你的需求——通用捕获所有控制台类Shell窗口(CMD、PowerShell、WSL终端等)的输入命令,以下是基于Windows API的可靠方案,优先提供C#实现思路:

核心思路

所有Windows控制台窗口(包括CMD、PowerShell)都依托conhost.exe(控制台宿主进程)运行,共享一套系统级的控制台API。我们需要结合窗口活动监控和控制台缓冲区读取来实现命令捕获,而非单纯依赖键盘钩子或窗口标题这类局限性方法。


方案1:结合窗口活动监控与控制台缓冲区读取

这是最实用的通用方案,核心逻辑是:识别激活的控制台窗口,监听Enter键按下事件,然后直接读取控制台当前输入行的内容。

步骤拆解

  1. 识别控制台窗口:通过GetClassName API获取窗口类名,控制台窗口的类名固定为ConsoleWindowClass。
  2. 监听窗口激活事件:用SetWinEventHook监听EVENT_SYSTEM_FOREGROUND事件,实时获取当前激活的窗口句柄。
  3. 监听Enter键按下:使用低级别键盘钩子WH_KEYBOARD_LL,仅当活动窗口是控制台时处理按键事件。
  4. 读取控制台输入行:检测到Enter键后,附加到目标控制台进程,读取其屏幕缓冲区中光标所在行的内容。

C#关键实现代码

using System;
using System.Runtime.InteropServices;
using System.Text;

public class ConsoleCommandCapture
{
    // 导入必要的Windows API
    [DllImport("user32.dll")]
    private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

    [DllImport("user32.dll")]
    private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);

    [DllImport("user32.dll")]
    private static extern bool UnhookWinEvent(IntPtr hWinEventHook);

    [DllImport("kernel32.dll")]
    private static extern bool AttachConsole(uint dwProcessId);

    [DllImport("kernel32.dll")]
    private static extern bool FreeConsole();

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetStdHandle(int nStdHandle);

    [DllImport("kernel32.dll")]
    private static extern bool GetConsoleScreenBufferInfo(IntPtr hConsoleOutput, out CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo);

    [DllImport("kernel32.dll")]
    private static extern bool ReadConsoleOutputCharacter(IntPtr hConsoleOutput, StringBuilder lpCharacter, uint nLength, COORD dwReadCoord, out uint lpNumberOfCharsRead);

    [DllImport("user32.dll")]
    private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

    // 定义委托与结构体
    private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
    private const uint EVENT_SYSTEM_FOREGROUND = 3;
    private const uint WINEVENT_OUTOFCONTEXT = 0;
    private const int STD_OUTPUT_HANDLE = -11;

    private struct COORD { public short X; public short Y; }
    private struct CONSOLE_SCREEN_BUFFER_INFO
    {
        public COORD dwSize;
        public COORD dwCursorPosition;
        public ushort wAttributes;
        public SMALL_RECT srWindow;
        public COORD dwMaximumWindowSize;
    }
    private struct SMALL_RECT { public short Left; public short Top; public short Right; public short Bottom; }

    private IntPtr _eventHook;
    private IntPtr _currentConsoleWnd;

    public void StartCapture()
    {
        // 注册窗口激活事件钩子
        _eventHook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, new WinEventDelegate(OnForegroundWindowChanged), 0, 0, WINEVENT_OUTOFCONTEXT);
        // 启动键盘钩子监听Enter键
        KeyboardHook.StartHook(OnKeyPressed);
    }

    public void StopCapture()
    {
        UnhookWinEvent(_eventHook);
        KeyboardHook.StopHook();
    }

    private void OnForegroundWindowChanged(IntPtr hook, uint eventType, IntPtr hwnd, int idObj, int idChild, uint threadId, uint eventTime)
    {
        StringBuilder className = new StringBuilder(256);
        GetClassName(hwnd, className, className.Capacity);
        _currentConsoleWnd = className.ToString() == "ConsoleWindowClass" ? hwnd : IntPtr.Zero;
    }

    private void OnKeyPressed(int keyCode)
    {
        // 仅处理控制台窗口的Enter键按下事件
        if (_currentConsoleWnd == IntPtr.Zero || keyCode != 13) return;

        // 获取目标控制台进程ID
        GetWindowThreadProcessId(_currentConsoleWnd, out uint processId);
        
        if (AttachConsole(processId))
        {
            try
            {
                IntPtr hConsoleOut = GetStdHandle(STD_OUTPUT_HANDLE);
                GetConsoleScreenBufferInfo(hConsoleOut, out CONSOLE_SCREEN_BUFFER_INFO csbi);
                
                // 计算输入行的起始位置与长度
                COORD readCoord = new COORD { X = csbi.srWindow.Left, Y = csbi.dwCursorPosition.Y };
                uint commandLength = (uint)(csbi.dwCursorPosition.X - csbi.srWindow.Left);
                StringBuilder capturedCommand = new StringBuilder((int)commandLength);
                
                ReadConsoleOutputCharacter(hConsoleOut, capturedCommand, commandLength, readCoord, out _);
                
                Console.WriteLine($"Captured command: {capturedCommand.ToString().Trim()}");
            }
            finally
            {
                FreeConsole(); // 必须释放,避免绑定目标控制台影响后续操作
            }
        }
    }
}

// 简易低级别键盘钩子实现
public static class KeyboardHook
{
    private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
    private static LowLevelKeyboardProc _proc = HookCallback;
    private static IntPtr _hookID = IntPtr.Zero;

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

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    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);

    public static event Action<int> OnKeyPressed;

    public static void StartHook()
    {
        _hookID = SetWindowsHookEx(13, _proc, GetModuleHandle(null), 0); // WH_KEYBOARD_LL = 13
    }

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

    private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)0x100) // 仅处理WM_KEYDOWN事件
        {
            int vkCode = Marshal.ReadInt32(lParam);
            OnKeyPressed?.Invoke(vkCode);
        }
        return CallNextHookEx(_hookID, nCode, wParam, lParam);
    }
}

注意事项

  • 权限要求:程序需要以管理员权限运行,否则无法附加到高权限的控制台进程。
  • 多窗口兼容:每次捕获后必须调用FreeConsole,避免当前进程绑定到单个控制台,影响其他窗口的捕获。
  • 多行命令处理:对于PowerShell的多行命令(如未闭合的代码块),Enter键仅换行不执行,可通过简单的语法检查(如判断未闭合的括号、引号)过滤这类情况。

方案2:注入conhost.exe进程(C/C++实现,更底层)

如果方案1存在权限或兼容性问题,可采用C/C++编写DLL,注入到conhost.exe进程中直接监控控制台输入缓冲区,这是最底层的实现方式,兼容性最好但复杂度较高:

  1. 枚举所有conhost.exe进程。
  2. 使用CreateRemoteThread将DLL注入目标进程。
  3. 在DLL中通过SetConsoleInputMode和ReadConsoleInput监控输入事件,捕获命令行内容。

为什么之前的方案无效?

  • 键盘事件捕获:之前的实现未过滤非控制台窗口的按键,也未结合控制台缓冲区读取,无法区分快捷键与普通输入。
  • 窗口标题提取:PowerShell窗口标题仅显示进程信息,不会实时更新当前输入的命令,因此仅适用于CMD。
  • GetWindowText:该API只能获取窗口标题,无法访问控制台内部的输入缓冲区内容,自然无法捕获命令。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 04:20:59