2

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 calling CallNextHookEx() 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).

Constantine Georgiou
  • 2,412
  • 1
  • 13
  • 17
  • "*The executable is copied to System32 (trusted folder)*" - have you tried using `Program Files` instead of `Sytem32`? Do you have the same problem? – Remy Lebeau Sep 20 '19 at 22:30
  • @RemyLebeau Thanks for commenting. Yes, i tried installing the application under Program Files and under a folder there. Even made a installer. The result was unfortunately the same in all cases. Also disabled temporarily Windows Defender Firewall, Antivirus, Malware Protection, Windows Discovery etc, but this didn't help either. There's something I seem to be missing. – Constantine Georgiou Sep 21 '19 at 14:40
  • @ConstantineGeorgiou Your project works for me on Windows 10. I can hear Beep voice continuously. What's your Windows version? – Rita Han Sep 24 '19 at 07:46
  • I encountered similar issues while doing some testing a while back.Just tried (my code) again and get the same issues you do. Yet if I start a Windows 10 Sandbox all works fine, the Sandbox VM has no AV running.. Test on Windows 10.1903.18362.295 – FredS Oct 06 '19 at 22:21
  • This worked for me: [Return Zero unless code<0 then return result of CallNextHookEx](https://edn.embarcadero.com/print/10323) – FredS Oct 07 '19 at 18:43
  • @RitaHan-MSFT Thanks for your testing and reporting, this actually gave me some ideas (test it on another machine). And sorry for not replying immediately, but I took some time to make more tests and attempts. I posted my findings in the **EDIT** section in my post. – Constantine Georgiou Oct 14 '19 at 19:31
  • @FredS Thanks for commenting. At least I now know that the problem is not due to my software and is not specific to my machine. Unfortunately the Sandbox won't do it I'm afraid, as the application will have to record/playback macros for another application, which won't be running under Sandbox. As for hook proc, thanks, but I'm not even getting there yet. And what do you mean by "AV"? – Constantine Georgiou Oct 14 '19 at 19:32
  • The point was that if it works on a Sandbox then its either a Security/Certificate issues or Anti-Virus (AV). Also 'home rolled' certificates may not work for this.. – FredS Oct 15 '19 at 15:23
  • FYI: after updating to W10.1903.18362.356 the same EXE now fails by locking up. Can still Play a recording but no longer record. – FredS Oct 17 '19 at 02:19

0 Answers0