Process Browser options implemented

This commit is contained in:
marcos.luna
2025-11-04 13:02:01 +01:00
parent 47978f4259
commit 30c2d6390f
11 changed files with 616 additions and 60 deletions
@@ -171,6 +171,10 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) {
_inf("===== PROCESSING WHOAMI_CMD (0x%02X) =====", tarea);
ObtenerUsuarioActual(analizadorTarea);
_inf("===== WHOAMI_CMD COMPLETED =====");
} else if (tarea == KILL_CMD) {
_inf("===== PROCESSING KILL_CMD (0x%02X) =====", tarea);
MatarProceso(analizadorTarea);
_inf("===== KILL_CMD COMPLETED =====");
} else {
_wrn("Tarea desconocida: 0x%02x", tarea);
_wrn("Tarea %u: Comando desconocido, liberando recursos", i+1);
@@ -53,6 +53,7 @@
#define MAKE_TOKEN_CMD 0x34
#define REV2SELF_CMD 0x35
#define WHOAMI_CMD 0x36
#define KILL_CMD 0x37
/* Extension marker to carry SOCKS array in binary messages */
#define SOCKS_BLOCK_MARKER 0xF5
@@ -910,3 +910,148 @@ VOID ObtenerUsuarioActual(PAnalizador argumentos) {
liberarPaquete(respuesta);
}
/**
* @brief Kill a process by PID
* Terminates a process using TerminateProcess API
*/
VOID MatarProceso(PAnalizador argumentos) {
// For input_type=string commands, format is: [UUID:length_prefixed][nbArg:4][param1:length_prefixed]...
SIZE_T tamanoUuid = 36;
PCHAR tareaUuid = getString(argumentos, &tamanoUuid);
UINT32 nbArg = getInt32(argumentos);
if (nbArg < 1) {
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuestaError, tareaUuid, FALSE);
PPaquete salidaError = nuevoPaquete(0, FALSE);
PackageAddFormatPrintf(salidaError, FALSE, "[kill] Error: Se requiere PID\n");
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
mandarPaquete(respuestaError);
liberarPaquete(salidaError);
liberarPaquete(respuestaError);
return;
}
// PID comes as a string parameter (input_type=string means all params are strings)
SIZE_T pid_len = 0;
PCHAR pid_str = getString(argumentos, &pid_len);
// Convert PID string to integer
UINT32 pid = 0;
if (pid_str && pid_len > 0) {
// Null-terminate the PID string for atoi
char pid_null_term[16] = {0};
SIZE_T copy_len = pid_len < sizeof(pid_null_term) - 1 ? pid_len : sizeof(pid_null_term) - 1;
memcpy(pid_null_term, pid_str, copy_len);
pid_null_term[copy_len] = '\0';
pid = (UINT32)atoi(pid_null_term);
}
if (pid == 0) {
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuestaError, tareaUuid, FALSE);
PPaquete salidaError = nuevoPaquete(0, FALSE);
PackageAddFormatPrintf(salidaError, FALSE, "[kill] Error: PID inválido o es 0\n");
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
mandarPaquete(respuestaError);
liberarPaquete(salidaError);
liberarPaquete(respuestaError);
return;
}
// Don't allow killing critical system processes (PIDs 0, 4, 8)
if (pid <= 4 || pid == 8) {
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuestaError, tareaUuid, FALSE);
PPaquete salidaError = nuevoPaquete(0, FALSE);
PackageAddFormatPrintf(salidaError, FALSE, "[kill] Error: No se puede matar procesos críticos del sistema (PID: %lu)\n", pid);
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
mandarPaquete(respuestaError);
liberarPaquete(salidaError);
liberarPaquete(respuestaError);
return;
}
// First, verify process exists using PROCESS_QUERY_INFORMATION
_inf("[kill] Intentando matar proceso PID %lu", pid);
HANDLE hCheck = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);
if (!hCheck) {
DWORD checkError = GetLastError();
_err("[kill] Proceso PID %lu no existe (verificación inicial falló, error: %lu)", pid, checkError);
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuestaError, tareaUuid, FALSE);
PPaquete salidaError = nuevoPaquete(0, FALSE);
PackageAddFormatPrintf(salidaError, FALSE, "[kill] Error: El proceso PID %lu no existe o ya terminó (error: %lu)\n", pid, checkError);
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
mandarPaquete(respuestaError);
liberarPaquete(salidaError);
liberarPaquete(respuestaError);
return;
}
_inf("[kill] Proceso PID %lu existe, verificando acceso de terminación...", pid);
CloseHandle(hCheck);
// Open process with PROCESS_TERMINATE access
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (!hProcess) {
DWORD error = GetLastError();
_err("[kill] OpenProcess(PROCESS_TERMINATE) falló para PID %lu, error: %lu", pid, error);
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuestaError, tareaUuid, FALSE);
PPaquete salidaError = nuevoPaquete(0, FALSE);
// Provide more specific error messages
if (error == ERROR_INVALID_PARAMETER || error == 87) {
PackageAddFormatPrintf(salidaError, FALSE, "[kill] Error: El proceso PID %lu no existe o ya terminó (error: %lu)\n", pid, error);
} else if (error == ERROR_ACCESS_DENIED || error == 5) {
PackageAddFormatPrintf(salidaError, FALSE, "[kill] Error: Acceso denegado al proceso PID %lu. Se requieren privilegios de administrador (error: %lu)\n", pid, error);
} else {
PackageAddFormatPrintf(salidaError, FALSE, "[kill] Error: No se pudo abrir el proceso PID %lu, error: %lu\n", pid, error);
}
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
mandarPaquete(respuestaError);
liberarPaquete(salidaError);
liberarPaquete(respuestaError);
return;
}
_inf("[kill] Proceso PID %lu abierto con éxito, terminando...", pid);
// Terminate the process
BOOL success = TerminateProcess(hProcess, 1); // Exit code 1
DWORD terminateError = GetLastError();
CloseHandle(hProcess);
if (!success) {
_err("[kill] TerminateProcess falló para PID %lu, error: %lu", pid, terminateError);
} else {
_inf("[kill] TerminateProcess ejecutado exitosamente para PID %lu", pid);
}
PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuesta, tareaUuid, FALSE);
PPaquete salida = nuevoPaquete(0, FALSE);
if (success) {
PackageAddFormatPrintf(salida, FALSE, "[kill] Proceso PID %lu terminado exitosamente\n", pid);
} else {
DWORD error = GetLastError();
PackageAddFormatPrintf(salida, FALSE, "[kill] Error al terminar proceso PID %lu, error: %lu\n", pid, error);
}
// Add output first (parser expects UUID -> output -> artifacts)
addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE);
// Add artifact for process termination AFTER output
if (success) {
char artifact_msg[256] = {0};
snprintf(artifact_msg, sizeof(artifact_msg), "Process Termination: PID %lu", pid);
addArtifact(respuesta, "Process Termination", artifact_msg);
}
mandarPaquete(respuesta);
liberarPaquete(salida);
liberarPaquete(respuesta);
}
@@ -11,6 +11,7 @@
#define STEAL_TOKEN_CMD 0x33
#define MAKE_TOKEN_CMD 0x34
#define REV2SELF_CMD 0x35
#define KILL_CMD 0x37
// Forward declarations
VOID ListarTokens(PAnalizador argumentos);
@@ -18,6 +19,7 @@ VOID RobarToken(PAnalizador argumentos);
VOID CrearToken(PAnalizador argumentos);
VOID RevertirToken(PAnalizador argumentos);
VOID ObtenerUsuarioActual(PAnalizador argumentos);
VOID MatarProceso(PAnalizador argumentos);
#endif
@@ -0,0 +1,115 @@
from mythic_container.MythicCommandBase import *
from mythic_container.MythicRPC import *
class KillArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(
name="pid",
type=ParameterType.Number,
description="Process ID to kill",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=1
)]
),
# Process Browser parameters (optional, passed automatically by Mythic)
CommandParameter(
name="host",
type=ParameterType.String,
description="Host name (from Process Browser)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=2
)]
),
CommandParameter(
name="process_id",
type=ParameterType.Number,
description="Process ID (from Process Browser, takes precedence over pid)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=3
)]
),
CommandParameter(
name="architecture",
type=ParameterType.String,
description="Process architecture (from Process Browser)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=4
)]
),
]
async def parse_arguments(self):
if len(self.command_line) == 0:
# Check if process_id is provided (from Process Browser)
if not self.get_arg("process_id") and not self.get_arg("pid"):
raise ValueError("Must supply a PID")
else:
try:
self.add_arg("pid", int(self.command_line))
except ValueError:
raise ValueError("PID must be a number")
async def parse_dictionary(self, dictionary_arguments):
self.load_args_from_dictionary(dictionary_arguments)
# If process_id is provided (from Process Browser), use it instead of pid
if self.get_arg("process_id") and not self.get_arg("pid"):
self.add_arg("pid", self.get_arg("process_id"))
class KillCommand(CommandBase):
cmd = "kill"
needs_admin = False
help_cmd = "kill <pid>"
description = "Terminate a process by PID"
version = 1
author = "@KaseyaOFSTeam"
attackmapping = ["T1489"]
supported_ui_features = ["process_browser:kill"]
argument_class = KillArguments
async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
response = PTTaskCreateTaskingMessageResponse(
TaskID=taskData.Task.ID,
Success=True,
)
pid = taskData.args.get_arg("pid")
if pid:
response.DisplayParams = f"Killing process PID: {pid}"
else:
response.DisplayParams = "Killing process (no PID specified)"
return response
async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
# Check if kill was successful by checking user_output
if response:
user_output = response.get("user_output", "")
# Check if output indicates success (doesn't contain error indicators)
if user_output and not any(err in user_output.lower() for err in ["error", "no se pudo", "falló", "failed"]):
# Get the PID that was killed
pid = task.args.get_arg("pid") or task.args.get_arg("process_id")
if pid:
# Get hostname
host = task.Callback.Host if task.Callback else ""
# Add removed_processes array to response (similar to removed_files for file browser)
# This tells Mythic Process Browser to remove this process from the cache
resp.ProcessResponse = response if isinstance(response, dict) else {}
if "removed_processes" not in resp.ProcessResponse:
resp.ProcessResponse["removed_processes"] = []
resp.ProcessResponse["removed_processes"].append({
"host": host,
"process_id": int(pid)
})
logging.info(f"[PROCESS_BROWSER] Marked process PID {pid} as removed in Process Browser")
return resp
@@ -5,7 +5,36 @@ from mythic_container.MythicRPC import *
class ListTokensArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = []
self.args = [
# Process Browser parameters (optional, passed automatically by Mythic)
CommandParameter(
name="host",
type=ParameterType.String,
description="Host name (from Process Browser)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=1
)]
),
CommandParameter(
name="process_id",
type=ParameterType.Number,
description="Process ID to list tokens from (from Process Browser)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=2
)]
),
CommandParameter(
name="architecture",
type=ParameterType.String,
description="Process architecture (from Process Browser)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=3
)]
),
]
async def parse_arguments(self):
pass
@@ -21,6 +50,7 @@ class ListTokensCommand(CommandBase):
description = "List all available tokens from running processes"
version = 1
author = "@KaseyaOFSTeam"
supported_ui_features = ["process_browser:list_tokens"]
argument_class = ListTokensArguments
async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
@@ -1,50 +1,62 @@
from mythic_container.MythicCommandBase import *
import json
class PsArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = []
async def parse_arguments(self):
if len(self.command_line) > 0:
raise Exception("ps no debe lanzarse con parametros")
class PsCommand(CommandBase):
cmd = "ps"
needs_admin = False
help_cmd = "ps"
description = "Lista los procesos del host."
version = 1
supported_ui_features = ["process_browser:list"]
author = "Kaseya OFSTeam"
argument_class = PsArguments
browser_script = BrowserScript(
script_name="ps", author="Kaseya OFSTeam", for_new_ui=True
)
attributes = CommandAttributes(
builtin=False,
supported_os=[ SupportedOS.Windows ],
suggested_command=True
)
attackmapping = []
async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
response = PTTaskCreateTaskingMessageResponse(
TaskID=taskData.Task.ID,
Success=True,
)
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
from mythic_container.MythicCommandBase import *
import json
import logging
class PsArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = []
async def parse_arguments(self):
if len(self.command_line) > 0:
raise Exception("ps no debe lanzarse con parametros")
class PsCommand(CommandBase):
cmd = "ps"
needs_admin = False
help_cmd = "ps"
description = "Lista los procesos del host."
version = 1
supported_ui_features = ["process_browser:list"]
author = "Kaseya OFSTeam"
argument_class = PsArguments
browser_script = BrowserScript(
script_name="ps", author="Kaseya OFSTeam", for_new_ui=True
)
attributes = CommandAttributes(
builtin=False,
supported_os=[ SupportedOS.Windows ],
suggested_command=True
)
attackmapping = []
async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
response = PTTaskCreateTaskingMessageResponse(
TaskID=taskData.Task.ID,
Success=True,
)
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.
We also ensure update_deleted=True is set for all processes to clear the cache.
"""
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
# Merge with existing response if present
if response and isinstance(response, dict):
resp.ProcessResponse = response
# Ensure processes have update_deleted=True to clear cache
if "processes" in resp.ProcessResponse and isinstance(resp.ProcessResponse["processes"], list):
for process in resp.ProcessResponse["processes"]:
if isinstance(process, dict):
process["update_deleted"] = True
logging.info(f"[PROCESS_BROWSER] Set update_deleted=True for {len(resp.ProcessResponse['processes'])} processes in process_response")
return resp
@@ -11,22 +11,56 @@ class StealTokenArguments(TaskArguments):
type=ParameterType.Number,
description="Process ID to steal token from",
parameter_group_info=[ParameterGroupInfo(
required=True,
required=False,
ui_position=1
)]
),
# Process Browser parameters (optional, passed automatically by Mythic)
CommandParameter(
name="host",
type=ParameterType.String,
description="Host name (from Process Browser)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=2
)]
),
CommandParameter(
name="process_id",
type=ParameterType.Number,
description="Process ID (from Process Browser, takes precedence over pid)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=3
)]
),
CommandParameter(
name="architecture",
type=ParameterType.String,
description="Process architecture (from Process Browser)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=4
)]
),
]
async def parse_arguments(self):
if len(self.command_line) == 0:
raise ValueError("Must supply a PID")
try:
self.add_arg("pid", int(self.command_line))
except ValueError:
raise ValueError("PID must be a number")
# Check if process_id is provided (from Process Browser)
if not self.get_arg("process_id") and not self.get_arg("pid"):
raise ValueError("Must supply a PID")
else:
try:
self.add_arg("pid", int(self.command_line))
except ValueError:
raise ValueError("PID must be a number")
async def parse_dictionary(self, dictionary_arguments):
self.load_args_from_dictionary(dictionary_arguments)
# If process_id is provided (from Process Browser), use it instead of pid
if self.get_arg("process_id") and not self.get_arg("pid"):
self.add_arg("pid", self.get_arg("process_id"))
class StealTokenCommand(CommandBase):
@@ -37,6 +71,7 @@ class StealTokenCommand(CommandBase):
version = 1
author = "@KaseyaOFSTeam"
attackmapping = ["T1134.001"]
supported_ui_features = ["process_browser:steal_token"]
argument_class = StealTokenArguments
async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
@@ -27,6 +27,7 @@ commands = {
"make_token": {"hex_code": 0x34, "input_type": "string"},
"rev2self": {"hex_code": 0x35, "input_type": None},
"whoami": {"hex_code": 0x36, "input_type": None},
"kill": {"hex_code": 0x37, "input_type": "string"},
# Added SOCKS control commands
"start_socks": {"hex_code": 0x60, "input_type": "int"},
"stop_socks": {"hex_code": 0x61, "input_type": None},
@@ -1135,8 +1135,13 @@ def postResponse(data):
# Si detectamos una lista de procesos, también incluir el formato para Process Browser
if is_process_list and len(processes) > 0:
# Mark ALL processes with update_deleted=True to tell Mythic this is the complete list
# According to Mythic docs: "setting this to true tells Mythic to mark any process not returned in this process array as deleted"
# This effectively clears the cache by removing processes that no longer exist
for process in processes:
process["update_deleted"] = True
jsonTask["processes"] = processes
print(f"[PROCESS_BROWSER] Respuesta convertida al formato Process Browser con {len(processes)} procesos")
print(f"[PROCESS_BROWSER] Respuesta convertida al formato Process Browser con {len(processes)} procesos (update_deleted=True en TODOS los procesos para limpiar caché)")
# Detectar y convertir formato File Browser (formato: path\nD/F\tSIZE\tDATE\tNAME)
# El formato del agente es: primera línea es el path, luego líneas con D/F\tSIZE\tMM/DD/YYYY HH:MM:SS\tNAME