Introducción
Esta es la primera parte de un serial de dos partes sobre rootkits para Linux. En esta primera entrega, nos centramos en la teoría detrás de cómo funcionan los rootkits: su taxonomía, evolución y las técnicas de enganche que emplean para subvertir el núcleo. En la segunda parte, pasamos al lado defensivo y nos adentramos en la ingeniería de detección, abordando enfoques prácticos para identificar y responder a estas amenazas en entornos de producción.
¿Qué son los rootkits?
Los rootkits son malware sigiloso diseñados para ocultar actividades maliciosas, como archivos, procesos, conexiones de red, módulos del núcleo o cuentas. Sus principales propósitos son la persistencia y la evasión, permitiendo a los atacantes mantener acceso a largo plazo a objetivos de alto valor como servidores, infraestructuras y sistemas empresariales. A diferencia de otras formas de malware, los rootkits se centran en permanecer indetectados en lugar de perseguir objetivos de inmediato.
¿Cómo funcionan los rootkits?
Los rootkits manipulan el sistema operativo para alterar cómo presenta la información a los usuarios y a las herramientas de seguridad. Operan en espacio de usuario o dentro del núcleo. Los rootkits en espacio de usuario modifican procesos a nivel de usuario empleando técnicas como LD_PRELOAD o secuestro de bibliotecas. Los rootkits en espacio kernel se ejecutan con los mayores privilegios, modificando estructuras del núcleo, interceptando llamadas a sistemas o cargando módulos maliciosos. Esta integración profunda les otorga poderosas capacidades de evasión, pero aumenta el riesgo operativo.
¿Por qué son difíciles detectar rootkits?
Los rootkits en espacio kernel pueden manipular funciones centrales del sistema operativo, subvirtiendo herramientas de seguridad y ocultando artefactos de la visibilidad en el territorio del usuario. A menudo dejan rastros mínimos de su presencia en el sistema, evitando indicadores evidentes como nuevos procesos o archivos, lo que dificulta la detección tradicional. Identificar rootkits a menudo requiere análisis forenses de memoria, comprobaciones de integridad del núcleo o telemetría por debajo del nivel del sistema operativo.
Por qué los rootkits son un arma de doble filo para los atacantes
Aunque los rootkits ofrecen sigilo y control, conllevan riesgos operativos. Los rootkits del kernel deben adaptar con precisión a las versiones y entornos del kernel. Errores, como un mal manejo de la memoria o el enganche incorrecto de las llamadas de sistema, pueden causar fallos del sistema (kernel panics), exponiendo inmediatamente al atacante. Como mínimo, estos fallos atraen una atención no deseada sobre el sistema, un escenario que el atacante intenta evitar activamente para mantener su posición.
Las actualizaciones del kernel también presentan desafíos: cambios en APIs, estructuras de memoria o llamadas a sistemas pueden romper la funcionalidad del rootkit, haciendo vulnerable la persistencia. La detección de módulos o ganchos sospechosos suele desencadenar una investigación forense profunda, ya que los rootkits indican fuertemente ataques dirigidos y de alta habilidad. Para los atacantes, los rootkits son herramientas de alto riesgo y alta recompensa; Para los defensores, esta fragilidad ofrece oportunidades de detección mediante monitorización a bajo nivel.
Windows vs Linux Rootkits
El ecosistema de rootkit de Windows
Windows es el foco principal para el desarrollo de rootkits. Los atacantes explotan hooks del kernel, controladores y llamadas de sistema no documentadas para ocultar malware, robar credenciales y persistir. Una comunidad de investigación madura y un uso generalizado en entornos empresariales impulsan la innovación continua, incluyendo técnicas como DKOM, bypasses PatchGuard y bootkits.
Las estables herramientas de seguridad y los esfuerzos de endurecimiento de Microsoft empujan a los atacantes hacia métodos cada vez más sofisticados. Windows sigue siendo atractivo debido a su dominio en los endpoints empresariales y en dispositivos de consumo.
El ecosistema de rootkit de Linux
Históricamente, los rootkits de Linux recibieron menos atención. La fragmentación entre distribuciones y versiones del núcleo complica la detección y el desarrollo. Aunque existe investigación académica, muchas herramientas están desactualizadas y los entornos Linux de producción a menudo carecen de monitorización especializada.
Sin embargo, el papel de Linux en la nube, los contenedores, el IoT y la computación de alto rendimiento lo convirtió en un objetivo en crecimiento. Se observaron rootkits reales de Linux en ataques contra proveedores de nube, telecomunicaciones y gobiernos. Los principales desafíos para los atacantes incluyen:
- Los núcleos diversos dificultan la compatibilidad entre distribuciones.
- Los largos tiempos de actividad prolongan las discrepancias del kernel.
- Características de seguridad como SELinux, AppArmor y la firma de módulos aumentan la dificultad.
Las amenazas únicas de Linux incluyen:
- Contenedores y Kubernetes: nuevos vectores de persistencia mediante escape de contenedores.
- Dispositivos IoT: núcleos obsoletos con monitorización mínima.
- Servidores de producción: sistemas headless que carecen de interacción del usuario, lo que reduce la visibilidad.
Con Linux dominando la infraestructura moderna, los rootkits representan una amenaza poco monitorizada pero en aumento. Mejorar la detección, las herramientas y la investigación de técnicas específicas de Linux es cada vez más urgente.
Evolución de los modelos de implementación de rootkits de Linux
En las dos últimas décadas, los rootkits de Linux evolucionaron desde técnicas básicas de usuario hasta implantes avanzados residentes en el kernel, aprovechando interfaces modernas como eBPF y io_uring. Cada etapa de esta evolución refleja tanto la innovación del atacante como la respuesta de los defensores, impulsando los diseños de rootkit hacia una mayor sigilosidad, flexibilidad y resiliencia.
Esta sección describe esa evolución, incluyendo características clave, contexto histórico y ejemplos reales.
Principios de los 2000: Rootkits de Userland de Objetos Compartidos (SO)
Los primeros rootkits de Linux funcionaban completamente en espacio de usuario sin necesidad de modificación del núcleo, confiando en técnicas como LD_PRELOAD o la manipulación de perfiles de shell para inyectar objetos compartidos maliciosos en binarios legítimos. Interceptando funciones estándar libc como opendir, readdiry fopen, estos rootkits podían manipular la salida de herramientas de diagnóstico como ps, lsy netstat. Aunque este enfoque facilitaba su despliegue, su dependencia de los ganchos de usuario hacía que tuvieran limitaciones en sigilo y alcance en comparación con implantes a nivel de kernel; se interrumpían fácilmente con simples reinicios o reinicios de configuración. Ejemplos destacados incluyen el rootkit Jynx (2009), que conectaba funciones libc para ocultar archivos y conexiones, y Azazel (2013), que combinaba la inyección de objetos compartidos con funciones opcionales de modo kernel. Las técnicas fundamentales para este abuso dinámico de los enlaces fueron detalladas de forma famosa en Phrack Magazine #61 en 2003.
Mediados de los 2000-2010: Rootkits de Módulos de Kernel Cargables (LKM)
A medida que los defensores se volvieron expertos en detectar manipulaciones de usuarios, los atacantes migraron al espacio del núcleo mediante Módulos de Núcleo Cargables (LKMs). Aunque los LKM son extensiones legítimas, los actores maliciosos los emplean para operar con todos los privilegios, enganchando el sys_call_table, manipulando ftraceo alterando listas internas enlazadas para ocultar procesos, archivos, sockets e incluso el rootkit en sí. Aunque los LKM ofrecen un control profundo y poderosos capacidades de ocultamiento, enfrentan un escrutinio significativo en entornos reforzados. Son detectables mediante estados de kernel contaminados, listados en /proc/moduleso escáneres LKM especializados, y cada vez se ven más obstaculizados por defensas modernas como Secure Boot, la firma de módulos y los Módulos de Seguridad de Linux (LSMs). Ejemplos tradicionales de esta época incluyen Adore-ng (2004+), un LKM con enganche de llamada de sistema capaz de ocultar; Diamorphine (2016), una prostituta popular que sigue funcionando en muchas distribuciones; y Reptile (2020), una variante moderna con capacidades de backdoor.
Finales de los años 2010: rootkits basados en eBPF
Para evadir la creciente detección de amenazas basadas en LKM, los atacantes comenzaron a abusar de eBPF, un subsistema originalmente diseñado para el filtrado seguro de paquetes y el rastreo de núcleos. Desde Linux 4.8+, eBPF evolucionó hasta convertir en una máquina virtual programable dentro del núcleo capaz de anexar código a ganchos de llamada sys, kprobes, tracepoints o eventos del Módulo de Seguridad de Linux. Estos implantes funcionan en espacio kernel pero evitan la carga tradicional de módulos, lo que les permite saltar escáneres LKM estándar como rkhunter y chkrootkit, así como las restricciones de arranque seguro. Como no aparecen en /proc/modules y son esencialmente invisibles para los mecanismos típicos de auditoría de módulos, requieren CAP_BPF o CAP_SYS_ADMIN (o raro acceso BPF no privilegiado) para desplegar. Esta era está definida por herramientas como Triple Cross (2022), una prueba de concepto que inyecta programas eBPF para enganchar llamadas de sistema como execve, y Boopkit (2022), que implementa un canal encubierto C2 completamente vía eBPF, junto con numerosas presentaciones de Defcon que exploran el tema.
Años 2025 y más allá: rootkits basados en io_uring (emergentes)
La evolución más reciente aprovecha io_uring, una interfaz de E/S asíncrona de alto rendimiento introducida en Linux 5.1 (2019) que permite a los procesos agrupar operaciones del sistema mediante anillos de memoria compartida. Aunque está diseñado para reducir la sobrecarga de las llamadas de sistema y mejorar el rendimiento, los miembros del red team demostraron que io_uring pueden abusar para crear agentes de usuario sigilosos o rootkits de contexto de kernel que evaden los EDRs basados en syscall. Al usar io_uring_enter para lotear operaciones de archivos, red y procesos, estos rootkits producen muchos menos eventos observables de llamada syscall, frustrando los mecanismos tradicionales de detección y evitando las restricciones impuestas a los LKMs y eBPF. Aunque aún experimental, ejemplos como RingReaper (2025), que emplea io_uring para reemplazar sigilosamente llamadas de sistema comunes como read, write, connecty unlink, y la investigación de ARMO , destacan este vector como un vector muy prometedor para el desarrollo futuro de rootkits, difícil de rastrear sin instrumentación personalizada.
El diseño de rootkit de Linux se adaptó constantemente en respuesta a mejores defensas. A medida que la carga de LKM se vuelve más difícil y la auditoría de llamadas de sistemas se vuelve más avanzada, los atacantes recurrieron a interfaces alternativas como eBPF y io_uring. Con esta evolución, la batalla ya no se trata solo de la detección, sino de comprender los mecanismos que los rootkits emplean para integrar en el núcleo del sistema, empezando por sus estrategias de enganche y su arquitectura interna.
Rootkit Internals and Hooking Techniques
Comprender la arquitectura de los rootkits de Linux es esencial para la detección y la defensa. La mayoría de los rootkits siguen un diseño modular con dos componentes principales:
- Loader: Instala o inyecta el rootkit y puede establecer persistencia. Aunque no es estrictamente necesario, a menudo se observa un componente de cargador separado en cadenas de infección de malware que despliegan rootkits.
- Carga útil: Realiza acciones maliciosas como ocultar archivos, interceptar llamadas de sistema o comunicaciones encubiertas.
Las cargas útiles dependen en gran medida de técnicas de enganche para alterar el flujo de ejecución y lograr el sigilo.
Componente Rootkit Loader
El loader es el componente responsable de transferir el rootkit a la memoria, inicializar su ejecución y, en muchos casos, establecer privilegios de persistencia o escalación. Su función es salvar la brecha entre el acceso inicial (por ejemplo, mediante exploit, phishing o configuración incorrecta) y el despliegue completo del rootkit.
Dependiendo del modelo de rootkit, el cargador puede operar completamente en espacio de usuario, interactuar con el núcleo a través de interfaces estándar del sistema o saltar por completo las protecciones del sistema operativo. En términos generales, los loaders pueden clasificar en tres clases: droppers basados en malware, inicializadores de rootkit userland y loaders personalizados en espacio kernel. Además, los rootkits pueden ser cargados manualmente por un atacante mediante herramientas de espacio de usuario como insmod.
Goteros basados en malware
Los droppers de malware son programas ligeros, a menudo desplegados tras el acceso inicial, cuyo único propósito es descargar o desempaquetar una carga útil de rootkit y ejecutarla. Estos droppers suelen operar en espacio de usuario pero escalan privilegios e interactúan con características a nivel de kernel.
Las técnicas más comunes incluyen:
- Inyección de módulo: Escribir un archivo
.komalicioso en disco e invocarinsmodomodprobepara cargarlo como módulo kernel. - Envoltorio de llamada sys: Usar un envoltorio alrededor de
init_module()ofinit_module()para cargar un LKM directamente a través de llamadas de sistema. - Inyección en memoria: Aprovechar interfaces como
ptraceomemfd_create, evitando a menudo artefactos de disco. - Carga basada en BPF: Uso de utilidades como
bpftool,tco llamadas directasbpf()syscalls para cargar y anexar programas eBPF a puntos de traza del núcleo o ganchos LSM.
Cargadores de Usuarioland
En el caso de rootkits de objetos compartidos, el cargador puede estar limitado a modificar la configuración del usuario o la configuración del entorno:
- Abuso del enlace dinámico: Configurar
LD_PRELOAD=/path/to/rootkit.sopermite que el objeto compartido malicioso sobreescribir las funciones libc cuando se ejecuta el binario destino. - Persistencia mediante modificación de perfil: Insertar configuraciones de precarga en archivos
.bashrc,.profile, o globales como/etc/profilecerciora la ejecución continua entre sesiones.
Aunque estos cargadores son triviales en su implementación, siguen siendo efectivos en entornos débilmente defendidos o como parte de cadenas de infección en varias etapas.
Cargadores de núcleo personalizados
Los rootkits avanzados pueden incluir cargadores de kernel personalizados diseñados para evitar por completo las rutas estándar de carga de módulos. Estos cargadores interactúan directamente con interfaces de kernel de bajo nivel o dispositivos de memoria para escribir el rootkit en memoria, a menudo eludiendo los registros de auditoría del núcleo o la verificación de firmas de módulos.
Por ejemplo, Reptile incluye un binario en espacio de usuario como cargador, lo que le permite cargar el rootkit sin invocar insmod o modprobe; sin embargo, sigue dependiendo de la llamada de init_mod syscall para cargar el módulo en la memoria.
Capacidades adicionales del cargador
El cargador de malware a menudo asume un papel ampliado más allá de la simple inicialización, convertir en un componente multifuncional de la cadena de ataques. Un paso clave para estos cargadores avanzados es el Elevar Privilegios, en el que buscan acceso root antes de cargar la carga útil principal, a menudo explotando vulnerabilidades locales del núcleo, una táctica común ejemplificada por la vulnerabilidad "Dirty Pipe" (CVE-2022-0847). Una vez cerciorados los privilegios, el cargador tiene la tarea de cubrir las vías. Esto implica un proceso de borrar las pruebas de ejecución borrando entradas de archivos críticos como bash_history, logs del kernel, logs de auditoría o la syslogprincipal del sistema. Finalmente, para garantizar la reejecución tras el resetear del sistema, el cargador cerciora la persistencia instalando mecanismos como unidades de systemd , trabajos de cron , reglas de udev o modificaciones en los scripts de inicialización. Estos comportamientos multifuncionales a menudo difuminan la distinción entre un simple "cargador" y malware completo, especialmente en infecciones complejas y de varias etapas.
Componente de carga útil
La carga útil ofrece funcionalidades básicas: sigilo, control y persistencia. Existen varios métodos principales que un atacante podría emplear. Las cargas útiles en espacio de usuario, a menudo denominadas rootkits SO, funcionan secuestrando funciones estándar de la biblioteca C como readdir o fopen a través del enlace dinámico. Esto les permite manipular la salida de herramientas comunes del sistema como ls, netstaty ps. Aunque generalmente son más fáciles de desplegar, su alcance operativo es limitado.
En cambio, las cargas útiles en espacio del núcleo operan con todos los privilegios del sistema. Pueden ocultar archivos y procesos directamente de /proc, manipular la pila de red y modificar las estructuras del núcleo. Un enfoque más moderno implica rootkits basados en eBPF, que aprovechan bytecode en el núcleo adjunto a puntos de traza de llamadas de sistema o ganchos del Linux Security Module (LSM). Estos kits ofrecen sigilo sin necesidad de módulos fuera del árbol, lo que los hace especialmente efectivos en entornos con Arranque Seguro o políticas de firma de módulos. Herramientas como bpftool simplifican su carga, complicando así la detección. Finalmente, las cargas útiles basadas en io_uringexplotan la integración asíncrona de E/S mediante io_uring_enter (disponible en Linux 5.1 y posteriores) para evitar la monitorización tradicional de llamadas de sistema. Esto permite operaciones sigilosas de archivos, red y procesos, minimizando la exposición a la telemetría.
Rootkits de Linux – Técnicas de enganche
Partiendo de esa base esencial, ahora nos centramos en el núcleo de la mayoría de las funcionalidades de rootkit: el engancho. En esencia, el hooking consiste en interceptar y modificar la ejecución de funciones o llamadas al sistema para ocultar actividades maliciosas o inyectar nuevos comportamientos. Al desviar el flujo normal de código, los rootkits pueden ocultar archivos y procesos, filtrar eventos de seguridad o monitorizar el sistema en secreto, a menudo sin dejar pistas evidentes. El enganche puede implementar tanto en el espacio de usuario como en el kernel, y a lo largo de los años, los atacantes idearon numerosas técnicas de engancheo, desde métodos heredados hasta maniobras evasivas modernas. En esta parte, ofreceremos una profunda exploración de las técnicas comunes de hooking empleadas por rootkits de Linux, ilustrando cada método con ejemplos y muestras reales de rootkits (como Reptile, Diamorphine, PUMAKIT y, más recientemente, FlipSwitch) para entender cómo funcionan y cómo la evolución del kernel los desafió.
El concepto de enganchar
A un nivel general, el hooking es la práctica de interceptar una invocación de función o llamada al sistema y redirigirla a código malicioso. De este modo, un rootkit puede modificar los datos o comportamientos devueltos para ocultar su presencia o manipular las operaciones del sistema. Por ejemplo, un rootkit podría enganchar la llamada de sistema que lista archivos en un directorio (getdents), haciendo que se saltara cualquier nombre de archivo que coincida con los propios archivos del rootkit, haciendo así que esos archivos sean "invisibles" para los comandos del usuario como ls.
El enganche no se limita a los componentes internos del núcleo; También puede ocurrir en el espacio de usuario. Los primeros rootkits de Linux funcionaban completamente en userland inyectando objetos compartidos maliciosos en los procesos. Técnicas como el uso de la variable de entorno LD_PRELOAD del enlazador dinámico permiten que un rootkit sobreescribir funciones estándar de la biblioteca C (por ejemplo, getdents, readdiry fopen) en programas de usuario. Esto significa que cuando un usuario ejecuta una herramienta como ps o netstat, el código inyectado del rootkit intercepta llamadas a procesos listados o conexiones de red y filtra las maliciosas. Estos hooks de usuario no requieren privilegios del núcleo y son relativamente sencillos de implementar.
Ejemplos notables incluyen JynxKit (2012) y Azazel (2014), rootkits en modo usuario que conectan decenas de funciones de libc para ocultar procesos, archivos, puertos de red e incluso habilitar puertas traseras. Sin embargo, el hooking en userland tiene limitaciones significativas: es más fácil de detectar y eliminar, y carece del control profundo que tienen los hooks a nivel de kernel. Como resultado, la mayoría de los rootkits modernos y "en la naturaleza" de Linux pasaron a enganchar en espacio kernel, a pesar de la mayor complejidad y riesgo, porque los hooks kernel pueden engañar de forma completa al sistema operativo y a las herramientas de seguridad a bajo nivel.
En el kernel, el hooking suele significar alterar las estructuras de datos o el código del kernel para que cuando el kernel intente ejecutar una operación concreta (por ejemplo, abrir un archivo o hacer una llamada al sistema), invoque el código del rootkit en lugar de (o además de) el código legítimo. A lo largo de los años, los desarrolladores del núcleo de Linux introdujeron protecciones más fuertes para proteger de modificaciones no autorizadas, pero los atacantes respondieron con métodos de enganche cada vez más sofisticados. A continuación, examinaremos las principales técnicas de enganche en el espacio del kernel, comenzando por métodos antiguos (ahora en gran parte obsoletos) y avanzando hacia técnicas modernas que intentan saltar las defensas del kernel contemporáneas. Cada subsección explicará la técnica, mostrará un ejemplo simplificado de código y discutirá su uso en rootkits conocidos y sus limitaciones dadas las salvaguardas actuales de Linux.
Técnicas de enganche en el núcleo
Enganche de la Tabla de Descriptores de Interrupciones (IDT)
Uno de los primeros trucos de hooking del kernel en Linux fue dirigir a la Tabla de Descriptores de Interrupciones (IDT). En Linux x86 de 32 bits, las llamadas al sistema solían invocar mediante una interrupción de software (int 0x80). El IDT es una tabla que asigna números de interrupción a direcciones de manejadores. Al modificar la entrada IDT para 0x80, un rootkit podría secuestrar el punto de entrada de la llamada al sistema antes de que el propio despachador de llamadas al sistema del núcleo tome el control. En otras palabras, cuando cualquier programa activaba una llamada a sistema vía int 0x80, la CPU saltaba primero al gestor personalizado del rootkit, permitiendo que el rootkit filtrara o redirigiera llamadas en el nivel más bajo. A continuación se muestra un ejemplo simplificado de código de enganche IDT (para fines ilustrativos):
// Install the IDT hook
static int install_idt_hook(void) {
// Get pointer to IDT table
idt_table = get_idt_table();
// Save original syscall handler (int 0x80 = entry 128)
original_syscall_entry = idt_table[0x80];
// Calculate original handler address
original_syscall_handler = (void*)(
(original_syscall_entry.offset_high << 16) |
original_syscall_entry.offset_low
);
// Install our hook
idt_table[0x80].offset_low = (unsigned long)custom_int80_handler & 0xFFFF;
idt_table[0x80].offset_high =
((unsigned long)custom_int80_handler >> 16)
& 0xFFFF;
// Keep same selector and attributes as original
// idt_table[0x80].selector and type_attr remain unchanged
printk(KERN_INFO "IDT hook installed at 0x80\n");
return 0;
}
El código anterior establece un nuevo manejador para interrupciones 0x80, redirigiendo el flujo de ejecución al manejador del rootkit antes de que ocurra cualquier manejo de llamadas de sistema. Esto permite que el rootkit intercepte o modifique el comportamiento de las llamadas syscall completamente por debajo del nivel de la tabla de syscall. El hooking IDT se emplea tanto por rootkits educativos como antiguos como SuckIT.
El hooking IDT es ahora mayormente una técnica histórica. Solo funcionaba en sistemas Linux antiguos que usaban el mecanismo int 0x80 (núcleos x86 de 32 bits antes de Linux 2.6). El Linux moderno de 64 bits emplea las instrucciones sysenter/syscall en lugar de la interrupción de software, por lo que la entrada IDT para 0x80 ya no se usa para llamadas al sistema. Además, el hooking IDT es altamente específico de arquitectura (solo x86) y no es efectivo en kernels modernos con x86_64 u otras arquitecturas.
Enganche de la tabla de llamadas syscall
El hooking de la tabla de syscall es una técnica tradicional de rootkit que implica modificar la tabla de despacho de llamadas al sistema del núcleo, conocida como la sys_call_table. Esta tabla es un array de punteros de función donde cada entrada corresponde a un número específico de llamada sys. Al sobreescribir un puntero en esta tabla, un atacante puede redirigir una llamada de sistema legítima, como getdents64, killo read, a un manejador malicioso. A continuación se muestra un ejemplo.
asmlinkage int (*original_getdents64)(
unsigned int,
struct linux_dirent64 __user *,
unsigned int);
asmlinkage int hacked_getdents64(
unsigned int fd,
struct linux_dirent64 __user *dirp,
unsigned int count)
{
int ret = original_getdents64(fd, dirp, count);
// Filter hidden entries from dirp
return ret;
}
write_cr0(read_cr0() & ~0x10000); // Disable write protection
sys_call_table[__NR_getdents64] = hacked_getdents64;
write_cr0(read_cr0() | 0x10000); // Re-enable write protection
En el ejemplo, para modificar la tabla, un módulo del núcleo primero tendría que desactivar la protección contra escritura en la página de memoria donde reside la tabla. El siguiente código ensamblador (como se ve en Diamorphine) demuestra cómo se puede borrar el bit 20 (Write Protect) del registro de control CR0 , aunque la función write_cr0 ya no se exporte a módulos:
static inline void
write_cr0_forced(unsigned long val)
{
unsigned long __force_order;
asm volatile(
"mov %0, %%cr0"
: "+r"(val), "+m"(__force_order));
}
Una vez deshabilitada la protección contra escritura, la dirección de una llamada de sistema en la tabla puede ser reemplazada por la dirección de una función maliciosa. Tras la modificación, la protección contra escritura se vuelve a activar. Ejemplos notables de rootkits que usaron esta técnica incluyen Diamorphine, Knark y Reveng_rtkit. El enganche de la tabla de syscall tiene varias limitaciones:
- Endurecimiento del núcleo (desde la 2.6.25) se esconde
sys_call_table. - Las páginas de memoria del núcleo se hicieron de solo lectura (
CONFIG_STRICT_KERNEL_RWX). - Características de seguridad como Secure Boot y el mecanismo de bloqueo del kernel pueden dificultar modificaciones en CR0.
La mitigación más definitiva llegó con el kernel de Linux 6.9, que cambió fundamentalmente la forma en que se despachan las llamadas a sistema en la arquitectura x86-64. Antes de la versión 6.9, el kernel ejecutaba llamadas a los sistemas buscando directamente el handler en el sys_call_table array:
// Pre-v6.9 Syscall Dispatch
asmlinkage const sys_call_ptr_t sys_call_table[] = {
#include <asm/syscalls_64.h>
};
A partir del kernel 6.9, el número de llamada syscall se emplea en una instrucción switch para encontrar y ejecutar el handler adecuado. El sys_call_table sigue existiendo, pero solo se puede incluir en compatibilidad con herramientas de trazado y ya no se emplea en la ruta de ejecución de la llamada sys.
// Kernel v6.9+ Syscall Dispatch
long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
{
switch (nr) {
#include <asm/syscalls_64.h>
default: return __x64_sys_ni_syscall(regs);
}
};
Como resultado de este cambio arquitectónico, sobreescribir punteros de función en el sys_call_table en kernels 6.9 y posteriores no afecta la ejecución de la llamada sys, haciendo que la técnica sea completamente ineficaz. Aunque esto nos llevó a suponer que el parcheo de la tabla de llamadas de sistemas ya no era viable, publicamos recientemente la técnica FlipSwitch , que demuestra que este vector está lejos de estar muerto. Este método aprovecha dispositivos específicos de manipulación de registros para desactivar momentáneamente los mecanismos de protección contra escritura del núcleo, permitiendo efectivamente a un atacante saltar la "inmutabilidad" de la ruta moderna de la llamada de sistema y reintroducir ganchos incluso dentro de estos entornos reforzados.
En lugar de dirigir a la sys_call_tablebasada en datos, FlipSwitch se centra en el código máquina compilado de la nueva función de despacho de llamadas de sistema del kernel, x64_sys_call. Debido a que el núcleo ahora emplea una instrucción switch-case masiva para ejecutar llamadas sys, cada llamada tiene una instrucción call codificada en el binario del despachador. FlipSwitch escanea la memoria de la función x64_sys_call para localizar la "firma" específica de una llamada de sistema objetivo, normalmente un código de operación 0xe8 (la instrucción CALL ) seguido de un desplazamiento relativo de 4 bytes que apunta al manejador original y legítimo.
Una vez identificado este sitio de llamada dentro del despachador, el rootkit emplea dispositivos para borrar el bit de Protección contra Escritura (WP) en el registro de control CR0, otorgando acceso temporal de escritura a los segmentos de código ejecutable del núcleo. El desplazamiento relativo original se sobreescribir con un nuevo desplazamiento que apunta a una función maliciosa controlada por el adversario. Esto efectivamente "cambia el interruptor" en el punto de despacho, cerciorando que cada vez que el núcleo intente ejecutar la llamada de sistema destino a través de su ruta moderna de sentencias switch, se redirija al rootkit en su lugar. Esto permite una interceptación fiable y precisa de llamadas de sistema que persiste a pesar del endurecimiento arquitectónico del kernel 6.9.
Parche de Prólogo de Enganche en Línea / Función
El enganche en línea es una alternativa al enganchado mediante tablas punter. En lugar de modificar un puntero en una tabla, el hooking en línea parchea el código de la función de destino. El rootkit escribe una instrucción de salto al inicio (prólogo) de una función del núcleo, que desvía la ejecución al propio código del rootkit. Esta técnica es similar al parche de funciones o a la forma en que funcionan los hooks en modo usuario en Windows (por ejemplo, modificar los primeros bytes de una función para saltar a un desvío).
Por ejemplo, un rootkit podría dirigir a una función del núcleo como do_sys_open (que forma parte del manejo de syscall de archivos abiertos). Al sobreescribir los primeros bytes de do_sys_open con una instrucción x86 JMP a código malicioso, el rootkit cerciora que cada vez que se llama do_sys_open , salte a la rutina del rootkit. La rutina maliciosa puede entonces ejecutar lo que quiera (por ejemplo, comprobar si el nombre de archivo a abrir está en una lista oculta y negar el acceso), y opcionalmente llamar al do_sys_open original para que proceda con el comportamiento normal para archivos no ocultos.
unsigned char *target = (unsigned char *)kallsyms_lookup_name("do_sys_open");
unsigned long hook = (unsigned long)&malicious_function;
int offset = (int)(hook - ((unsigned long)target + 5));
unsigned char jmp[5] = {0xE9};
memcpy(&jmp[1], &offset, 4);
// Memory protection omitted for brevity
memcpy(target, jmp, 5);
asmlinkage long malicious_function(
const char __user *filename,
int flags, umode_t mode) {
printk(KERN_INFO "do_sys_open hooked!\n");
return -EPERM;
}
Este código sobreescribir el inicio de do_sys_open() con una instrucción JMP que redirige la ejecución a código malicioso. El rootkit de código abierto Reptile emplea extensamente parche de funciones en línea mediante un framework personalizado llamado KHOOK (del que hablaremos en breve).
Los ganchos en línea de Reptile apuntan a funciones como sys_kill y otras, habilitando comandos backdoor (por ejemplo, enviar una señal específica a un proceso activa el rootkit para elevar privilegios u ocultar el proceso). Otro ejemplo es Suterusu, que también aplicó parche en línea en algunos de sus ganchos.
El hooking en línea es frágil y de alto riesgo: sobreescribir el prólogo de una función es sensible a diferencias de versión del kernel y del compilador (por lo que los hooks a menudo necesitan parches por compilación o desensamblado en tiempo de ejecución), puede hacer que el sistema se bloquee fácilmente si las instrucciones o la ejecución concurrente no se gestionan correctamente, y requiere saltar las protecciones de memoria modernas (W^X, CR0 WP, firma/bloqueo de módulos) o explotar vulnerabilidades para hacer que el texto del kernel sea grabable.
Enganche de sistemas de archivos virtuales
La capa de Sistema de Archivos Virtual (VFS) en Linux proporciona una abstracción para las operaciones de archivos. Por ejemplo, cuando lees un directorio (como ls /proc), el kernel eventualmente llamará a una función para iterar sobre las entradas del directorio. Los sistemas de archivos definen su propio file_operations con punteros de función para acciones como iterate_shared (para listar el contenido de directorios) o lectura/escritura para E/S de archivos. El hooking VFS consiste en reemplazar estos punteros de función por funciones proporcionadas por rootkit para manipular cómo el sistema de archivos presenta los datos.
En esencia, un rootkit puede conectarse al VFS para ocultar archivos o directorios filtrándolos fuera de los listados de directorios. Un truco común: engancha la función que itera las entradas de directorio y haz que se salten los nombres de archivo que coincidan con un patrón determinado. La estructura file_operations para directorios (particularmente en /proc o /sys) es un objetivo frecuente, ya que ocultar procesos maliciosos a menudo implica ocultar entradas bajo /proc/<pid>.
Consideremos este gancho de ejemplo para una función de listado de directorios:
static iterate_dir_t original_iterate;
static int malicious_filldir(
struct dir_context *ctx,
const char *name, int namelen,
loff_t offset, u64 ino,
unsigned int d_type)
{
if (!strcmp(name, "hidden_file"))
return 0; // Skip hidden_file
return ctx->actor(ctx, name, namelen, offset, ino, d_type);
}
static int malicious_iterate(struct file *file, struct dir_context *ctx)
{
struct dir_context new_ctx = *ctx;
new_ctx.actor = malicious_filldir;
return original_iterate(file, &new_ctx);
}
// Hook installation
file->f_op->iterate = malicious_iterate;
Esta función de reemplazo filtra los archivos ocultos durante las operaciones de listado de directorios. Al conectarse a nivel VFS, el rootkit no necesita manipular tablas de llamadas del sistema ni ensamblaje de bajo nivel; simplemente se aprovecha de la interfaz del sistema de archivos. Adore-NG, un rootkit de Linux que fue popular en su día, empleaba el hooking VFS para ocultar archivos y procesos. Parcheó los punteros de función para la iteración de directorios para ocultar entradas de PIDs y nombres de archivo específicos. Muchos otros rootkits del núcleo tienen código similar para ocultar a sí mismos o a sus artefactos mediante ganchos VFS.
El hooking VFS sigue siendo ampliamente empleado, pero tiene limitaciones debido a cambios en los desplazamientos de la estructura del núcleo entre versiones, lo que puede provocar la rotura de los hooks.
Enganche basado en ftrace
Los kernels modernos de Linux incluyen un poderoso marco de trazado llamado ftrace (function tracer). Ftrace está destinado a depuración y análisis de rendimiento, permitiendo anexar hooks (callbacks) a casi cualquier entrada o salida de una función del núcleo sin modificar directamente el código del núcleo. Funciona modificando dinámicamente el código del núcleo en tiempo de ejecución de forma controlada (a menudo parcheando un trampolín ligero que llama al manejador de trazado). Es importante destacar que ftrace proporciona una API para módulos del núcleo que registran manejadores de trazas, siempre que se cumplan ciertas condiciones (como tener el núcleo construido con soporte ftrace y la interfaz de depuración disponible).
Los rootkits empezaron a abusar de ftrace para implementar ganchos de una forma menos evidente. En lugar de escribir manualmente un JMP en una función, un rootkit puede pedir a la maquinaria ftrace del núcleo que lo haga en su nombre; Básicamente "legitimando" el gancho. Esto significa que el rootkit no tiene que encontrar la dirección de la función ni modificar las protecciones de la página; simplemente registra una callback para el nombre de la función que quiere interceptar, y el kernel instala el hook.
Aquí tienes un ejemplo simplificado de cómo usar ftrace para enganchar el gestor de llamadas de sistema mkdir :
static int __init hook_init(void) {
target_addr = kallsyms_lookup_name(SYSCALL_NAME("sys_mkdir"));
if (!target_addr) return -ENOENT;
real_mkdir = (void *)target_addr;
ops.func = ftrace_thunk;
ops.flags = FTRACE_OPS_FL_SAVE_REGS
| FTRACE_OPS_FL_RECURSION_SAFE
| FTRACE_OPS_FL_IPMODIFY;
if (ftrace_set_filter_ip(&ops, target_addr, 0, 0)) return -EINVAL;
return register_ftrace_function(&ops);
}
Este gancho intercepta la función sys_mkdir y la redirige a través de un manejador malicioso. Rootkits recientes como KoviD, Singularity y Umbra emplearon ganchos basados en ftrace. Estos rootkits registran callbacks ftrace en diversas funciones del núcleo (incluidas las llamadas sys) para monitorizarlas o manipularlas.
El principal beneficio del hook ftrace es que no deja huellas evidentes en tablas globales ni en código parcheado. El enganche se realiza mediante interfaces legítimas del kernel. Para un ojo inexperto, todo parece normal; sys_call_table está intacto, los prólogos de funciones no se sobreescribir manualmente por el rootkit (sí lo sobreescribir el mecanismo ftrace, pero eso es algo común y permitido en un kernel con trazado activado). Además, los ganchos de ftrace a menudo pueden activar o desactivar al vuelo y son inherentemente menos intrusivos que el parche manual.
Aunque el hook ftrace es poderoso, está limitado por los límites del entorno y de privilegios (si se usa desde fuera del kernel). Requiere acceso a la interfaz de trazado (debugfs) y privilegios de CAP_SYS_ADMIN , que pueden no estar disponibles en sistemas reforzados o contenedores donde incluso el 0 UID está restringido por espacios de nombres, LSMs o políticas de bloqueo de arranque seguro. Los depuradores también pueden ser desmontados o de solo lectura en producción por razones de seguridad. Así, aunque un usuario raíz totalmente privilegiado puede normalmente usar ftrace, las defensas modernas a menudo deshabilitan o limitan estas capacidades, reduciendo la practicidad de los hooks basados en ftrace en entornos altamente reforzados.
Ganchos Kprobes
Kprobes es otra característica del núcleo destinada a depuración e instrumentación, que los atacantes reutilizaron para el enganche de rootkits. Los Kprobes permiten acceder dinámicamente a casi cualquier rutina del kernel en tiempo de ejecución registrando un gestor de sondas. Cuando la instrucción especificada está a punto de ejecutar, la infraestructura kprobe almacena el estado y transfiere el control al manejador personalizado. Después de que el manejador se ejecute (incluso puedes modificar registros o el puntero de instrucciones), el núcleo reanuda la ejecución normal del código original. En términos más simples, los kprobes permiten anexar una callback personalizada a un punto arbitrario del código del kernel (entrada de función, instrucción específica, etc.), algo parecido a un punto de interrupción con un manejador.
Usar kprobes para enganches maliciosos suele implicar interceptar una función para evitar que haga algo o para obtener información. Un uso común en rootkits modernos: dado que muchos símbolos importantes (como sys_call_table o kallsyms_lookup_name) ya no se exportan, un rootkit puede desplegar un kprobe en una función que sí tenga acceso a ese símbolo y robarlo. A continuación se muestra una estructura kprobe y el registro.
// Declare a kprobe targeting the symbol "kallsyms_lookup_name"
static struct kprobe kp = {
.symbol_name = "kallsyms_lookup_name"
};
// Function pointer type matching kallsyms_lookup_name
typedef unsigned long
(*kallsyms_lookup_name_t)(const char *name);
// Global pointer to the resolved kallsyms_lookup_name
kallsyms_lookup_name_t kallsyms_lookup_name;
// Register the kprobe; kernel resolves kp.addr
// to the address of the symbol
register_kprobe(&kp);
// Assign resolved address to our function pointer
kallsyms_lookup_name =
(kallsyms_lookup_name_t) kp.addr;
// Unregister the kprobe (only needed it once)
unregister_kprobe(&kp);
Esta sonda se emplea para recuperar el nombre del símbolo de kallsyms_lookup_name, típicamente un precursor del hooking de la tabla syscall. Aunque no está presente en los commits iniciales, una actualización reciente de Diamorphine empleó esta técnica. Coloca un kprobe para captar el puntero de kallsyms_lookup_name mismo (o usa un kprobe sobre una función conocida para obtener indirectamente lo que necesita). De manera similar, otros rootkits usan un kprobe temporal para localizar símbolos, luego lo desregistran una vez terminado, pasando a realizar hooks por otros medios. Las kprobes también pueden usar para enganchar directamente el comportamiento (no solo para encontrar direcciones). O una jprobe (una kproba especializada) puede redirigir una función por completo. Sin embargo, usar kprobes para reemplazar completamente funcionalidades es complicado y no es común, porque es más sencillo parchear o usar ftrace si quieres secuestrar una función de forma constante. Las Kprobes se emplean a menudo para enganchar intermitente o auxiliar.
Las kprobes son útiles pero limitadas: agregan sobrecarga en tiempo de ejecución y pueden desestabilizar sistemas si se colocan sobre funciones de bajo nivel muy calientes o restringidas (las sondas recursivas se suprimen), por lo que los atacantes deben elegir cuidadosamente los puntos de sonda; También son auditables y pueden activar advertencias del kernel o ser registradas por auditoría del sistema, y las sondas activas pueden ver bajo /sys/kernel/debug/kprobes/list (por lo que las entradas inesperadas son sospechosas); Algunos kernels pueden construir sin soporte para kprobe/debug.
Marco de Kernel Hook
Como se mencionó antes, con el rootkit de Reptile, los atacantes a veces crean frameworks de nivel superior para gestionar sus ganchos. Kernel Hook (KHOOK) es uno de esos frameworks (desarrollado por el autor de Reptile) que abstrae el trabajo sucio del parche en línea y proporciona una interfaz más limpia para los desarrolladores de rootkits. Esencialmente, KHOOK es una biblioteca que permite especificar una función para enganchar y tu reemplazo, y se encarga de modificar el código del kernel mientras proporciona un trampolín para llamar a la función original de forma segura. Para ilustrar, aquí tienes un ejemplo de cómo se podría usar una macro similar a KHOOK (basada en el uso de Reptile) para enganchar la llamada de kill sys:
// Creates a replacement for sys_kill:
// long sys_kill(long pid, long sig)
KHOOK_EXT(long, sys_kill, long, long);
static long khook_sys_kill(long pid, long sig) {
// Signal 0 is used to check if a process
// exists (without sending a signal)
if (sig == 0) {
// If the target is invisible (hidden by
// a rootkit), pretend it doesn't exist
if (is_proc_invisible(pid)) {
return -ESRCH; // No such process
}
}
// Otherwise, forward the call to the original sys_kill syscall
return KHOOK_ORIGIN(sys_kill, pid, sig);
}
KHOOK opera mediante parcheo de funciones en línea, sobrscribiendo prólogos de funciones con un salto a manejadores controlados por el atacante. El ejemplo anterior ilustra cómo sys_kill() se redirige a un manejador malicioso si la señal de apagada es 0.
Aunque KHOOK simplifica el parcheo en línea, sigue heredando todos sus inconvenientes: modifica el texto del kernel para insertar jump stubs, por lo que protecciones como el bloqueo del kernel, Secure Boot o W^X pueden bloquearlo. También dependen de la arquitectura y la versión (comúnmente limitados a x86 y fallan en kernel 5.x+), lo que los hace frágiles entre compilaciones.
Técnicas de enganche en el espacio de usuario
El hooking en espacio de usuario es una técnica que apunta a la capa libc, u otras bibliotecas compartidas a las que se accede a través del enlace dinámico, para interceptar llamadas a API comunes empleadas por herramientas de usuario. Ejemplos de estas llamadas incluyen readdir, getdents, open, fopen, fgetsy connect. Al interponer funciones de reemplazo, un atacante puede manipular herramientas de usuario ordinarias como ps, ls, lsofy netstat para devolver vistas alteradas o "sanitizadas". Esto se emplea para ocultar procesos, archivos, sockets u ocultar pruebas de código malicioso.
Los métodos comunes para implementar esto reflejan cómo el enlazador dinámico resuelve símbolos o implican modificar la memoria del proceso. Estos métodos incluyen usar la variable de entorno LD_PRELOAD o LD_AUDIT para forzar una carga temprana de un archivo malicioso de objeto compartido (.so), modificar entradas de ELF DT_* o rutas de búsqueda de bibliotecas para priorizar una biblioteca hostil, o realizar sobrescribencias GOT/PLT en tiempo de ejecución dentro de un proceso. Sobreescribir el GOT/PLT normalmente implica cambiar la configuración de protección de memoria (mprotect), escribir el nuevo código (write) y luego restaurar la configuración original (restore) tras la inyección.
Una función ganchuda suele llamar al símbolo real libc usando dlsym(RTLD_NEXT, ...) para su funcionamiento normal. Luego filtra o altera los resultados solo para los objetivos que pretende ocultar. A continuación se muestra un ejemplo básico de un filtro LD_PRELOAD para la función readdir() .
#define _GNU_SOURCE // GNU extensions (RTLD_NEXT)
#include <dlfcn.h> // dlsym(), RTLD_NEXT
#include <dirent.h> // DIR, struct dirent, readdir()
#include <string.h> // strstr()
// Pointer to the original readdir()
static struct dirent *(*real_readdir)(DIR *d);
struct dirent *readdir(DIR *d) {
if (!real_readdir) // resolve original once
real_readdir =
dlsym(RTLD_NEXT, "readdir");
struct dirent *ent;
// Fetch next dir entry from real readdir
while ((ent = real_readdir(d)) != NULL) {
// If name contains the secret marker,
// skip this entry (hide it)
if (strstr(ent->d_name, ".secret"))
continue;
return ent; // return visible entry
}
return NULL; // no more entries
}
Este ejemplo sustituye readdir() en proceso proporcionando una biblioteca resuelta antes que el libcreal, ocultando efectivamente nombres de archivo que coinciden con un filtro. Herramientas históricas de ocultación en modo usuario y "rootkits" ligeros emplearon parches LD_PRELOAD o GOT/PLT para ocultar procesos, archivos y sockets. Los atacantes también inyectan objetos compartidos en servicios específicos para lograr un sigilo dirigido sin necesidad de módulos del kernel.
La interposición en el espacio de usuario afecta solo a los procesos que cargan la biblioteca maliciosa (o en los que se inyectan). Es frágil para la persistencia a nivel de sistema (archivos de servicio/unidad, entornos sanitizados, binarios setuid/estáticos lo complican). La detección es sencilla en relación con los hooks del kernel: comprueba entradas sospechosas de LD_PRELOAD/LD_AUDIT , objetos compartidos mapeados inesperados en /proc/<pid>/maps, discrepancias entre bibliotecas en disco e importaciones en memoria, o entradas GOT alteradas. Las herramientas de integridad, los supervisores de servicio (systemd) y la simple inspección de memoria de procesos suelen exponer esta técnica.
Técnicas de enganche usando eBPF
Un modelo de implementación de rootkit más reciente implica el abuso de eBPF (Filtro de Paquetes Berkeley extendido). eBPF es un subsistema en Linux que permite a los usuarios privilegiados cargar programas en bytecode en el núcleo. Aunque a menudo se describe como una "máquina virtual en formato sandbox", su seguridad en realidad depende de un verificador estático que garantiza que el bytecode sea seguro (sin bucles infinitos, sin acceso ilegal a la memoria) antes de compilar en JIT en código máquina nativo para una ejecución casi nula de latencia.
En lugar de insertar un LKM para modificar el comportamiento del kernel, un atacante puede cargar uno o más programas eBPF que se anexan a eventos sensibles del kernel. Por ejemplo, se puede escribir un programa eBPF que se anexe a la entrada de llamada del sistema para execve (mediante un kprobe o tracepoint), permitiéndole monitorizar o manipular la ejecución del proceso. De manera similar, eBPF puede conectarse a la capa LSM (como notificaciones de ejecución de programas) para impedir ciertas acciones u ocultarlas. A continuación se muestra un ejemplo.
// Attach this eBPF program to the tracepoint for sys_enter_execve
SEC("tp/syscalls/sys_enter_execve")
int tp_sys_enter_execve(struct sys_execve_enter_ctx *ctx) {
// Get the current process's PID and TID as a 64-bit value
// Upper 32 bits = PID, Lower 32 bits = TID
__u64 pid_tgid = bpf_get_current_pid_tgid();
// Delegate handling logic to a helper function
return handle_tp_sys_enter_execve(ctx, pid_tgid);
}
Dos ejemplos públicos destacados son TripleCross y Boopkit. TripleCross demostró un rootkit que usaba eBPF para enganchar llamadas de sistema como execve para persistencia y ocultación. Boopkit empleaba eBPF como canal de comunicación encubierto y puerta trasera, conectando programas eBPF que podían manipular búferes de socket (permitiendo que una parte remota se comunicara con el rootkit mediante paquetes elaborados). Estos son proyectos de prueba de concepto, pero demostraron la viabilidad de eBPF en el desarrollo de rootkits.
Los principales beneficios son que el enganche eBPF no requiere cargar un LKM y es compatible con las protecciones modernas del kernel. Para núcleos soportados por eBPF, esta es una técnica poderosa. Pero aunque son poderosos, también están limitados. Necesitan privilegios elevados para cargar, están limitados por las comprobaciones de seguridad del verificador, son efímeras en reinicios (requiriendo persistencia separada) y cada vez son más detectables por herramientas de auditoría o forenses. El uso de eBPF será especialmente visible en sistemas que normalmente no emplean herramientas eBPF.
Técnicas de evasión usando io_uring
While io_uring is not used for hooking, it deserves a honorable mention as a recent addition to the EDR evasion techniques used by rootkits. io_uring is an asynchronous, ring-buffer-based I/O API that lets processes submit batches of I/O requests (SQEs) and reap completions (CQEs) with minimal syscall overhead. It is not a hooking framework, but its design changes the syscall/visibility surface and exposes powerful kernel-facing primitives (registered buffers, fixed files, mapped rings) that attackers can abuse for stealthy I/O, syscall-evading workflows, or, when combined with a vulnerability, as an exploit primitive that leads to installing hooks at a lower layer.
Los patrones de ataque se dividen en dos clases: (1) evasión/abuso de rendimiento: un proceso malicioso emplea io_uring para realizar muchas lecturas/escrituras/operaciones de metadatos en grandes lotes, de modo que los detectores tradicionales por llamada de sistema detectan menos eventos o patrones atípicos; y (2) habilitación de exploits: los errores en io_uring superficies (mapeos de anillo, recursos registrados) fueron históricamente el vector de escalada de privilegios, tras lo cual un atacante puede instalar hooks del kernel por medios más tradicionales. io_uring también evita algunos envoltorios libc si el código envía operaciones directamente, por lo que el hooking de usuario, que intercepta llamadas libc, puede eludir. A continuación se ilustra un flujo sencillo de envío/cosecha:
// Minimal io_uring usage (error handling omitted)
// io_uring context (SQ/CQ rings shared with kernel)
struct io_uring ring;
// Initialize ring with space for 16 SQEs
io_uring_queue_init(16, &ring, 0);
// Grab a free submission entry (or NULL if full)
struct io_uring_sqe *sqe =
io_uring_get_sqe(&ring);
// Prepare SQE as a read(fd, buf, len, offset=0)
io_uring_prep_read(sqe, fd, buf, len, 0);
// Submit pending SQEs to the kernel (non-blocking)
io_uring_submit(&ring);
struct io_uring_cqe *cqe;
// Block until a completion is available
io_uring_wait_cqe(&ring, &cqe);
// Mark the completion as handled (free slot)
io_uring_cqe_seen(&ring, cqe);
El ejemplo anterior muestra una cola de envío que alimenta muchos archivos de operaciones al kernel con una o pocas llamadas de sistema io_uring_enter , reduciendo la telemetría por llamada de sistema por operación.
Los adversarios interesados en la recogida de datos de forma sigilosa o en la exfiltración de alto rendimiento pueden cambiar a io_uring para reducir el ruido de la llamada de sistema. io_uring no instala inherentemente ganchos globales ni cambia el comportamiento de otros procesos; Es local de procesos a menos que se combine con la escalada de privilegios. La detección es posible instrumentando las llamadas de sistema io_uring (io_uring_enter, io_uring_register) y observando patrones anómalos: lotes inusualmente grandes, muchos archivos/búferes registrados, o procesos que realizan operaciones pesadas de metadatos en lotes. Las diferencias en las versiones del kernel también importan: io_uring características evolucionan rápidamente, por lo que las técnicas del atacante pueden depender de la versión. Finalmente, como io_uring requiere un proceso malicioso en ejecución, los defensores a menudo pueden interrumpirlo e inspeccionar sus anillos, archivos registrados y mapeos de memoria para descubrir un mal uso.
Conclusión
Las técnicas de hooking en Linux avanzaron mucho desde que simplemente sobreescribir un puntero en una tabla. Ahora vemos a atacantes explotando marcos legítimos de instrumentación del kernel (ftrace, kprobes, eBPF) para implantar ganchos más difíciles de detectar. Cada método, desde los parches de IDT y la tabla de syscall hasta los hooks en línea y las sondas dinámicas, tiene sus propios beneficios en sigilo y estabilidad. Los defensores deben estar atentos a todos estos posibles vectores. En la práctica, los rootkits modernos suelen combinar múltiples técnicas de enganche para lograr sus objetivos. Por ejemplo, PUMAKIT emplea un gancho directo de tabla syscall y ganchos ftrace, y Diamorphine emplea ganchos syscall más un kprobe para evitar ocultar símbolos. Este enfoque en capas implica que las herramientas de detección deben comprobar muchos aspectos del sistema: entradas IDT, tablas de syscall, registros específicos del modelo (para los ganchos de sysenter), integridad de los prólogos de funciones, contenido de punteros de función críticos en estructuras (VFS, etc.), operaciones ftrace activas, kprobes registrados y programas eBPF cargados.
En la segunda parte de este serial, pasamos de la teoría a la práctica. Armados con el conocimiento de la taxonomía de rootkit y las técnicas de hooking que aquí se tratan, nos centraremos en la ingeniería de detección, construyendo y aplicando estrategias prácticas de detección para identificar estas amenazas en entornos Linux reales.
