I'm trying to create a simple application to record and playback a series of keyboard and mouse commands (macros). Read the documentation and concluded that the most suitable implementation (if not the only one) would be to set a Windows journal record hook (WH_JOURNALRECORD
) and play it back with a journal playback one (WH_JOURNAL_PLAYBACK
).
According to the documentation, these hooks don't need to reside in a DLL, instead they can be in an executable (application). So, I had the Visual Studio creating a simple Win32 application for me. It's a very classic application, registering a window class, creating the window, and running a message-loop. The documentation also mentions that the hook procedures for WH_JOURNALRECORD
/WH_JOURNAL_PLAYBACK
hooks run in the context of the thread that set them. However, it doesn't specifically mention what this thread should be doing, eg run a message-loop, sleep in an alertable state or what. So I just set the hook and run the message loop - it's the application's main and only thread. It's what some code samples I found also do, albeit they don't seem to work as of now, as they are quite old, and some Windows security updates have made things quite more difficult.
I believe I have taken all the necessary steps I have found in some samples and posts:
- Set the Manifest options "UAC Execution Level" to "requireAdministrator (/level='requireAdministrator')" and "UAC Bypass UI Protection" to "Yes (/uiAccess='true')".
- Created and installed a certificate - the application is signed with it after built.
- The executable is copied to System32 (trusted folder) and run from there "As Administator". Without the above actions, installation of the hook fails, with an error-code of 5 (Access denied).
I have managed to successfully (?) install the WH_JOURNALRECORD
hook (SetWindowsHookEx()
returns a non-zero handle), however the hook procedure is not called.
Below is my code (I have omitted the window class registration, window creation, window procedure and About dialog stuff, as there is nothing interesting or special in there - they do just the barebones):
// 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;
}
I monitored the application with the Spy++ tool, and found that when the hook is set, the application receives a series of successive WM_TIMER
timer messages, with a timer ID of 1, posted to the queue with the PostMessage()
function (P in Spy++). The window handle for these messages is reported to belong to the same application and thread as the main window, and its class name is "UserAdapterWindowClass". My code neither creates timers nor explicitly creates any window of this class, so apparently these are created by the system. Also, there is another message, with an ID of 0x0060 (Unknown!), posted to the same window only once, after the 1st or 2nd WM_TIMER
one.
The application appears to somehow "lock" the system (recording, waiting for a resource, or what?), until I press Alt+Ctrl+Del, which the documentation says stops recording (I have not yet implemented some mechanism to stop recording/uninstall the hook at will, so this is what I use so far). The hook procedure seems to never be called, and this is the problem I'm facing at this point. I have considered examining the code
parameter in the hook procedure and act accordingly, but I'm not even close to this, as the procedure is never called - and therefore it would have no effect - so I just call CallNextHookEx()
in there (and assume it would work well).
Notes:
- The hook procedure may occasionally be called just once, but it's rather rare (almost non-reproducible) and definitely not consistent, so I think I can't rely on this; the
code
parameter is 0 (HC_ACTION
). Tested it though, and returning either zero or non-zero, either callingCallNextHookEx()
or not, makes no difference at all. - I have also tried to set the hook in another thread (created after the main one has started processing messages), which then as well runs a message-loop, but I get exactly the same behavior.
Could someone please explain what may be wrong here? Checked some other posts too, esp these ones: SetWindowsHookEx for WH_JOURNALRECORD fails under Vista/Windows 7 and WH_JOURNALRECORD hook in Windows (C++) - Callback never called. , however couldn't find a solution, and I have to note that the usage conditions differ too. What I'm experiencing is closest to this SetWindowsHookEx(WH_JOURNALRECORD, ..) sometimes hang the system though in my case this happens always, not just "sometimes".
I would greatly appreciate any help.
I have uploaded the solution (sources + VS files, but not the .exe, .obj., .pch, .pdb etc file, so a rebuild is required) here, if anyone would like to take a look.
Thank you in advance
EDIT:
Tested the application under different configurations.
- Initially, the application was created in Visual Studio 2017, and tested under a Windows 10 Pro 32-bit version 1803, 2-core AMD processor computer (VS2017 is installed on this machine). Got the results described above.
- Then tested under a Windows 10 Pro 64-bit version 1903, 4-core AMD processor computer. This machine had a very old version of Visual Studio installed (although it was not involved in development and testing in any way). Installed the certificate and run the application (both created on the other machine). Initially the result was the same (hook proc never called).
- Tried deleting the certificate from the "PrivateCertStore" store location, leaving only the copy under "Trusted Root Certification Authorities" (this is the way the batch file I wrote, calling makecert and certmgr, stores the certificate, ie under both the above locations). Unexpected, but it worked, got multiple beeps as I moved the mouse! Tested it again, adding/removing/moving the certificate and the behaviour was fully reproducible: the certificate needed to be installed under "Trusted Root Certification Authorities" only, for the hook to work.
- Then repeated the above test on the 32-bit machine. It didn't work there, the application was "frozen" as before (getting only WM_TIMER messages).
- On the 64-bit machine, uninstalled the old VS version and quite a few Windows SDK versions installed there, and installed VS 2019 Community Edition. Rebuilt the application in VS2019 (created and installed the certificate anew, and signed the executable). The application now does not work under either machine (again, hook creation succeeds, but the hook proc is not called). Maybe the VS2019 installation, or some Windows Update has caused this.
- Identically, the application/certificate created on the 32-bit machine does not work under either machine now.
So it must be a Windows module or some requirements about the certificates which I don't meet, that cause this. Seacrhed on the internet a lot, but can't find which specs the certificate must conform to. For example, what are the requirement about the format, private key or the purposes (now I have all purposes checked, altough I also tried checking only Code Signing, which made no differnce). The makecert utility is now deprecated (the documentation suggests creating certificates with PowerShell instead) but I don't know if this is somehow related to my problem.
I have only come to two conclusions: - The certificate must be installed under "PrivateCertStore", for the application to be possible to be built (otherwise signing fails). Possibly because I have the /s PrivateCertStore option set in my script. But, - The certificate must be installed under "Trusted Root Certification Authorities", for the application to be able to be run (otherwise execution fails, with an access-denied error).