Process Explorer fixed
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user