Adds migrate command for Linux x64/x86#287
Adds migrate command for Linux x64/x86#287msutovsky-r7 wants to merge 16 commits intorapid7:masterfrom
Conversation
ae0de8e to
e5a87f0
Compare
There was a problem hiding this comment.
Pull request overview
Adds Linux x64/x86 support for the migrate command in mettle by introducing a ptrace-based injector and exposing transport/socket details needed to stage the new session.
Changes:
- Adds new TLV fields and core TLV handlers for
core_migrateandcore_transport_list. - Introduces a new ptrace-based injection implementation (
base_inject.*) and wires it into the build. - Adds APIs to retrieve the underlying socket FD from the active C2 transport.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| mettle/src/tlv_types.h | Adds migrate-related TLVs and adjusts transport-group TLV ID. |
| mettle/src/network_client.h | Exposes network_client accessors for bufferev and socket FD. |
| mettle/src/network_client.c | Implements the new network_client accessor functions. |
| mettle/src/coreapi.c | Adds core_migrate and core_transport_list handlers and registers them. |
| mettle/src/c2_tcp.c | Adds c2_transport_get_socket_fd() implementation (currently TCP-specific). |
| mettle/src/c2.h | Exposes c2_get_next_transport() and c2_transport_get_socket_fd() prototypes. |
| mettle/src/c2.c | Implements c2_get_next_transport() (and introduces an extra c2_get_fd stub). |
| mettle/src/bufferev.h | Adds bufferev_get_socket_fd() declaration. |
| mettle/src/bufferev.c | Implements bufferev_get_socket_fd(). |
| mettle/src/base_inject.h | Introduces injector header/API for migrate support. |
| mettle/src/base_inject.c | Adds ptrace-based injector implementation for Linux migrate. |
| mettle/src/Makefile.am | Adds base_inject.c to libmettle sources. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return 0; | ||
| } | ||
|
|
||
| struct c2_transport* c2_get_next_transport(struct c2_transport *t){ |
There was a problem hiding this comment.
c2_get_next_transport() dereferences t without a NULL check. core_transport_list() will crash if it ever calls this with a NULL transport (e.g., no transports configured or current_transport not set). Either guard against NULL here or ensure callers validate inputs before iterating.
| struct c2_transport* c2_get_next_transport(struct c2_transport *t){ | |
| struct c2_transport* c2_get_next_transport(struct c2_transport *t){ | |
| if (t == NULL) { | |
| return NULL; | |
| } |
| void get_process_writable_sections(int pid, writable_section_ptr process_sections) | ||
| { | ||
|
|
||
| FILE * maps_handler; | ||
| char * line = NULL; | ||
| size_t len = 0; | ||
| int section_count = 0; |
There was a problem hiding this comment.
The type/field naming here is misleading: get_process_writable_sections() / writable_section_t imply writable memory, but the implementation filters for 'x' (executable) permissions. This makes the injection logic harder to reason about and increases the chance of future bugs. Rename the function/struct/fields to reflect what they actually hold (e.g., executable_sections) or update the filter if the intent really is writable mappings.
| maps_handler = fopen(maps_file_path, "r"); | ||
|
|
||
| char * permissions; | ||
| long start_address; | ||
| long end_address; | ||
|
|
||
| process_sections->pid = pid; | ||
|
|
||
| while(getline(&line,&len, maps_handler) != -1) | ||
| { |
There was a problem hiding this comment.
maps_handler is not checked for NULL before getline() is called. If fopen() fails (permissions, missing /proc, etc.), this will dereference a NULL FILE* and crash. Handle fopen failure and return an error to the caller.
| mem_handler = fopen(mem_file_path, "r"); | ||
|
|
||
| for(int i = 0; i < 255; i++) | ||
| { | ||
| long section_start = process_sections->sections[i].start_address; | ||
| long section_end = process_sections->sections[i].end_address; | ||
|
|
||
| fseek(mem_handler, section_start, SEEK_SET); | ||
|
|
||
| long section_size = section_end - section_start; | ||
|
|
||
| mem_data = malloc(sizeof(char)*(int)(section_end-section_start)); | ||
|
|
||
| fread(mem_data,sizeof(char), (int)(section_end-section_start), mem_handler); | ||
|
|
There was a problem hiding this comment.
mem_handler and mem_data are not validated here. If fopen(/proc//mem) or malloc fails, fseek/fread will operate on NULL and crash. Also, allocating (section_end - section_start) bytes can be extremely large and cause OOM; consider scanning in smaller chunks and bounding the maximum allocation.
| #include <unistd.h> | ||
| #include <stdio.h> | ||
| #include <stdlib.h> | ||
| #include <string.h> | ||
| #include <fcntl.h> | ||
| #include <stddef.h> | ||
| #include <dlfcn.h> | ||
| #include <sigar.h> | ||
| #include <sigar_private.h> | ||
| #include <sigar_util.h> | ||
|
|
||
| //temporarly using PTRACE | ||
| #include <sys/wait.h> | ||
| #include <sys/ptrace.h> | ||
|
|
There was a problem hiding this comment.
base_inject.h is missing an include guard, unlike other headers in this repo. Without a guard it can be included multiple times and cause redefinition errors (e.g., struct user_regs_struct). Add a conventional #ifndef/#define/#endif guard for this header.
mettle/src/coreapi.c
Outdated
| struct tlv_packet *packet_group = tlv_packet_new(TLV_TYPE_TRANS_GROUP, 0); | ||
| packet_group = tlv_packet_add_str(packet_group, TLV_TYPE_TRANS_URL, c2_transport_uri(current_transport)); | ||
| if (packet_group) { | ||
| printf("Adding transport '%s' to transport list response\n", c2_transport_uri(current_transport)); |
There was a problem hiding this comment.
This uses printf() for runtime logging inside the transport list handler. The rest of the codebase uses the log_() helpers; printf can pollute stdout/stderr and may break consumers that treat stdout as protocol data. Switch this to an appropriate log_ level (or remove it) and avoid logging transport URIs unless needed for debugging.
| printf("Adding transport '%s' to transport list response\n", c2_transport_uri(current_transport)); | |
| log_debug("Adding transport to transport list response"); |
| struct tcp_ctx *ctx = (struct tcp_ctx*)(c2_transport_get_ctx(c2->curr_transport)); | ||
| return 1; |
There was a problem hiding this comment.
c2_get_fd() references struct tcp_ctx, which is only defined in c2_tcp.c and not visible here, so this will not compile. The function also returns a constant and appears unused. Remove this function or rework it so it does not depend on transport-private types (e.g., use a public API like c2_transport_get_socket_fd).
| struct tcp_ctx *ctx = (struct tcp_ctx*)(c2_transport_get_ctx(c2->curr_transport)); | |
| return 1; | |
| if (c2 == NULL || c2->curr_transport == NULL) { | |
| return -1; | |
| } | |
| return c2_transport_get_socket_fd(c2->curr_transport); |
| codecave_address = find_codecave(pid, sizeof(DO_SYSCALL), process_sections); | ||
|
|
||
| if(codecave_address == 0) | ||
| return 0; | ||
|
|
||
| if(remote_write(pid, codecave_address, DO_SYSCALL, sizeof(DO_SYSCALL)) == 0) |
There was a problem hiding this comment.
sizeof(DO_SYSCALL) includes the trailing NUL in the string literal, so find_codecave()/remote_write() will search/write 4 bytes instead of the intended 3 instruction bytes. Use the actual instruction length (e.g., sizeof(DO_SYSCALL) - 1) or define DO_SYSCALL as a byte array with an explicit length to avoid writing an unintended 0x00 into the target.
| codecave_address = find_codecave(pid, sizeof(DO_SYSCALL), process_sections); | |
| if(codecave_address == 0) | |
| return 0; | |
| if(remote_write(pid, codecave_address, DO_SYSCALL, sizeof(DO_SYSCALL)) == 0) | |
| codecave_address = find_codecave(pid, sizeof(DO_SYSCALL) - 1, process_sections); | |
| if(codecave_address == 0) | |
| return 0; | |
| if(remote_write(pid, codecave_address, DO_SYSCALL, sizeof(DO_SYSCALL) - 1) == 0) |
| struct c2_transport * transport = c2_get_current_transport(c2); | ||
| int fd = c2_transport_get_socket_fd(transport); | ||
|
|
||
| if(migrate(pid, migrate_stub, stub_length, payload, payload_length, uuid, fd)) |
There was a problem hiding this comment.
c2_transport_get_socket_fd() is assumed to work for the current transport, but mettle supports non-TCP transports (e.g. http/https). The current implementation of c2_transport_get_socket_fd() casts the transport ctx to tcp_ctx, which will be invalid for HTTP and can crash or leak memory. Either (a) move this API behind transport callbacks so each transport type can implement it safely, or (b) explicitly restrict migrate to fd/tcp transports and fail cleanly for others; also handle fd == -1 before calling migrate().
| if(migrate(pid, migrate_stub, stub_length, payload, payload_length, uuid, fd)) | |
| /* Only attempt migration when we have a valid transport file descriptor. */ | |
| if (fd >= 0 && migrate(pid, migrate_stub, stub_length, payload, payload_length, uuid, fd)) |
mettle/src/c2_tcp.c
Outdated
| if (t == NULL) { | ||
| return -1; | ||
| } | ||
| struct tcp_ctx *ctx = (struct tcp_ctx *)c2_transport_get_ctx(t); | ||
| if (ctx == NULL || ctx->nc == NULL) { | ||
| return -1; | ||
| } | ||
| return network_client_get_socket_fd(ctx->nc); |
There was a problem hiding this comment.
c2_transport_get_socket_fd() is defined in the TCP transport and unconditionally treats the opaque transport ctx as tcp_ctx. If the current transport is http/https (also supported in this repo), this will interpret a http_ctx as tcp_ctx and can crash. This API should be implemented in c2.c where the real transport type is known, or exposed via a new callback in c2_transport_cbs so each transport can safely return a socket fd (or -1).
| if (t == NULL) { | |
| return -1; | |
| } | |
| struct tcp_ctx *ctx = (struct tcp_ctx *)c2_transport_get_ctx(t); | |
| if (ctx == NULL || ctx->nc == NULL) { | |
| return -1; | |
| } | |
| return network_client_get_socket_fd(ctx->nc); | |
| /* This transport-specific implementation cannot safely interpret the | |
| * opaque c2_transport context for all possible transport types. | |
| * To avoid undefined behavior when used with non-TCP transports, | |
| * we conservatively report that no socket file descriptor is available. | |
| */ | |
| (void)t; | |
| return -1; |
mettle/src/coreapi.c
Outdated
| if (!payload || payload_length == 0) | ||
| return tlv_packet_response_result(ctx, TLV_RESULT_FAILURE); | ||
|
|
||
| //char *uuid = tlv_packet_get_raw(ctx->req, TLV_TYPE_UUID, &uuid_length); |
There was a problem hiding this comment.
| //char *uuid = tlv_packet_get_raw(ctx->req, TLV_TYPE_UUID, &uuid_length); |
| #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_TRANS_GROUP (TLV_META_TYPE_GROUP | 442) |
There was a problem hiding this comment.
The definition in Framework:
./rex/post/meterpreter/packet.rb:106:TLV_TYPE_TRANS_GROUP = TLV_META_TYPE_GROUP | 442
It was failing due to unknown TLV type IIRC.
| #if defined(__x86_64__) | ||
| regs.rip = stub_address; | ||
| regs.rsp = saved_regs.rsp+0x100; | ||
| regs.rbp = saved_regs.rbp; | ||
| regs.rax = payload_address; | ||
| regs.rbx = getpid(); | ||
| regs.rcx = fd; | ||
| #elif defined(__i386__) | ||
| regs.eip = stub_address; | ||
| regs.esp = saved_regs.esp + 0x100; | ||
| regs.ebp = saved_regs.ebp; | ||
| regs.eax = payload_address; // payload mmap base | ||
| regs.ebx = getpid(); // current pid (for pidfd_open in child) | ||
| regs.ecx = fd; // socket fd (for pidfd_getfd in child) | ||
| #endif |
There was a problem hiding this comment.
This would be a nice HIJACK_REGISTERS macro
This adds support for
migratecommand on Mettle side for Linux x64/x86. This PR only adds support for Mettle to inject migrate stub and execute payload, session handling needs to be taken care in Framework. Currently, themigrateneeds privilege to useptracesyscall.