Resolviendo el #TuentiChallenge4: Challenge 6 – Man in the middle

A diferencia de otros concursos de programación, en Tuenti van más allá de la pregunta algorítmica. Les encanta poner acertijos, problemas con pocas pistas y animarnos a investigar. En esta edición, el problema 6 fue el primero con la “firma Tuenti” (al menos para mi).

La situación que nos planteaban era la de protagonizar un ataque conocido como “Man in the middle”. El problema nos enseñaba cómo se comunicaban un cliente y un servidor y nos pedían meternos en medio de la conversación para capturar sus mensajes.

Este problema me gustó bastante pues nunca había hecho nada sobre encriptación ni seguridad y el problema te ofrecía la información necesaria para buscar sobre el asunto y programar la solución.

Análisis del problema

El ataque que nos pedían implementar es posible debido a que el método de encriptación que se utiliza (Diffie-Hellman) es anónimo, es decir, no hace falta autenticarse con el receptor para iniciar la comunicación. Gracias a esto, podemos hacernos pasar por el cliente original (total, nadie nos preguntará quiénes somos). El protocolo de comunicación original entre cliente y servidor era el siguiente:

  1. Cliente inicia la encriptación de su parte y genera una clave pública y un número primo y los envía al servidor
  2. Servidor inicia la encriptación de su parte usando usando el primo y la clave pública del cliente, genera una clave secreta que conservará y envía su propia clave pública de vuelta
  3. Cliente recibe la clave pública del servidor y crea su propia clave secreta.
  4. A partir de ahora, ambas entidades envían sus mensajes utilizando sus respectivas claves secretas para encriptar y desencriptar

Lo que debíamos hacer nosotros es intervenir en el flujo anterior de la siguiente manera:

  • Al terminar el paso 1, interceptar las claves enviadas por el cliente y crear un par publico y secreto para hablar con él, como si nosotros fuésemos el servidor. A la vez, crear un nuevo par de claves que le enviamos al servidor, haciéndonos pasar por el cliente.
  • Al terminar el paso 2, recibir las claves enviadas por el servidor con nuestra información y enviar al cliente las que creamos en el paso anterior. En este punto tenemos un clave secreta para hablar con el cliente (la que el cliente original envió y cree que tiene el servidor) y una clave secreta para hablar con el servidor (creada por nosotros), quien cree que somos el cliente original pues no nos pide autenticación y hablamos “su idioma”
  • Al terminar el paso 4, cada comunicación del cliente será interceptada por nosotros, desencriptada con la clave secreta del cliente y reencriptada con la clave secreta del servidor. Lo mismo en la otra dirección. ¡Ya somos espías!
  • Solución enviada

    En mi solución, no utilicé la variable de estado que sí usan el cliente y el servidor. Lo que sí hice fue separar todos los mensajes en dos trozos de código claramente diferenciados: aquellos que van de cliente a servidor y los que van de servidor a cliente. Hubo participantes que no vieron esto necesario o que usaron la variable de estado. Todas las opciones son igualmente válidas para alcanzar la solución. Mi decisión se basó en que, para mi, era la forma más legible de organizar el código.

    Esta es la sección de mensajes de cliente a servidor. He resaltado la línea 11 pues es la clave que he generado para el cliente pero que le enviaré más adelante, cuando intercepte al servidor. También he resaltado la línea 23 pues utiliza una variable que se inicializa luego de interceptar la clave pública que nos envía el servidor. Finalmente, la línea 24 es donde le envío al servidor la KEYPHRASE del ataque, en vez de la que envía el cliente.

    if (data[0] == 'CLIENT->SERVER') { // Intercepting message from client.
    	data = data[1].split('|'); // From client code.
    	if (data[0] == 'hello?') {
    		// Attempt to connect.
    		socket.write(data[0]);
    	} else if (data[0] == 'key') {
    		// Intercept prime and public key and create the mitmServer.
    		mitmServer = crypto.createDiffieHellman(data[1], 'hex'); // From server code.
    		mitmServer.generateKeys();
    		mitmServerSecret = mitmServer.computeSecret(data[2], 'hex');
    		mitmServerPublic = mitmServer.getPublicKey('hex'); // This will be sent later to the client.
    		// Create mitmClient.
    		mitmClient = crypto.createDiffieHellman(256); // From client code.
    		mitmClient.generateKeys();
    
    		// Send mitmClient credentials to the server instead of the originals.
    		socket.write(util.format('key|%s|%s\n', mitmClient.getPrime('hex'), mitmClient.getPublicKey('hex')));
    	} else if (data[0] == 'keyphrase') {
    		// Intercept keyphrase and send KEYPHRASE instead.
    
    		// Cipher KEYPHRASE and send it to server using mitmClient.
    		// mitmClientSecret has been initialized with a message from the server.
    		var cipher = crypto.createCipheriv('aes-256-ecb', mitmClientSecret, '');
    		var keyphrase = cipher.update(KEYPHRASE, 'utf8', 'hex')+cipher.final('hex');
    		socket.write(util.format('keyphrase|%s\n', keyphrase));
    	}
    }

    Y ahora a interceptar los mensajes de servidor a cliente. Las líneas resaltadas son los puntos donde inicializamos la clave que se usa en la línea 23 y donde usamos la clave creada en la línea 11, respectivamente.

    else { // Intercepting message from server.
    	data = data[1].split('|'); // From server code.
    	if (data[0] == 'hello!') {
    		// Connection established.
    		socket.write(data[0]);
    	} else if (data[0] == 'key') {
    		// Create clientSecret using the server info
    		mitmClientSecret = mitmClient.computeSecret(data[1], 'hex'); // From client code.
    
    		// Pass mitmServerPublic to client instead
    		socket.write(util.format('key|%s\n', mitmServerPublic));
    	} else if (data[0] == 'result') {
    		// Decipher our message to complete the mission
    		var decipher = crypto.createDecipheriv('aes-256-ecb', mitmClientSecret, '');
    		var message = decipher.update(data[1], 'hex', 'utf8') + decipher.final('utf8');
    		console.log(message); // Mission completed!
    
    		// Cipher it with mitmServer to pass it to the client
    		var cipher = crypto.createCipheriv('aes-256-ecb', mitmServerSecret, '');
    		var result = cipher.update(message, 'utf8', 'hex')+cipher.final('hex');
    		socket.end(util.format('result|%s\n', result));
    	}
    }

    Aunque no decía nada al respecto el problema, decidí añadir las líneas 44-47 para completar la comunicación, enviándole al cliente el mensaje de nuestra KEYPHRASE. La mayoría de los participantes simplemente cerraron la conexión. No me gustaba esa opción pues del lado del cliente es como si hubiese fallado todo. La mía me disgustaba un poco menos porque el cliente no se da cuenta de que algo falla, aunque sí es cierto que recibe información falsa.

    Con un problema donde la solución eran tan específica, la diferencia entre los compañeros fue simplemente en cómo organizaron su código y cómo hicieron para leer de entrada estándar la KEYPHRASE. ¡Por cierto! en el submit a mi se me olvidó leer de entrada estándar la clave pues la había pegado directamente en el código como constante cuando pedí el test la primera vez para ver como venía la información. Mala mía. Estaba tan emocionado con mi primer código de hacker que no revisé eso antes de enviar.

    Como es costumbre, para terminar me gusta resaltar las soluciones que más me han gustado de los otros participantes y para este problema mis favoritas fueron:

    Y tú ¿cómo lo resolviste?

2 thoughts on “Resolviendo el #TuentiChallenge4: Challenge 6 – Man in the middle

  1. Yo también hice una “división de roles”, usando el código de la parte de servidor organizado de la misma forma que el original para hablar con el cliente y el del cliente para la parte que se encarga de hablar con el servidor; por otra parte, me guardaba “para la siguiente respuesta” el mensaje que recibiría la siguiente parte.
    En mi código también se puede comprobar que el cliente recibe “la respuesta” (falseada), ya que yo considero que eso es importantísimo de cara a este tipo de ataques: que nadie se entere excepto por la información.

    1. Hola Guillermo.
      He visto tu código. Sí que coincidimos bastante en cómo lo organizamos. ¿No te pasó que mientras revisabas tenías que estar moviéndote de una sección a otra? Por eso es que, luego de ver las otras soluciones, me ha gustado más la organización secuencial. Al final es simplemente una forma de escribir. Cada quien elige la que cree más cómoda.
      Lo que no encontré en tu código es la parte en la que el cliente recibe la respuesta falseada. En tu “if” del resultado del servidor escribes en consola el mensaje desencriptado y luego cierras el socket en vez de escribirle al cliente. ¿A qué te refieres entonces con que recibe la info falseada? Puede que yo esté entendiendo mal la cosa.