我正在尝试创建一个简单的应用程序来记录和播放一系列键盘和鼠标命令(宏)。阅读文档并得出结论,最合适的实现(如果不是唯一的实现)是设置 Windows 日志记录挂钩(WH_JOURNALRECORD
)并用日志回放一个(WH_JOURNAL_PLAYBACK
).
根据文档,这些挂钩不需要驻留在 DLL 中,而是可以位于可执行文件(应用程序)中。因此,我让 Visual Studio 为我创建了一个简单的 Win32 应用程序。这是一个非常经典的应用程序,注册窗口类、创建窗口并运行消息循环。该文档还提到了钩子程序WH_JOURNALRECORD
/WH_JOURNAL_PLAYBACK
钩子在设置它们的线程的上下文中运行。但是,它没有具体提及该线程应该做什么,例如运行消息循环、在可警报状态下睡眠或什么。所以我只是设置钩子并运行消息循环 - 它是应用程序的主线程,也是唯一的线程。我发现的一些代码示例也是这样做的,尽管它们现在似乎无法工作,因为它们已经很旧了,而且一些 Windows 安全更新使事情变得更加困难。
我相信我已经采取了在一些示例和帖子中找到的所有必要步骤:
- 设置清单选项“UAC执行级别" to "requireAdministrator (/level='requireAdministrator')" and "UAC绕过UI保护" to "是 (/uiAccess='true')".
- 创建并安装证书 - 应用程序在构建后使用它进行签名。
- 可执行文件被复制到 System32(受信任的文件夹)并从那里“以管理员身份”运行。
如果不执行上述操作,挂钩安装将失败,错误代码为 5(访问被拒绝)。
我已经成功(?)安装了WH_JOURNALRECORD
hook (SetWindowsHookEx()
返回一个非零句柄),但是不会调用钩子过程。
下面是我的代码(我省略了窗口类注册、窗口创建、窗口过程和关于对话框的内容,因为其中没有什么有趣或特别的东西 - 它们只是做准系统):
// Not sure if these are needed, found it in some code samples
#pragma comment(linker, "/SECTION:.SHARED,RWS")
#pragma data_seg(".SHARED")
HHOOK hhJournal = NULL;
#pragma data_seg()
// Not sure if the Journal proc needs to be exported
__declspec(dllexport) LRESULT CALLBACK _JournalRProc(_In_ int code, _In_ WPARAM wParam, _In_ LPARAM lParam)
{
Beep(1000, 30); // Clumsy way to trace the JournalRProc calls
return CallNextHookEx(NULL, code, wParam, lParam);
}
void AddKMHooks(HMODULE _hMod)
{
if (hhJournal) return;
MessageBox(NULL, "Adding Hooks", szTitle, MB_OK | MB_ICONINFORMATION | MB_TASKMODAL);
hhJournal = SetWindowsHookEx(WH_JOURNALRECORD, _JournalRProc, _hMod, 0);
if (!hhJournal)
{
CHAR s[100];
wsprintf(s, "Record Journal Hook Failed!\nThe Error-Code was %d", GetLastError());
MessageBox(NULL, s, szTitle, MB_OK | MB_ICONSTOP | MB_TASKMODAL);
}
}
void RemoveKMHooks()
{
if (!hhJournal) return;
MessageBox(NULL, "Removing Hooks", szTitle, MB_OK | MB_ICONINFORMATION | MB_TASKMODAL);
UnhookWindowsHookEx(hhJournal);
hhJournal = NULL;
}
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: Place code here.
// Initialize global strings
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_KMRECORD, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// Perform application initialization:
if (!InitInstance(hInstance, nCmdShow)) return FALSE;
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_KMRECORD));
AddKMHooks(hInstance);
// Calling AddKMHooks(GetModuleHandle(NULL)) instead, delivers the same results
MSG msg;
// Main message loop:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// Once the hook has is set the GetMessage() call above
// always returns a WM_TIMER message with a timer ID of 1,
// posted to the queue with the PostMessage() function,
// as the Spy++ tool reports
RemoveKMHooks();
return (int) msg.wParam;
}
我用Spy++工具监控应用程序,发现当设置钩子时,应用程序会收到一系列连续的WM_TIMER
计时器消息,计时器 ID 为 1,发布到队列中PostMessage()
函数(Spy++ 中的 P)。据报告,这些消息的窗口句柄与主窗口属于同一应用程序和线程,其类名称为“UserAdapterWindowClass”。我的代码既不创建计时器,也不显式创建此类的任何窗口,因此显然这些是由系统创建的。此外,还有另一条消息,ID 为 0x0060(未知!),仅在第一次或第二次之后发布到同一窗口一次WM_TIMER
one.
该应用程序似乎以某种方式“锁定”系统(录制、等待资源,或者什么?),直到我按 Alt+Ctrl+Del,文档说停止录制(我还没有实现某种机制来停止录制/随意卸载钩子,所以这是我目前使用的)。钩子过程似乎永远不会被调用,这就是我目前面临的问题。我考虑过检查code
钩子过程中的参数并采取相应的行动,但我还没有接近这一点,因为该过程从未被调用 - 因此它不会产生任何效果 - 所以我只是调用CallNextHookEx()
在那里(并假设它会运作良好)。
Notes:
- 钩子过程有时可能只被调用一次,但这种情况相当罕见(几乎不可重现)并且绝对不一致,所以我认为我不能依赖于此;这
code
参数为 0 (HC_ACTION
)。但测试了它,并返回零或非零,或者调用CallNextHookEx()
或不,根本没有区别。
- 我还尝试在另一个线程中设置挂钩(在主线程开始处理消息后创建),然后该线程也运行消息循环,但我得到了完全相同的行为。
有人可以解释一下这里可能出了什么问题吗?还检查了其他一些帖子,特别是这些:WH_JOURRNALRECORD 的 SetWindowsHookEx 在 Vista/Windows 7 下失败 https://stackoverflow.com/questions/9165666/setwindowshookex-for-wh-journalrecord-fails-under-vista-windows-7 and Windows (C++) 中的 WH_JOURNALRECORD 挂钩 - 从未调用过回调。 https://stackoverflow.com/questions/3157212/wh-journalrecord-hook-in-windows-c-callback-never-called/3157256,但是找不到解决方案,而且我必须注意使用条件也不同。我所经历的最接近这个SetWindowsHookEx(WH_JOURRNALRECORD, ..) 有时会挂起系统 https://stackoverflow.com/questions/29335012/setwindowshookexwh-journalrecord-sometimes-hang-the-system尽管就我而言,这种情况总是会发生,而不仅仅是“有时”。
我将非常感谢任何帮助。
我已经上传了解决方案(源+ VS 文件,但不是 .exe、.obj.、.pch、.pdb 等文件,因此需要重建)here https://github.com/GCoves81/KbdMouseRecorderSource,如果有人想看的话。
先感谢您
EDIT:
在不同配置下测试应用程序。
- 最初,该应用程序是在 Visual Studio 2017 中创建的,并在 Windows 10 Pro 32 位版本 1803、2 核 AMD 处理器计算机(本机上安装了 VS2017)下进行了测试。得到了上面描述的结果。
- 然后在Windows 10 Pro 64位版本1903、4核AMD处理器电脑下进行测试。这台机器安装了一个非常旧版本的Visual Studio(尽管它没有以任何方式参与开发和测试)。安装证书并运行应用程序(均在另一台计算机上创建)。最初的结果是相同的(钩子过程从未被调用)。
- 尝试从“PrivateCertStore”存储位置删除证书,只保留“受信任的根证书颁发机构”下的副本(这是我编写的批处理文件调用 makecert 和 certmgr 存储证书的方式,即在上述两个位置下) 。出乎意料,但它有效,当我移动鼠标时听到多次蜂鸣声!再次测试,添加/删除/移动证书,行为完全可重现:只需将证书安装在“受信任的根证书颁发机构”下,挂钩即可工作。
- 然后在32位机器上重复上述测试。它在那里不起作用,应用程序像以前一样被“冻结”(仅获取 WM_TIMER 消息)。
- 在64位机器上,卸载旧的VS版本和安装的相当多的Windows SDK版本,并安装VS 2019社区版。在 VS2019 中重建应用程序(重新创建并安装证书,并对可执行文件进行签名)。该应用程序现在无法在任何一台机器上运行(同样,钩子创建成功,但未调用钩子过程)。可能是 VS2019 安装或某些 Windows 更新导致了此问题。
- 同样,在 32 位计算机上创建的应用程序/证书现在在任何一台计算机上都不起作用。
所以它一定是 Windows 模块或我不满足的一些有关证书的要求,导致了这种情况。在互联网上搜索了很多,但找不到证书必须符合哪些规范。例如,对格式、私钥或用途有什么要求(现在我已经检查了所有用途,尽管我也尝试过仅检查代码签名,这没有什么区别)。 makecert 实用程序现已弃用(文档建议使用 PowerShell 创建证书),但我不知道这是否与我的问题有关。
我只得出两个结论:
- 证书必须安装在“PrivateCertStore”下,才能构建应用程序(否则签名失败)。可能是因为我在脚本中设置了 /s PrivateCertStore 选项。但,
- 证书必须安装在“受信任的根证书颁发机构”下,应用程序才能运行(否则执行会失败,并出现拒绝访问错误)。