如何捕获各类Shell窗口中用户执行的命令(支持C#/C/C++)
解决方案:捕获控制台Shell窗口的输入命令
针对你的需求——通用捕获所有控制台类Shell窗口(CMD、PowerShell、WSL终端等)的输入命令,以下是基于Windows API的可靠方案,优先提供C#实现思路:
核心思路
所有Windows控制台窗口(包括CMD、PowerShell)都依托conhost.exe(控制台宿主进程)运行,共享一套系统级的控制台API。我们需要结合窗口活动监控和控制台缓冲区读取来实现命令捕获,而非单纯依赖键盘钩子或窗口标题这类局限性方法。
方案1:结合窗口活动监控与控制台缓冲区读取
这是最实用的通用方案,核心逻辑是:识别激活的控制台窗口,监听Enter键按下事件,然后直接读取控制台当前输入行的内容。
步骤拆解
- 识别控制台窗口:通过
GetClassNameAPI获取窗口类名,控制台窗口的类名固定为ConsoleWindowClass。 - 监听窗口激活事件:用
SetWinEventHook监听EVENT_SYSTEM_FOREGROUND事件,实时获取当前激活的窗口句柄。 - 监听Enter键按下:使用低级别键盘钩子
WH_KEYBOARD_LL,仅当活动窗口是控制台时处理按键事件。 - 读取控制台输入行:检测到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进程中直接监控控制台输入缓冲区,这是最底层的实现方式,兼容性最好但复杂度较高:
- 枚举所有
conhost.exe进程。 - 使用
CreateRemoteThread将DLL注入目标进程。 - 在DLL中通过
SetConsoleInputMode和ReadConsoleInput监控输入事件,捕获命令行内容。
为什么之前的方案无效?
- 键盘事件捕获:之前的实现未过滤非控制台窗口的按键,也未结合控制台缓冲区读取,无法区分快捷键与普通输入。
- 窗口标题提取:PowerShell窗口标题仅显示进程信息,不会实时更新当前输入的命令,因此仅适用于CMD。
- GetWindowText:该API只能获取窗口标题,无法访问控制台内部的输入缓冲区内容,自然无法捕获命令。
内容的提问来源于stack exchange,提问作者Saurabh Singh
相关产品推荐
相关产品推荐

