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;
}