Inicio / Blog / Malware en Linux: Rootkits en espacio de kernel

Malware en Linux: Rootkits en espacio de kernel

Publicado el 26/03/2015, por Antonio López (INCIBE)
Malware en Linux

En un artículo anterior se introducía el concepto de rootkit y su versión más sencilla: los rootkits que operan en modo usuario. A continuación, vamos a ver cómo un opera un rootkit en modo kernel, y cómo es capaz de integrarse e interaccionar con la ejecución del propio kernel.

Kernel Linux y módulos dinámicos

El Kernel de Linux es de tipo monolítico, es decir toda la gestión de recursos, organización/planificación de procesos y acceso a dispositivos recae sobre él. Desde la versión 2.2, aparece la funcionalidad de "módulos" conocida como Loadable Kernel Modules (LKM), un mecanismo que permite al kernel cargar/descargar en memoria código adicional. Gracias a los módulos, el kernel se hace mucho más flexible siendo posible cargar en tiempo de ejecución el código necesario para realizar nuevas tareas, todo ello sin necesidad de recompilar el kernel. Esta funcionalidad es muy útil por ejemplo para cargar drivers de dispositivos, pero también abre una importante vía para el desarrollo de software malintencionado, como los rootkits.

Operación de un rootkit en modo kernel

Es muy habitual que un rootkit que actúe a nivel del kernel utilice la funcionalidad de módulos dinámicos. Existen otros modos de operación que manipulan directamente el propio núcleo del kernel que se está ejecutando en lugar de añadir módulos. Esta técnica, ha sido históricamente explotada accediendo directamente a la imagen en memoria del Kernel a través del fichero /dev/kmem. Sin embargo la evolución del kernel de Linux, trajo consigo mayores medidas de seguridad y esta interfaz fue desactivada desde la versión 2.6. Estas y otras razones más complejas han consolidado el uso de LKM como la técnica más frecuentemente utilizada por los rootkits en modo kernel.

Llamadas al sistema

En un sistema operativo el modo usuario y el modo Kernel interaccionan y se comunican entre sí a través de un mecanismo intermedio. En el caso de Linux, el interfaz que permite este intercambio entre espacio de usuario y espacio de Kernel es Glibc, librería principal del sistema encargada de mediar en las llamadas al sistema. De este modo, cualquier tarea en el espacio de usuario que necesite acceder a recursos privilegiados utilizará el interfaz de comunicación proporcionado través de Glibc.

Syscalls y Glibc. Interfaz de comunicación entre espacio de usuario y espacio de kernel

- Syscalls y Glibc. Interfaz de comunicación entre espacio de usuario y espacio de kernel -

Cuando una llamada al sistema (por ejemplo open() para acceder a un fichero) se produce, el flujo de eventos que sucede es el siguiente:

1. Una interrupción en la ejecución del modo usuario se produce (en Linux una instrucción 0x80 o sysenter en procesadores modernos).

2. El controlador de la interrupción consulta el evento producido en la tabla de interrupciones (IDT) y determina que debe realizar una llamada al sistema o syscall. La ejecución entra en espacio de kernel a través de la llamada al sistema.Mediante la syscall se consulta la tabla de llamadas (sys_call_table) y se comprueba la dirección de memoria de la función solicitada y saltando a esa dirección. El interfaz entre el espacio de usuario y espacio de kernel que maneja la llamada al sistema en Linux es las librería gclibc.

3. La función solicitada se ejecuta y se devuelve el control al programa de usuario.

En la siguiente figura se muestra el flujo que se produce en una llamada al sistema:

Llamadas al sistema (system calls): Interrupción(sysenter, 0x80) y flujo de ejecución usuario <-> kernel

- Llamadas al sistema (system calls): Interrupción(sysenter, 0x80) y flujo de ejecución usuario <-> kernel -

Interceptación de syscalls con módulos dinámicos

En un rootkit en espacio de usuario, se interceptan las librerías dinámicas que llaman los binarios en espacio de usuario. El objetivo de un rootkit en modo kernel es directamente una llamada al sistema, es decir, interceptar la invocación a una función del propio kernel.

Para conseguir interceptar la ejecución de una llamada al sistema, el rootkit puede optar por:

a. Modificar la tabla de llamadas original de modo que cada llamada apunte a otras ubicaciones donde se han dispuesto funciones modificadas (syscall hooking).

b. O bien, manipular la tabla de descriptores de interrupción (IDT), en concreto la correspondiente a syscall (0x80) para llevar a una tabla de syscalls modificada (Interrupt hooking).

El rootkit debe tomar el control del flujo de ejecución en todas aquellas llamadas del sistema que le convenga interceptar. La opción más frecuente es la de syscall hooking y modificar las entradas de la tabla de llamadas original (sys_call_table[]) sustituyendo la entrada correspondiente de la llamada a la función original por otra dirección donde se ha dispuesto el código malicioso.

Módulos del kernel y hooking de syscalls

Los módulos del kernel constituyen una importante funcionalidad que se puede aprovechar para realizar hooking de llamadas al sistema y modificar su comportamiento. Para ello un módulo preparado para interceptar una llamada al sistema debe realizar,, como mínimo las siguientes tareas:

1. Localizar la dirección de la tabla de llamadas (sys_call_table[])

2. Habilitar la escritura de la zona de memoria donde se ubica la tabla de llamadas puesto que se encuentra en una zona protegida de solo lectura.

3. Modificar en la tabla de llamadas, las entradas de las funciones del kernel escogidas para conseguir el objetivo buscado (ocultar procesos, ficheros, etc.) de modo que apunten otras funciones maliciosas.

4. Definir l las tareas a realizar por las funciones modificadas para conseguir el objetivo.

5. Adicionalmente,  llevar a cabo las acciones necesarias para  ocultar su propia presencia, de modo que no sea visible en /proc/modules, /sys/module o en el listado proporcionado por lsmod.

Módulos del kernel en Linux: estructura

Según la documentación de GNU/Linux, un módulo del kernel debe incluir la siguiente estructura mínima: Librerías necesarias, Información del módulo y funciones de inicialización y destrucción con las operaciones deseadas.

Estructura básica de un módulo del kernel

-Estructura básica de un módulo del kernel -

El módulo una vez compilado puede cargarse en memoria e integrarse con el kernel a través del comando insmod. Otros comandos de interés para el manejo de módulos son lsmod para listar los módulos en memoria y rmmod para la descarga de módulos.

Puede ampliarse información sobre la programación de módulos en el siguiente enlace: http://www.tldp.org/LDP/lkmpg/2.6/html/

Hooking de syscalls con módulos kernel: Un ejemplo

Utilizar un módulo del kernel para interceptar y manipular llamadas al sistema se convierte en uno de los recursos más utilizados por los rootkits, dada su relativa sencillez y su potencia. Por ello es importante conocer el modo de operación habitual de los módulos. A través de un ejemplo comprobaremos como se intercepta una syscall. En este ejemplo se ha creado un módulo para interceptar la llamada al sistema getcwd realizada por el binario /bin/pwd. Este binario devuelve la ruta actual donde nos encontramos. Con el comando strace que muestra las llamadas al sistema en la ejecución de un binario, observamos las funciones invocadas para escoger un objetivo:

Strace. Llamadas al sistema realizadas por /bin/pwd. Obsérvese la syscall getcwd

- Strace. Llamadas al sistema realizadas por /bin/pwd. Obsérvese la syscall getcwd -

Efectivamente, entre las llamadas realizadas por el binario /bin/pwd observamos getcwd como objetivo a sustituir. El módulo de ejemplo, interceptará las llamadas a la syscall getcwd para manipular el resultado de /bin/pwd de modo que, en lugar de mostrar la ruta actual, muestre la cadena "/home/pepito".

Los pasos a realizar por el módulo serán:

1. Localiza la dirección de la sys_call_table[]

2. Puesto que la zona de memoria donde reside la sys_call_table está en modo protegido (solo lectura) ha de poner la página de memoria en modo escritura, modificando un bit del registro 0 del procesador.

3. Una vez eliminada la protección contra escritura, modifica la entrada correspondiente a getcwd (sys_call_table[__NR_getcwd]).

4. Define una función alternativa que será llamada en lugar de la original (getcwd). Esta función modificará la cadena que contiene la ruta actual por "/home/pepito"

El resultado lo podemos comprobar en la siguiente ilustración:

Intercepción de la syscall getcwd manipulada para que /bin/pwd devuelva siempre "/home/pepito"

- Intercepción de la syscall getcwd manipulada para que /bin/pwd devuelva siempre "/home/pepito" -

Efectivamente en las llamadas al sistema se puede comprobar como la interceptación ha modificado los valores en la función getcwd:

Interceptación por el módulo de la función getcwd

- Interceptación por el módulo de la función getcwd -

La programación de módulos del kernel es sin duda un mecanismo de gran eficacia para el desarrollo de rootkits. No obstante, ha de ser acompañada por las medidas necesarias que hagan su detección y eliminación lo más difícil posible. Un rootkit en espacio de kernel cuidadosamente diseñado puede llegar a ser prácticamente indetectable incluso por usuarios privilegiados o herramientas avanzadas y, dada su potencia ha de considerarse una amenaza muy importante.