3

I have diagnostic program that uses SetWindowsHookEx and WH_KEYBOARD_LL to scan codes system-wide I'd like to extend it to monitor window focus changes, which is a possibility using SetWindowsHookEx and the computer-based training CBT hook WH_CBT.

For the WH_KEYBOARD_LL hook, I was able to put the hook function in my process and it worked, capturing keypresses in just about every application window on my desktop. My understanding is that WH_CBT actually needs to be in a separate dll so that it can be injected into other processes. So I've done this.

I'm also aware that this imposes a bit-ness requirement - If my dll is 64-bit, I can't inject it into 32-bit processes, and vice-versa.

Anyways, I tried it out in the VS2008 debugger, and sure enough, I saw OutputDebugString output (my handler calls OutputDebugString). But only in Visual Studio and in DebugView - when I switched focus to DebugView, DebugView would show the focus-change string output. When I switched back to the VS debugger, the VS output window would show the focus-change string output.

I figured that this might be an ugly interaction between VS and DebugView, so I tried running my program on its own, without the debugger. Again, it would show output in DebugView, but only when switching to DebugView. When I switched focus to Notepad++, SourceTree, and a half dozen other apps, nothing registered in DebugView.

I got a bit suspicious so I fired up process explorer and did a search for my injection dll. Sure enough, only a small selection of processes seem to get the dll. When I build the dll 32-bit, Visual Studio, DebugView, procexp.exe all seem to get the dll, but NOT any of the other running 32-bit processes in my machine. When I build the dll 64-bit, explorer.exe and procexp64.exe get the dll, but not any of the other 64-bit processes on my machine.

Can anyone suggest anything? Any possible explanations? Is it possible to get logging events somewhere, which might explain why my dll goes into one particular process but not another? SetWindowsHookEx reports ERROR_SUCCESS with GetLastError. Where can I look next?

UPDATE:

I've uploaded the visual studio projects that demonstrate this.

https://dl.dropboxusercontent.com/u/7059499/keylog.zip

I use cmake and unfortunately cmake won't put 32-bit and 64-bit targets in the same sln - so the 64-bit .sln is in _build64, and the 32-bit .sln is in _build32. Just to be clear, you don't need cmake to try this out - it's just that I used cmake to generate these project files originally.

Here's my main.cpp

#include <iostream>
#include <iomanip>
#include <sstream>
#include "stdafx.h"
#include "km_inject.h"

using namespace std;

typedef pair<DWORD, string> LastErrorMessage;

LastErrorMessage GetLastErrorMessage()
{
    DWORD code = GetLastError();
    _com_error error(code);
    LPCTSTR errorText = error.ErrorMessage();
    return LastErrorMessage( code, string(errorText) );
}

static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
  switch (message)
  {
  case WM_DESTROY:
      PostQuitMessage(0);
      break;
  default:
      return DefWindowProc(hWnd, message, wParam, lParam);
  }
  return 0;
}


LRESULT __stdcall CALLBACK LowLevelKeyboardProc(
  _In_  int nCode,
  _In_  WPARAM wParam,
  _In_  LPARAM lParam
)
{
    KBDLLHOOKSTRUCT * hookobj = (KBDLLHOOKSTRUCT *)lParam;
    DWORD vkCode = hookobj->vkCode;
    DWORD scanCode = hookobj->scanCode;
    DWORD flags = hookobj->flags;
    DWORD messageTime = hookobj->time;

    UINT vkCodeChar = MapVirtualKey( vkCode, MAPVK_VK_TO_CHAR );

#define BITFIELD(m) string m##_str = (flags & m)? #m : "NOT " #m
    BITFIELD(LLKHF_EXTENDED);
    BITFIELD(LLKHF_INJECTED);
    BITFIELD(LLKHF_ALTDOWN);
    BITFIELD(LLKHF_UP);
#undef BITFIELD

    string windowMessageType;

#define KEYSTRING(m) case m: windowMessageType = #m; break

    switch ( wParam )
    {
        KEYSTRING( WM_KEYDOWN );
        KEYSTRING( WM_KEYUP );
        KEYSTRING( WM_SYSKEYDOWN );
        KEYSTRING( WM_SYSKEYUP );
    default: windowMessageType = "UNKNOWN"; break;
    };
#undef KEYSTRING

    stringstream ss;
    ss << left 
       << setw(10) << messageTime << " "
       << setw(15) << windowMessageType << ": "
       << right
       << "VK=" << setw(3) << vkCode << " (0x" << hex << setw(3) << vkCode << dec << ") " << setw(2) << vkCodeChar << ", " 
       << "SC=" << setw(3) << scanCode << " (0x" << hex << setw(3) << scanCode << dec << "), " 
       << setw(20) << LLKHF_EXTENDED_str << ", " 
       << setw(20) << LLKHF_INJECTED_str << ", " 
       << setw(20) << LLKHF_ALTDOWN_str << ", " 
       << setw(15) << LLKHF_UP_str << endl;
    OutputDebugString( ss.str().c_str() );

    return CallNextHookEx( 0, nCode, wParam, lParam );
}


int WINAPI WinMain(
  __in  HINSTANCE hInstance,
  __in_opt  HINSTANCE hPrevInstance,
  __in_opt  LPSTR lpCmdLine,
  __in  int nCmdShow )
{
    OutputDebugString( "Beginning test...\n" );

    // Set up main event loop for our application.
    WNDCLASS windowClass = {};
    windowClass.lpfnWndProc = WndProc;
    char * windowClassName = "StainedGlassWindow";
    windowClass.lpszClassName = windowClassName;
    windowClass.hbrBackground = (HBRUSH)GetStockObject( WHITE_BRUSH );
    if (!RegisterClass(&windowClass)) 
    {
        LastErrorMessage fullMessage = GetLastErrorMessage();
        stringstream ss;
        ss << "Failed to register window class: " << fullMessage.first << " \"" << fullMessage.second << "\"\n";
        OutputDebugString( ss.str().c_str() );
        return -1;
    }
    HWND mainWindow = CreateWindow(windowClassName, // class
        "keylogger", // title
        WS_OVERLAPPEDWINDOW | WS_VISIBLE , // 'style'
        CW_USEDEFAULT, // x
        CW_USEDEFAULT, // y
        CW_USEDEFAULT, // width
        CW_USEDEFAULT, // height
        NULL, // parent hwnd - can be HWND_MESSAGE
        NULL, // menu - use class menu
        hInstance, // module handle
        NULL); // extra param for WM_CREATE

    if (!mainWindow) 
    {
        LastErrorMessage fullMessage = GetLastErrorMessage();
        stringstream ss;
        ss << "Failed to create main window: " << fullMessage.first << " \"" << fullMessage.second << "\"\n";
        OutputDebugString( ss.str().c_str() );
        return -1;
    }

    // Get the name of the executable
    char injectFileName[ MAX_PATH + 1 ];
    {
        int ret = GetModuleFileName( hInstance, injectFileName, MAX_PATH );
        if ( ret == 0 || ret == MAX_PATH )
        {
            LastErrorMessage fullMessage = GetLastErrorMessage();
            stringstream ss;
            ss << "GetModuleFileName failed: " << fullMessage.first << " \"" << fullMessage.second << "\"\n";
            OutputDebugString( ss.str().c_str() );
            return -1;
        }
        char * sep = strrchr( injectFileName, '\\' );
        if ( sep == NULL )
        {
            stringstream ss;
            ss << "Couldn't find path separator in " << injectFileName << endl;
            OutputDebugString( ss.str().c_str() );
            return -1;
        }
        *sep = 0;
        strcat_s( injectFileName, "\\km_inject.dll" );
    }

    // Get the module handle
    HINSTANCE inject = LoadLibrary( injectFileName );
    if ( NULL == inject )
    {
        LastErrorMessage fullMessage = GetLastErrorMessage();
        stringstream ss;
        ss << "Failed to load injector with LoadLibrary: " << fullMessage.first << " \"" << fullMessage.second << "\"\n";
        OutputDebugString( ss.str().c_str() );
        return -1;
    }

#ifdef _WIN64
    HOOKPROC LowLevelCBTProc = (HOOKPROC)GetProcAddress( inject, "LowLevelCBTProc" );
#else
    HOOKPROC LowLevelCBTProc = (HOOKPROC)GetProcAddress( inject, "_LowLevelCBTProc@12" );
#endif

    if ( !LowLevelCBTProc )
    {
        LastErrorMessage fullMessage = GetLastErrorMessage();
        stringstream ss;
        ss << "Failed to find LowLevelCBTProc function: " << fullMessage.first << " \"" << fullMessage.second << "\"\n";
        OutputDebugString( ss.str().c_str() );
        return -1;
    }

    // Install the keyboard and CBT handlers
    if ( NULL == SetWindowsHookEx( WH_KEYBOARD_LL, LowLevelKeyboardProc, NULL, 0 ) )
    {
        LastErrorMessage fullMessage = GetLastErrorMessage();
        stringstream ss;
        ss << "Failed to set llkey hook: " << fullMessage.first << " \"" << fullMessage.second << "\"\n";
        OutputDebugString( ss.str().c_str() );
        return -1;
    }

    if ( NULL == SetWindowsHookEx( WH_CBT, LowLevelCBTProc, inject, 0 ) )
    {
        LastErrorMessage fullMessage = GetLastErrorMessage();
        stringstream ss;
        ss << "Failed to set cbt hook: " << fullMessage.first << " \"" << fullMessage.second << "\"\n";
        OutputDebugString( ss.str().c_str() );
        return -1;
    }


    BOOL bRet;
    MSG msg;

    while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
    { 
        if (bRet == -1)
        {
            LastErrorMessage fullMessage = GetLastErrorMessage();
            stringstream ss;
            ss << "What on earth happened? errcode=" << fullMessage.first << ", \"" << fullMessage.second << "\"\n";
            OutputDebugString( ss.str().c_str() );
            break;
        }
        else
        {
            TranslateMessage(&msg); 
            DispatchMessage(&msg); 
        }
    } 



    OutputDebugString( "Bye, bye!\n" );

    return 0;
}

This is the dll I created for this, km_inject.cpp/.h

km_inject.h:

#ifndef INCLUDED_keyloggermini_km_inject_h
#define INCLUDED_keyloggermini_km_inject_h

#if defined(__cplusplus__)
extern "C" {
#endif

LRESULT __declspec(dllimport)__stdcall CALLBACK LowLevelCBTProc(
  _In_  int nCode,
  _In_  WPARAM wParam,
  _In_  LPARAM lParam
);

#if defined(__cplusplus__)
};
#endif


#endif

km_inject.cpp:

#include <windows.h>
#include <utility>
#include <string>
#include <sstream>
#include <iostream>
#include <iomanip>
#include <ctime>

using namespace std;

extern "C" LRESULT __declspec(dllexport)__stdcall CALLBACK LowLevelCBTProc(
  _In_  int nCode,
  _In_  WPARAM wParam,
  _In_  LPARAM lParam
)
{
#define HCBTCODE(m) case m: OutputDebugString( #m "\n" ); break;
    switch ( nCode )
    {
        HCBTCODE( HCBT_ACTIVATE );
        HCBTCODE( HCBT_CLICKSKIPPED );
        HCBTCODE( HCBT_CREATEWND );
        HCBTCODE( HCBT_DESTROYWND );
        HCBTCODE( HCBT_KEYSKIPPED );
        HCBTCODE( HCBT_MINMAX );
        HCBTCODE( HCBT_MOVESIZE );
        HCBTCODE( HCBT_QS );
        HCBTCODE( HCBT_SETFOCUS );
        HCBTCODE( HCBT_SYSCOMMAND );
    default:
        OutputDebugString( "HCBT_?\n" );
        break;
    }
    return CallNextHookEx( 0, nCode, wParam, lParam );
}

extern "C" BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        //
        break;

    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;

    case DLL_PROCESS_DETACH:
        //
        break;
    }
    return TRUE;
}
Ted Middleton
  • 6,859
  • 10
  • 51
  • 71
  • 1
    If the DLL itself is generating the output (rather than sending a message to your process) then naturally the output will be written by whichever process has received the event, so the behaviour you describe for `OutputDebugString` is as expected. Also, I believe the way hooks work means that only processes which have actually received the relevant event (i.e., processes which have received focus since the hook was installed) will have loaded the DLL, which may explain what you're seeing in Process Explorer. – Harry Johnston May 07 '14 at 04:40
  • OutputDebugString maps in and writes to the shared debugging page in Windows - ANY process that calls OutputDebugString will show up in DebugView. Injecting a DLL into a process and calling OutputDebugString from it should still show up in DebugView. – Ted Middleton May 07 '14 at 05:17
  • I've switched focus back and forth to a bunch of process windows, and still I don't see debug output in DebugView, and still process explorer says that only a handful of processes have gotten the injection DLL. – Ted Middleton May 07 '14 at 05:17
  • 1
    Just FYI, you can only trust the return value of `GetLastError` if the last function call failed. In this case, that would mean that `SetWindowsHookEx` returned `NULL`. If the function succeeds, and you get a valid handle to a hook procedure, then you can't call `GetLastError`. Anyway, you might want to post the code that you're using to install the hook. – Cody Gray - on strike May 07 '14 at 05:21
  • @TedMiddleton: the documentation isn't very clear, but that obviously isn't what is actually happening, since you're getting output in two separate places. Have you checked that you're running DebugView as administrator? Have you tried getting your hook procedure to do something unmistakable, e.g., exit the application? – Harry Johnston May 07 '14 at 20:38
  • It might also be worth trying directly injecting your DLL instead of registering it as a hook; if OutputDebugString works as you expect in that scenario, you've ruled out a bunch of possible causes, and if it doesn't work, the problem is likely to be easier to diagnose. – Harry Johnston May 07 '14 at 20:41
  • @HarryJohnston: Something is going wrong - just not sure what it is. In the past I've had a lot of trouble using DebugView because of permissions and access. I'm wondering whether that's what's happening here. But still, I can see from process explorer that the DLL just isn't getting injected into very much. – Ted Middleton May 07 '14 at 21:06
  • @HarryJohnston: I'm intrigued, though - is there a systematic way to inject an arbitrary dll into other processes in Windows? – Ted Middleton May 07 '14 at 21:06
  • Using CreateRemoteThread, e.g., [like this](http://stackoverflow.com/questions/14096215/c-createremotethread-dll-injection-windows-7). – Harry Johnston May 07 '14 at 21:15
  • I am not sure your beliefs about OutputDebugString are correct. Have a look at the comments below the documentation here: http://msdn.microsoft.com/en-us/library/windows/desktop/aa363362(v=vs.85).aspx – Ben May 07 '14 at 21:44
  • Which comment in particular? There are a lot of comments from posters struggling with the baroque permissions and vista filters issues, along with vista's awful behavior when debugging services, but I don't see a comment that seems relevant? – Ted Middleton May 07 '14 at 22:14
  • 1
    Just a quick note: I happen to be working on an application that uses both 32-bit and 64-bit Windows hooks as I am writing this, and I had to rip out all calls to OutputDebugString from my hook procs because they were causing my hooks to hang (presumably) and then get uninstalled by Windows (8.1.). I haven't fully researched the exact underlying cause yet (if I am doing something really silly) so this is just a hint. – 500 - Internal Server Error May 07 '14 at 23:11
  • @500-InternalServerError: I think I figured this out, and perhaps your problem, too - see my answer. – Ted Middleton May 07 '14 at 23:22
  • 2
    Just don't. Use SetWinEventHook() instead. – Hans Passant May 07 '14 at 23:31
  • Try using Process Monitor filtered on your inject dll name, to see if there are any related events in the non-working processes. – Ben May 08 '14 at 08:18

1 Answers1

0

I'm pretty sure that I know what's going on here. @500-InternalServerError mentioned that when he has OutputDebugString() in his injected dll, it seems to hang and doesn't get installed. I think this is what's happening to me, too.

OutputDebugString() developed a very mean streak with Vista. In particular, Vista introduced the debug output filter at HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter. I've stumbled into this before but in the context of kernel debugging and this can be responsible for completely silencing your DbgPrint/OutputDebugString/printk output.

As per the instructions here (http://blogs.msdn.com/b/doronh/archive/2006/11/14/where-did-my-debug-output-go-in-vista.aspx) I added a completely permissive DEFAULT filter to my debug output, and then rebooted. Now, when I run my keylogger, every application that starts after my keylogger seems to get the injected dll. It works! DebugView now sees debugging output from pretty much every application that I start after my keylogger.

I'm thinking that as per @500-InternalServerError's experience, perhaps when Windows doesn't see a DEFAULT filter in Debug Print Filter, and this is only my guess, that Windows doesn't make the OutputDebugString symbol available for linking and so injecting the dll would fail (silently?). Applications that already link to OutputDebugString - like DebugView itself, Visual Studio, process explorer, and apparently explorer.exe, would be ok - my injection dll would link properly. Anyway, that's my guess.

Thanks, everyone for the suggestions.

UPDATE:

Ok, I'm not so sure about this anymore. I went back and removed the DEFAULT filter and I can still see my hook dll getting loaded into new processes. So what was going on here? I really don't know. Misusing process explorer? If you don't start process explorer with admin privileges, it won't be able to search all processes for a particular dll. But even then, it shouldn't have had a problem spotting the dozen or standard non-admin processes I started.

I can't reproduce the problem anymore. If anyone is interested, the example code is still available at the link above. Obviously I'm not going to mark this as an answer because the problem just 'went away'. I'll update this if the problem comes back.

Ted Middleton
  • 6,859
  • 10
  • 51
  • 71
  • 1
    Linker symbols cannot be hidden dynamically like you suggest. They either exist or they don't. More likely, `OutputDebugString()` itself, or whatever it communicates with internally, simply does not generate a catchable debug event if the filter is missing, that's all. – Remy Lebeau May 07 '14 at 23:57
  • In principal, you could do this - if dll1 links to dll2, and LoadLibrary can't find dll2 when loading dll1, it could certainly fail. But that isn't the case here - OutputDebugString is in kernel32.dll and the underlying DbgPrint is in ntdll.dll, both of which are already going to be in any windows process. – Ted Middleton May 08 '14 at 01:07
  • This isn't 'static' linking, nor is it an implicit shared linking based on an IMPORTS section, which is what I think you mean. The target processes don't have my hook dll in their IMPORTS table. The Windows user-mode libs will be using LoadLibrary or something like it to pull my hook dll into other processes. – Ted Middleton May 08 '14 at 01:47