Integrantes : Alexandre VIGNAL y Cédric ESCUDERO
En varios casos, como en una relación padres-hijo, puede ser útil vigilar la actividad de una persona en una máquina para asegurarse de la ausencia de comportamientos digitales arriesgados. La base de este tipo de vigilancia consiste en el seguimiento de la actividad del teclado del computador, y es más conocido bajo el nombre de “keylogger”.
Este contexto ha sido la base del proyecto que hemos desarrollado. Desde un punto de vista técnico, la parte la más importante del proyecto tendrá el siguiente propósito: supervisar, en tiempo real, la actividad del teclado de la máquina "observada" en un proceso que se ejecuta en segundo plano, invisible para un usuario no experimentado.
Entonces, emitimos las siguientes especificaciones:
Para conservar los rastros de la actividad del usuario “espiado”, el programa debe ser capaz de guardar toda la información en un archivo local, en un directorio conveniente para no ser "descubierto" por el usuario en cuestión.
Un segundo programa, llamado el monitor, será diseñado para ser ejecutado por el usuario "espía". Este programa debe permitir las siguientes acciones:
Listar todos los espiados que están ejecutando el keylogger. De hecho, pensando en mayor escala, este programa podría ser utilizado para controlar una lista de personas en riesgo (todos los niños de una familia, por ejemplo). Cada una de estas personas se identifica por un número que corresponde al número del socket de conexión creado al lanzamiento del keylogger.
El programa debe permitir seleccionar uno de los computadores observado que esta corriendo el keylogger para observar su actividad en vivo, directamente en la consola linux.
Este programa también debe tener una función para descargar el histórico completo de las teclas que fueron presionadas por una de las máquinas que ejecutan el keylogger. Eso tendrá dos consecuencias: del lado del espía: la creación de un archivo donde serán guardadas todas las informaciones. Del lado espiado: el vaciado del archivo de historia para no tener redundancia de informaciones en una solicitud futura.
Para funcionar correctamente y continuamente, proponemos crear un tercero programa. Este programa, que llamaremos 'servidor', funcionará continuamente en un servidor público de tipo “Aragorn”. Esperará tanto por la conexión de los usuarios del keylogger: "los espiados", como por la conexión del usuario que desea rastrear su actividad: "el espía". De esta manera, aseguramos estar siempre listo para crear la conexión entre el keylogger y un servidor, conexión que al lanzamiento de los programas de vigilancia. Este servidor también permite la comunicación de datos entre el espiado y la espía en mayor escala que la red local, ya que el programa ejecutado en Aragorn permitirá el “forwarding” de los datos entre las diferentes redes.
De hecho, como se explicó anteriormente, este proyecto consiste en un conjunto de tres programas.
El keylogger que, ejecutándose en la máquina a observar, analiza, almacena y envía al servidor las introducciones de datos del teclado.
El servidor, ejecutado en Aragorn, que espera para las conexiones por parte del espiado y del espía. Este programa reenvía los mensajes desde el espiado al espía y viceversa.
El monitor que, corriendo en la máquina del espía, permite imprimir la lista de los usuarios espiados conectados, enviar una solicitud para descargar el histórico de los comandos ingresados o ver en vivo la actividad del usuario observado.
Esta sección pretende detallar los puntos importantes del funcionamiento de cada uno de estos programas, y destacar aquellos son relacionados con los temas abordados durante este ramo de programación de sistemas.
Para trabajar, este programa corre dos softwares externos: xinput y xmodmap.
Xinput es un programa nativo instalado en la mayoría de las máquinas linux y que, según su página man, "permite listar todos los dispositivos de entrada disponibles, solicitar información de estos dispositivos y modificar sus parámetros de funcionamiento.
Utilizado de la siguiente manera: "xinput test [KeyboardID]", xinput captura cada evento (presión y relaja) generado por el teclado y envía un mensaje a la salida estándar. Estos mensajes son de la forma:
Capturar y procesar esos mensajes dentro de nuestro programa nos permitirá analizar y almacenar cada uno de los eventos generados por el teclado del usuario espiado.
Sin embargo, para poder ejecutar xinput desde el programa keylogger, es útil conocer el ID [KeyboardID] del teclado que se observará. Para eso, xinput tiene una opción '--list'. La salida que muestra el programa con esta opción es de la siguiente forma:
El teclado estándar siempre tiene el siguiente nombre: « AT Translated Set 2 keyboard». Por lo tanto, tenemos que extraer, dentro de nuestro programa, el identificador asociado con este nombre de teclado. Para eso, utilizamos el siguiente código:
Explicación:
1: Creamos una pipa para capturar la información que será devuelta por xinput.
2: Abrimos un FILE* en lectura desde el descriptor de archivo de entrada pfd_from_xinput_keyid [0].
3: Creamos un nuevo proceso.
4: en este proceso, redirigimos la salida estándar dentro de la pipa creada previamente.
5: En este mismo proceso, corremos una instancia del programa xinput. Los parámetros indicados permiten mostrar sólo la línea con los datos del teclado deseado. Esta línea, gracias a la función dup2(...), no se muestra en la salida estándar pero está enviada en la pipa y recuperada por nuestro programa.
Después, utilizamos el siguiente código para extraer solamente el ID del teclado en la línea recibida. La función getline de c ++ está utilizada con varios separadores para extraer solamente la parte antes de este separador (en nuestro caso solamente el ID).
En esta etapa del programa, somos capaces de iniciar un nuevo proceso en segundo plano que ejecuta el programa xinput, especificando el buen ID del teclado que queremos vigilar. La creación del nuevo proceso y de la pipa utilizados para ejectuar este programa es totalmente similar a la anterior.
XInput es ejecutado con los parámetros adecuados con la línea siguiente :
execlp ("xinput", "xinput", "prueba", (keyid.c_str), (char *) 0);
Sin embargo, antes de la utilización de xinput, es útil obtener la tabla de conversión “números de tecla” -> “significación de la tecla”. Para eso, se utilice el comando ' xmodmap – pke’. Este comando imprime una lista de las diferentes significaciones de cada una de las teclas, según que estemos en mayúsculas, minúscula, o que estemos presionando la tecla alt-gr por ejemplo. El resultado es de la forma:
De la misma forma que anteriormente, este comando se ejecuta desde un nuevo proceso. La información obtenida a través de una nueva pipa se interpreta por una sucesión de getline en nuestro programa, para extraer los campos que nos interesan.
Cada campo está insertado en una “map” correspondiente en C++. Tenemos tres objetos de tipo “map”, keymap, keymap_shift y keymap_altgr. De esta manera, para cada identificador (numero) de tecla, podemos acceder a su significación en las diferentes tablas, de la siguiente manera:
Pues, en este punto, tenemos una tabla de conversión que tiene la siguiente forma:
Para más legibilidad, creamos una nueva función llamada justo después de la ejecución de xmodmap, que recorre las tres tablas y reemplaza los nombres estándar de las teclas con el símbolo que queremos asociar. Por ejemplo, “ampersand” se convierte en “&”, “space” se convierte en “ “, y “Backspace” se convierte en “[◄]”. Por razones de claridad, este código no se muestra aquí. El resultado final de la tabla es el siguiente :
Ahora el programa keylogger está listo para recibir las informaciones transmitidas por el proceso en que se ejecuta xinput, y para procesarlas correctamente. Sin embargo, para enviar estas informaciones a un servidor distante, necesitamos establecer una conexión. Por lo tanto, el keylogger está conectado al servidor que corre continuamente en Aragorn a través del siguiente código.
Una vez conectado al servidor, para cada nueva línea que se envía en la pipa ligada con xinput, extraemos los dos campos útiles en variables dedicadas: ‘state’ ("press" o "realeased") y ‘key’ (número de la tecla). Estos campos así como las tablas de conversión creadas previamente son ingresadas a la función interpretAndSendKey. Esta función realiza un primer tratamiento para saber si el usuario presionó una tecla calquiera o si se pusó en mayúscula, presionó la tecla Shift, Alt-Gr, etc… Con este primer tratamiento, esta función envía, si hay necesidad, el buen carácter o el buen símbolo al servidor y lo almacena en un archivo de texto local para guardar un histórico de las teclas presionadas.
El siguiente esquema trata de sintetizar los diferentes procesos creados y las comunicaciones entre el programa principal y estos procesos en los que corren los softwares externos.
De esta manera, hemos logrado a crear un keylogger genérico, legible, conectado a un servido externo y que está ejecutado de forma invisible en segundo plano en una maquina Linux.
Cuando el server corre, crea una nueva hebra “monitor” para esperar una comunicación TCP/IP con el programa monitor en el puerto asignado (12346 por omisión). Esta hebra es concurrente con la hebra la principal que ejecuta el resto del método main, para establecer las comunicaciones TCP/IP con los clientes (keylogger) en un puerto distinto (por defecto 12345). Creación de la thread monitor en la thread main :
Parte interesante de la función monitor : espera de la conexión del monitor:
Cuando la hebra monitor está ejecutada, escuchamos el puerto del monitor hasta que una conexión llega. Cuando llega, aceptamos la conexión y mostramos en la salida estándar del servidor "Monitor connected".
Ahora veamos el otro hilo del programa server: el hilo principal:
Al principio, esta hebra espera a una conexión en el puerto del cliente. Cuando una conexión llega, añadimos el número de socket dentro un arreglo. De efecto, nuestro server debe ser capaz de almacenar los datos de más de un solo cliente. De hecho, creamos un arreglo ptr[FD_SETSIZE] donde almacenamos los números de sockets de los clientes conectados al puerto definido.
Una vez que un primer cliente está conectado, ingresamos en un bucle infinito en el cual revisamos, a cada paso, el estado de los sockets para saber si hay datos que han llegados. Si detectamos una actividad, podemos tener que: aceptar nuevas conexiones, o procesar los datos de los clientes ya existantes.
Revisar el estado de los sockets se hace gracias a la función bloqueante “select”, que aquí espera (sin consumir actividad en la CPU) hasta que haya actividad en uno de los “file descriptors” (socket en nuestro caso) presentes en readfds.
Cuando una actividad ocurre en cualquier de estos filedescripotrs, revisamos el socket ligado al puerto en el cual esperamos para nuevos clientes, para saber si nos llegó une nueva conexión.
Si este socket recibió actividad, aceptamos la conexión del nuevo cliente y actualizamos la lista de sockets en el arreglo asociado.
Si el estado de uno de los sockets dentro el arreglo cambió, recuperamos el número de cliente (numClient) dentro del arreglo y almacenamos la información que fue enviada por el cliente en un buffer (buf).
Este proceso maneja dos variables compartidas: numClient y buf que están también utilizado por la hebra monitor (enviados al programa monitor) después de su actualización en el main. Por eso, tenemos que diseñar un mecanismo de sincronización de estos datos, usando mutex.
Mecanismo de synchronisacion :
- Thread main :
- Thread monitor :
Podemos ver en el mecanismo de sincronización de la hebra principal que tenemos una condición aplicada en el número de byte recibido. En efecto, si N>0 es decir que recibimos datos. En este caso debemos cargar el buffer con estos datos y avisar la thread monitor que el buffer esta listo en usando la variable de condición bufferpret. Liberamos después el mutex y enviamos una señal para avisar la otra thread que la variable de condición ha cambiado.
Un N que no está superior a 0 significa que ocurrió un problema con el socket (desconexión del cliente, etc.). En este caso, borramos el socket del arreglo y liberamos el mutex, sin avisar la hebra monitor de la llegada de nuevos datos, ya que estos datos no son interesantes para él.
Del lado del hilo monitor, en el cual llegamos después de actualizar el buffer de recepción, enviamos los datos al programa monitor. Antes de transmitir estos datos, enviamos el número del socket (client) asociado para que el monitor sepa de donde vienen las nuevas informaciones.
Esperamos una primera respuesta de este servidor para confirmarnos que el número de socket fue bien recibido. Una vez esta confirmación obtenida, empezamos la transmisión de los datos del buffer obtenidos por el keylogger. Por fin, liberamos el mutex y enviamos una señal para avisar a la otra hebra que la variable de condición cambió de estado...
En otra máquina, un usuario espera los datos de todos los clientes. El ejecuta el programa monitor. Este programa se conecta al servidor por un puerto distinto de los clientes (y donde la conexión es única). Cuando está conectado con el servidor, el programa ingresa en un bucle infinito en el que revisa continuamente el estado de los files descripors añadidos en la función select para saber si nuevos datos llegaron: estos datos pueden venir de dos tipos de fuentes diferentes :
Revisar el estado de los sockets se hace de la siguiente manera:
Cuando cualquier estado cambia, revisamos si el socket asociado al servidor recibo datos:
Primero, esperamos la recepción del número de cliente. Cuando este número llega, lo guardamos en una variable numClient. Buscamos con la función findNum si este número de cliente ya existe dentro nuestro arreglo de clientes conectados ptrnumClient. Si no existe (it=-1), añadimos este socket dentro este arreglo y lo actualizamos. Después, recibimos los datos del servidor que corresponden a lo que fue ingresaba por el cliente elegido.
Si el usuario ingreso por el teclado un numero de cliente pues mostramos el flujo del cliente en vivo en el mismo tiempo que almacenamos los datos en un archivo con el nombre "numerodecliente.txt". Si el número de socket observado no es el mismo que el que tiene actividad, almacenamos solamente los datos en el archivo guardado en la máquina del monitor.
Recuperar y tratar los datos ingresados en el teclado :
Cuando el file descriptor de la entrada estándar (0) esta modificado, es decir cuando el usuario pulsa el teclado, recuperamos el valor ingresado y actualizamos el menú con la función displayMenu().
El siguiente esquema sintetiza las comunicaciones posibles, y muestra el interés del programa servidor : corriendo continuamenteen un servidor publico, este programa permite conectar los keyloggers al monitor aunque se ubican en redes separadoras...
En conlusion, hemos logrado a cumplir con todas las especificaciones que habiamos fijado al principio del desarrollo del proyecto, menos la posibilidad de pedir el historico a un espiado desde el monitor. Tenemos unos programas funcionales, probados en varios computadores. Sin embargo, la ejecucion en condicion real del conjunto de programas no fue probada. De efecto, no tenemos la posibilidad de hacer correr el programa server en un servidor publico para intentar conectar los clientes al monitor desde redes diferentes. El único servidor publico que se pusó como "candidato" para correr el programa server era Aragorn (.elo.utfsm.cl), pero el no dispone de puertos libres abiertos...
También quedan posibilidades de mejoras. Hemos por ejemplos pensado en:
Por fin, podemos decir que gracias a este proyecto, hemos implementados en los diferentes programas los conceptos estudiados en el ramo siguientes:
Por eso, quedamos ahora con la impresión de haber recorrido una grande parte del material del ramo, ramo que nos dio competencias de alto nivel para desarrollar un programa bastante complejo, que uno logra normalmente difícilmente a llegar a buen término sin el apoyo de personas competentes en cada dominio evocado.
Para funcionar, el programa keylogger necesita que sean instalados en la maquina observada los programas externales siguientes :