使用 .NET 实现全局可拦截程序和系统挂钩






4.92/5 (22投票s)
介绍如何在 .NET 中实现全局可拦截挂钩
引言
本文将介绍一种在托管 .NET 中实现所有全局窗口挂钩类型的方法,可以在系统消息被目标应用程序处理之前拦截和修改它们。
背景
对于所有不了解挂钩是什么以及为什么需要它们的人,这里有一些快速信息
引用挂钩是系统消息处理机制中的一个点,应用程序可以在此点安装一个子例程来监视系统中的消息流量,并在某些类型的消息到达目标窗口过程之前对其进行处理。
如果您不想阅读有关挂钩的全部文档,请在继续之前查看以下文章
我做这件事的动机是,当我听说挂钩和窗口消息时,我在网上搜索,想找到 C# 中全局挂钩的实现。大多数挂钩是鼠标和键盘挂钩,因为它们不需要非托管 DLL。然后我发现了上述文章,它们做了更多的事情,但仍然不是全部。让我们引用 MSDN
引用.NET Framework 不支持全局挂钩 您无法在 Microsoft .NET Framework 中实现全局挂钩。
这听起来像是一个挑战。
在此,我想描述文章标题的命名法
- 全局:挂钩过程加载到系统中的每个进程中。
- 系统:如果发生系统事件,将调用挂钩过程。
- 程序:挂钩过程加载到一个特定的进程中。
最有用的挂钩
有些挂钩具有特殊属性
WH_CBT
:代表计算机辅助培训,在激活、创建、销毁、最小化、最大化、移动或调整窗口大小时,以及在移动鼠标或按下鼠标时被调用。它还可以阻止这些操作。WH_GETMESSAGE
:在消息到达窗口之前调用,并且可以拦截和修改发送到窗口的消息(在示例应用程序中使用)。WH_CALLWNDPROCRET
可以检查目标应用程序对此消息返回了什么返回值。WH_DEBUG
:可能非常有用,在调用任何其他hooktype
之前调用。如果设置全局调试挂钩,您可以看到操作系统执行的每个窗口消息和每个系统操作(GUI,而非网络或文件)。
目标
让我们快速浏览一下我们的目标
.NET 挂钩类应实现
- 为每种本机窗口挂钩类型提供一个**事件**,以及与挂钩相关的所有信息。
- **监控****特定**进程或系统中的所有进程(**全局**)。
- 一个**翻译器**
.ToString()
方法,它将hookproc
回调中的**所有信息**提取为单个人类可读的字符串。 - 一种挂钩和取消挂钩的方式。以及**更改和拦截窗口消息**。
- 使类的使用**直观**,并去除所有不必要的负担。
所有这些如何实现?很简单。
我们需要两个项目
- 一个具有一些导出的本机 C++ DLL
- 一个 C# 项目,它实现了对 1) 的封装器以及一些 Pinvoke 函数
首先,让我们看看我们的目标
var hook = new Hook(HookType.WH_CBT, true);
hook.HookTriggered += hook_HookTriggered;
void hook_HookTriggered(HookArguments Msg, ref bool Intercept)
{
var msg = new WH_CBT(Msg);
Console.WriteLine(msg.ToString());
}
以及一个通用挂钩
var hook = new Hook<WH_CBT>(true);
void hook_HookTriggered(WH_CBT Message, ref bool Intercept)
{
Console.WriteLine(Message.ToString());
}
1) 深入底层
要挂钩另一个窗口的消息,没有办法绕过将非托管 DLL 加载到目标进程中,并重定向/拦截即将被 messageloop
接收的消息。幸运的是,Windows API 提供了丰富的函数集,可以轻松挂钩。最重要的函数是 SetWindowsHookEx()
。此函数甚至具有全局挂钩的可能性。全局挂钩会将此 DLL 加载到每个可用的进程中。此 DLL 不能用托管代码编写,因为大多数进程不运行 .NET Framework。
对于两种类型的挂钩,编写本机 DLL 不是必需的:您仅在自己的应用程序(本地挂钩)中使用的挂钩,以及挂钩到键盘或鼠标的挂钩(WH_KEYBOARD_LL, WH_MOUSE_LL)。 无论如何,此 DLL 必须设计为可以在每个进程中并行工作。这是托管 .NET 功能性包装器的关键部分。
让我们开始吧。
要做的第一件事是定义一个 DLL 导出方法供 .NET 稍后使用
请记住,此 DLL 会被多次加载。这就是为什么我们需要一些共享数据段
// Shared data among all DLL instances.
#pragma comment(linker, "/SECTION:.SHARED,RWS")
#pragma data_seg(".SHARED")
HWND g_hWnd = NULL; // Window handle
HHOOK g_hHook = NULL; // Hook handle
INT hooktype = 0;
#pragma data_seg()
DWORD WINAPI SetHook(int HookType, BOOL bInstall, DWORD dwThreadId, HWND hWndCaller)
{
BOOL bOk = FALSE;
g_hWnd = hWndCaller;
g_hHook = NULL;
if (bInstall)
{
g_hHook = ::SetWindowsHookEx(HookType, AllHookProc,
ModuleFromAddress(AllHookProc), dwThreadId);
hooktype = HookType;
bOk = (g_hHook != NULL);
}
else
{
bOk = ::UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
return GetLastError();
}
此函数基本上是对 Windows.h ::SetWindowsHookEx
的封装。SetHook
函数将我们的 C# 应用程序窗口的句柄保存到共享数据段。此 DLL 反射其自身的模块,使用 ModuleFromAdress
,并加载到一个或多个进程中。此方法返回 LastWin32Error
,以便在 C# 代码中知道发生了什么错误。
下一步是定义 AllHookProc
函数,该函数应处理所有窗口挂钩回调。不变于 HookType
说明符。首先,让我们考虑所有挂钩共有的内容以及我们想要发送的额外信息。让我们看看每个挂钩是如何定义的:http://msdn.microsoft.com/en-us/library/windows/desktop/ms644959%28v=vs.85%29.aspx#procedures。
引用LRESULT CALLBACK HookProc( int nCode, WPARAM wParam, LPARAM lParam ) { // process event ... return CallNextHookEx(NULL, nCode, wParam, lParam); }
有趣。每个挂钩都有一个 nCode
和两个 pointers
以及一个 LRESULT
(int
或 pointer
)作为结果。LRESULT
值**应**(我们将打破这个规则来拦截)始终是 CallNextHookEx(NULL, nCode, wParam, lParam);
。这两个指针可能是实际信息,或者它们可能是指向包含更多指针和数据的结构的指针等等。由于历史原因,指针被命名为 WideParam
和 LongParam
。
当挂钩被触发时,会调用 HookProc()
函数。但现在怎么办?我们不是在自己的进程中执行,您还记得吗?我们的 DLL 在远程应用程序上执行,那么我们如何将数据传回我们的应用程序呢?嗯,我找到了一个救命的窗口消息,称为 WM_COPYDATA
。这听起来很像进程间通信,而且确实如此。
引用应用程序发送 **WM_COPYDATA** 消息以将数据传递给另一个应用程序。
关于 IPC,我不知道除了套接字、管道和共享内存之外还有其他东西。此窗口消息非常方便,因为它会自动将任何结构编组到另一个进程。我们找到了发送数据到另一个进程的方法。但是我们要发送什么?
当然是越多越好
HOOKDLL_API typedef struct AllHookMSG
{
INT HookType;
INT nCode;
WPARAM wParam;
LPARAM lParam;
DWORD Process;
time_t Time;
INT MilliSecond;
} HookMsg, *PHookMsg;
此结构可以由您随意扩展。此 struct
非常有用,因为它可以在可能的情况下收集尽可能多的**信息**,因为您实际上是在**另一个应用程序域**中执行自己的代码。
我选择发送 hookproc
的所有参数以及确切时间以及 ProcessId
。ProcessId
对于 .NET 项目非常重要,当您想找出实际调用此挂钩的进程时。
现在到实际的 hookproc
函数
// Hook callback called when hook triggers.
LRESULT CALLBACK AllHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode < 0) //this is a must (see hookproc documentation)
{
return ::CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
SYSTEMTIME st;
GetSystemTime(&st);
HookMsg info;
info.lParam = lParam;
info.wParam = wParam;
info.nCode = nCode;
info.Time = time(0);
info.MilliSecond = st.wMilliseconds;
info.Process = GetCurrentProcessId();
info.HookType = hooktype;
COPYDATASTRUCT InfoBoat;
InfoBoat.lpData = (PVOID)&info;
InfoBoat.cbData = sizeof(info);
InfoBoat.dwData = 0;
//very important line:
BOOL Intercept = SendMessage(g_hWnd, WM_COPYDATA, 0, (LPARAM)&InfoBoat);
if (Intercept)
{
if (info.HookType == WH_CBT) return -1;
return -1;
}
return ::CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
前 20 行收集信息并将其格式化以便与 WM_COPYDATA
传输。我称之为 Infoboat。http://msdn.microsoft.com/en-us/library/windows/desktop/ms649010%28v=vs.85%29.aspx
SendMessage
这一行非常重要。SendMessage
会阻塞直到 Message
被处理(与 PostMessage
不同)。SendMessage
正是我们需要的。这保证了进程不会收到任何消息,GUI 线程被阻塞,最重要的是,**上下文切换到了我们的 C# 应用程序**。
这一行也给了我们**拦截**挂钩链的机会(消息将不会传递给进程)。有些挂钩是可拦截的,有些则不是,我们无法改变这一点。SendMessage
会返回一个值,这个值在我们 C# 项目中定义。
高级读者可能会注意到,如果我们**拦截**了 HookProc
,它会返回 -1
,如果 HookType
是 WH_CBT
,它也返回 -1
。在两种情况下,我们都不会调用下一个挂钩。这证明您必须始终保持怀疑,即使对于最值得信赖的来源,因为 Microsoft 文档明确说明
引用对于对应于以下 CBT 挂钩代码的操作,返回值必须为 0 以允许该操作,或 1 以阻止该操作。
经验测试表明,仅返回 -1
才能阻止该操作。(这可以用来阻止窗口显示或销毁。基本上,您无法在目标应用程序中打开或退出任何窗口。)
您可能注意到的另一件事是,wParam
和 lParam
是按值复制的。这些指针在 .NET 进程中肯定无效。这个问题将在 .NET 项目中得到解决。
我们已经可以编写一个使用此 DLL 来使用所有 hooktypes
的 C++ 项目。但这并非目标。
2).NET 项目
我们首先要做的就是实现我们自己的函数和 struct
s(在一个名为 HookDll
的静态
类中)。
[StructLayout(LayoutKind.Sequential)]
struct AllHookMSG
{
public int HookType;
public int nCode;
public IntPtr wParam;
public IntPtr lParam;
public uint Process;
public long Time;
public int MilliSecond;
}
[
DllImport("HookDll.dll", CharSet = CharSet.Auto,
EntryPoint = "?SetHook@@YGKHHKPAUHWND__@@@Z",
ExactSpelling = false, CallingConvention = CallingConvention.StdCall)
]
public static extern uint SetHook(int HookType, bool bInstall,
[MarshalAs(UnmanagedType.U4)] UInt32 dwThreadId, IntPtr hWndCaller);
第二件事我们需要知道的是,只有窗体(win32 窗口)才能接收窗口消息。正如我们上面所见,我们希望接收我们的 Inforboat(AllHookMsg
)。如果我们想从控制台应用程序中使用我们的类怎么办?简单,我们定义自己的 Messageloop
类,它继承自 form。
在那里,我们可以重载 msgproc
方法并过滤 WM_COPYDATASTRUCT
。
class MessageLoop : Form
{
int filter = 0;
public IntPtr hWnd
{
get { return base.Handle; }
}
public MessageLoop(WindowsMessages Filter) : base ()
{
filter = (int)Filter;
base.FormBorderStyle = FormBorderStyle.FixedToolWindow;
base.ShowInTaskbar = false;
base.StartPosition = FormStartPosition.Manual;
base.Location = new System.Drawing.Point(-2000, -2000);
base.Size = new System.Drawing.Size(1, 1);
base.Show();
}
protected override void WndProc(ref Message m)
{
bool Intercept=false;
if (m.Msg==filter&&MessageCallback!=null)
{
MessageCallback(ref m,ref Intercept);
}
base.WndProc(ref m);
if (Intercept)
{
m.Result = new IntPtr(1);
}
}
public event dWndProc MessageCallback;
public delegate void dWndProc(ref Message m,ref bool Intercept);
}
这是整个 Messageloop
类实现,所以您可以直接复制并使用它。我们不继承 hook 自 form,因此我们没有所有重载。Messageloop
为我们创建了一个窗口,并完全将其隐藏起来。它还有一个用于接收 windowsmessages
的 eventhandler
。它还会过滤我们的 WH_COPYDATASTRUCT
消息。非常方便。
我们还看到了在 WndProc(ref Message m)
中返回 1
的可能性。如果您还记得 DLL 声明,其中说明:Intercept=SendMessage()
。因此,通过将 m.Result
设置为 1
,我们也告诉本机 DLL 拦截消息。
挂钩类
现在我们为 hook
类准备了几乎所有东西。我们需要定义所有 hooktype
windows 可以挂钩的类型
public enum HookType : int
{
WH_CALLWNDPROC = 4,
WH_CBT = 5,
WH_SYSMSGFILTER = 6,
WH_MOUSE = 7,
....
}
并非所有东西都已准备就绪,我们可以构建我们最终的 hook
类
MessageLoop MessageHandler;
public HookType HookType;
public Hook(HookType hooktype, Process ToWatch)
{
MessageHandler = new MessageLoop(WindowsMessages.WM_COPYDATA);
MessageHandler.MessageCallback += MessageHandler_WndProc;
HookType = hooktype;
uint TID = (uint)ToWatch.Threads[0].Id;
if (HookType == HookType.WH_SYSMSGFILTER) { TID = 0; }
uint HookEnabled = HookDll.SetHook((int)HookType,
true, TID, MessageHandler.Handle);
if (HookEnabled != 0) { throw new Win32Exception((int)HookEnabled); }
}
public void Dispose()
{
HookDll.SetHook(0, false, 0, IntPtr.Zero);
MessageHandler.Dispose();
}
这是 SetHook
的另一个包装器。所有这些抽象都是为了让事情尽可能简单。我们可以清楚地看到,现在我们可以挂钩到一个特定的目标进程。ThreadID
参数是进程的第一个线程(必须包含一个窗口),我们还设置了我们自己定义的 messageloop 的句柄。全局挂钩是此构造函数的另一个重载,并将 TID
设置为 0
。这将把本机 DLL 挂钩到所有当前打开的窗口。
只有当 WM_COPYDATASTRUCT
消息进入 messageloop
时,MessageCallback
才会被触发,我们定义自己的 messagecallback
如下
void MessageHandler_WndProc(ref Message m, ref bool Intercept)
{
if (HookTriggered == null) return;
var InfoBoat = (COPYDATASTRUCT)Marshal.PtrToStructure(m.LParam, typeof(COPYDATASTRUCT));
var HookInfo = (AllHookMSG)Marshal.PtrToStructure(InfoBoat.lpData, typeof(AllHookMSG));
var time = new System.DateTime(1970, 1, 1).AddSeconds(HookInfo.Time).ToLocalTime().AddMilliseconds(HookInfo.MilliSecond);
var process = Process.GetProcessById((int)HookInfo.Process);
//All above variables go into one wrapper class (HookEvent)
//This is our HookTriggered event which feeds everything to the user
HookTriggered(HookEvent AllAboveVariables,ref Intercept);
}
幸运的是,如上所述,windows 会将我们的 struct
编组到我们自己的内存中,因此当我们调用 Marshal.PtrToStructure
时一切正常。首先,我们提取 AllHookMsg
的指针,然后提取它。现在我们拥有了所有需要的东西。一个全局或特定的挂钩,一种拦截方式,以及与挂钩相关的所有信息。时间、进程以及 hookproc
的信息。当然。
那么我们现在做什么?**提取所有信息。**
翻译器类
现在我们有一个很大的问题。我们没有从 HookTriggered
事件中获得任何信息。有 lParam
和 wParam
以及 nCode
。**但没有信息**。
幸运的是,有一个变通方法。我们知道目标应用程序现在无法接收任何新消息,因为它仍然阻塞在 SendMessage
中。它无法接收任何东西,因此无法覆盖任何东西。lParam
和 wParam
**指向我们目标(远程)进程中的一个有效结构**。这些struct
取决于hooktype
,并且都记录在 MSDN 中。这里有一个例子
http://msdn.microsoft.com/en-us/library/windows/desktop/ms644976%28v=vs.85%29.aspx
引用
- lParam [in]
类型:LPARAM
指向 CWPRETSTRUCT 结构的指针,该结构包含有关消息的详细信息。
该解决方案之所以有效,是因为我们知道目标线程被阻塞了。我们可以直接**读取目标应用程序的进程内存**。请注意:所有翻译的消息在我们的事件方法之外都无效。这样,我们就不需要目标应用程序的合作,不需要 COM 对象,也不需要额外的 IPC。
[DllImport("kernel32.dll")]
static extern IntPtr OpenProcess
(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
[DllImport("kernel32.dll")]
static extern bool ReadProcessMemory(int hProcess, int lpBaseAddress,
byte[] lpBuffer, int dwSize, ref int lpNumberOfBytesRead);
const int PROCESS_VM_READ = 0x0010;
const int PROCESS_VM_WRITE = 0x0020;
const int PROCESS_VM_OPERATION = 0x0008;
这两个函数将一个 datablock
从任何进程复制到我们自己的进程。我们现在需要一种方法将其从 byte[]
转换为任何 struct
。这可以通过泛型来完成
public static T GetStructFromProcess<T>(Process Process,IntPtr Address) where T:struct
{
IntPtr ProcessHandle = OpenProcess(PROCESS_VM_READ, false, Process.Id);
int bytesrecieved = 0;
byte[] buffer = new byte[Marshal.SizeOf(typeof(T))];
bool Ok=ReadProcessMemory(ProcessHandle.ToInt32(),
Address.ToInt32(), buffer, buffer.Length, ref bytesrecieved);
if (!Ok) { throw new Win32Exception(Marshal.GetLastWin32Error()); }
return MarshalHelper.DeserializeMsg<T>(buffer);
}
static T DeserializeMsg<T>(Byte[] data) where T : struct
{
int objsize = Marshal.SizeOf(typeof(T));
IntPtr buff = Marshal.AllocHGlobal(objsize);
Marshal.Copy(data, 0, buff, objsize);
T retStruct = (T)Marshal.PtrToStructure(buff, typeof(T));
Marshal.FreeHGlobal(buff);
return retStruct;
}
我还编写了一个 WriteStructToProcess
方法,该方法理论上可以更改目标进程中的任何窗口消息和结构,即使是那些标记为不可拦截的。
现在我们将此代码放入一个助手类(MarshalHelper
)中,并且只需调用
CWPRETSTRUCT IsWMCOPY = MarshalHelper.GetStructFromProcess<CWPRETSTRUCT>
(process, PassData.lParam);
我们就快完成了。我为每种 HookType
创建了一个 messagetranslator
类(称为 WH_HookType
),并重写了 .ToString()
方法,使其更易于人类阅读。因此,任何挂钩使用的struct
都在 System.Hooks
命名空间中。
所以,这是 HookType.WH_MOUSE
的示例翻译器,称为 WH_MOUSE
public class WH_MOUSE : IHook
{
public int Code { get; private set; }
public IntPtr wParam { get; private set; }
public IntPtr lParam { get; private set; }
public Process Caller { get; private set; }
public DateTime Time { get; private set; }
new public const string Description = "The system calls this function whenever
an application calls the GetMessage or PeekMessage function and there is a
mouse message to be processed. ";
public override bool InterceptEffective
{
get
{
return true;
}
}
public INPUT_Messages Attachment
{
get
{
return (INPUT_Messages)Code;
}
}
public MouseMessages MouseMessage
{
get { return (MouseMessages)wParam; }
}
public bool KeyIsDown
{
get { return !Convert.ToBoolean(lParam.ToInt32() & (1 << 30));}
}
public Win32Window Above
{
get { return new Win32Window(MouseData.hwnd);}
}
public MOUSEHOOKSTRUCT MouseData
{
get {return MarshalHelper.GetStructFromProcess<MOUSEHOOKSTRUCT>(Caller, lParam);}
}
public override string ToString()
{
return MouseMessage + " event @ " + MouseData.pt +
" above " + (HitTest)MouseData.wHitTestCode+" at "+Caller.ProcessName;
}
public WH_MOUSE(HookArguments Msg): base(Msg)
{
if (Msg == null) { return; }
this.Code = Msg.nCode;
this.wParam = Msg.wParam;
this.lParam = Msg.lParam;
this.Caller = Msg.Process;
this.Time = Msg.TimeStamp;
}
}
我创建了 12 个这样的类,以使每个挂钩回调都易于人类阅读,并更容易编程或过滤特定事件。
这是 WH_CBT
的示例输出
示例应用程序
如果您只想自己尝试一下,请查看下载链接。
基本上,我们想使用我们的功能,并拦截和更改我们对远程记事本进程的输入。(我们不会使用 WH_KEYBOARD_LL
)。
我们创建一个新的窗体,然后挂钩一个 WH_GETMESSAGE
挂钩。为此,我们不必将 Intercept
设置为 true
,但我们可以直接设置 hook.message
(此 hooktype
的特殊性)。
我们将监视 WM_CHAR
事件,因为这是一个在按下字符到达记事本之前触发的事件。
using System.Hooks
//constructor
var k = new Hook(HookType.WH_GETMESSAGE, NotepadProcess);
k.HookTriggered += k_HookTriggered;
//hookcallback
void k_HookTriggered(HookArguments Msg, ref bool Intercept)
{
var hook = new WH_GETMESSAGE(Msg);
if (hook.Message.Msg == (int)WindowsMessages.WM_CHAR)
{
IntPtr character = new IntPtr(HelloWorld());
hook.Message = Message.Create(hook.Message.HWnd,
hook.Message.Msg, character, hook.Message.LParam);
}
}
我们将 WM_CHAR
中的 char
更改为我们自己的字符(字符代码是一个 IntPtr
)。
备注
如果在调试时设置全局挂钩,则无法设置断点,因为这会阻止所有窗口刷新其内容。您必须从任务管理器中杀死挂钩应用程序,一切都会恢复正常。
想法
以下是我们可以在挂钩方面做的一些事情
- 使任何进程崩溃
- 拦截按键、重绘和其他事件
- 修改发送到进程的消息(在示例应用程序中使用)
- 禁止启动某些进程(安全)
- 更改任何 GUI 元素中的文本
- 阻止窗口刷新其内容
- 调试应用程序
未完成的事项
单比特信息提取
此类库是针对本机 Win32 API 创建的。我已将大部分 Pinvoke 声明复制到命名空间,但仍缺少一些包装器。特别是将编码为 lParam
或 wParam
的单个比特的信息的翻译缺失。例如,wh_keyboard
回调的 lParam
。
最常见 Windows 消息的翻译
大多数挂钩回调返回指向 msg struct
的指针。它包含有关窗口消息的信息,例如 WM_CHAR
或 NTCHTEST
。应该有一个翻译器类来提取 Msg struct
中的信息。
递归结构到进程读/写
如上所示,有一种方法可以将本机结构复制/读取到/从目标进程。目前,只能复制 wparam
或 lparam
的值。如果这些值是指向其他 struct
的引用,则也应复制这些引用,而不会写入错误的内存位置。
将本机 DLL 编译为嵌入式数据
在 EXE 旁边有一个额外的文件不是最理想的解决方案,并且肯定有一种方法可以摆脱它。也许将其嵌入为资源并在运行时提取。
编译 64 位和 32 位本机 DLL
SetWindowsHookEx
方法将 32 位 DLL 注入到 32 位进程中,并将 64 位 DLL 注入到 64 位进程中。需要有两个 DLL 版本。
如果您找到任何这些问题的解决方案,请随时留下评论。
结束语
我们已经看到了如何创建一个挂钩类,该类不需要用户任何先验知识,就可以在托管 C# 程序中拦截、更改和监视全局或本地 windowsmessages
。
请注意使用这些类,因为它们为您提供了比纯 .NET Framework 更多的功能。
特别是全局挂钩。
它们为您提供了干扰其他进程的能力。部分代码在其他进程的命名空间中运行,部分函数直接写入进程内存。Windows 8(我测试过的版本)行为良好,不会将 globalhook
DLL 注入到 taskmanager
或 explorer 中,但仍然
如果您阻止或拦截回调,任何带有窗口的进程都将崩溃。
请**不要滥用这项工作**。例如,读取密码,因为这完全可以通过 WH_GETMASSAGE
实现,或者在拦截某些鼠标事件时让用户抓狂。
如果某些工作有错误或令人困惑/不一致,请告诉我。如果您发现任何错误或有有趣的内容要展示,请留下评论。
历史
- 2014 年 8 月 5 日:初始版本