Problemas de comunicacion entre lenguajes

Los lenguajes de alto nivel como Javascript, Python y Ruby se caracterizan principalmente por facilitar el desarrollo de software de una manera sencilla e intuitiva y por presentar el nivel de abstracción más alto con respecto a la máquina en la que se ejecutan, previniendo que los desarrolladores introduzcan errores de software de bajo nivel, como los errores de corrupción de memoria, que se producen en lenguajes de bajo nivel como C y C++.

Sin embargo, los lenguajes de alto nivel dependen de sus sistemas de tiempo de ejecución para tener acceso a funcionalidad, como por ejemplo una interfaz con el procesador o acceso al sistema de ficheros o a funciones de red, que de otra forma no sería posible. Uno de los ejemplos más populares es Node.js para ejecutar Javascript en el lado servidor. Dichos sistemas de tiempo de ejecución están normalmente escritos en C/C++, lenguajes conocidos por no garantizar la seguridad de la memoria ni la seguridad de tipos, lo que quiere decir que son propensos a errores y un objetivo para los atacantes.

Para interactuar entre dos lenguajes de distinto nivel como Javascript y C++ que tienen distintas estrategias de gestión de la memoria, sistema de tipos, etc., es necesaria una capa intermedia encargada de transformar los tipos y la representación de los valores de forma adecuada para el lenguaje destino, además de propagar los errores producidos. Los problemas aparecen debido a que, en las funciones de la capa intermedia, se deben hacer comprobaciones tales como verificar el tipo de los parámetros, comprobar que los valores son «legales» (como que un índice se encuentre dentro de los límites de un array), que no se hacen en todos los casos, lo que conlleva que se produzcan errores que atentan contra la seguridad de la memoria y la seguridad de tipos.

Tomando como ejemplo la imagen anterior, podemos ver como en la capa superior se han introducido comprobaciones para asegurar que los valores de start y end se encuentran dentro de los límites del buffer antes de llamar a la implementación de fill en la capa intermedia. Por otra parte, el código C++ presupone que los valores de start y end recibidos por parámetros son enteros de 32 bits sin signo.

Esta situación se puede aprovechar pasando un objeto, en lugar de un número, como valor start que ejecute la función Symbol.toPrimitive definida cuando se intente obtener su valor númerico. De esta forma, la comprobación en la capa superior se hará sin ningún problema devolviendo un 0, pero al obtener el valor en la capa intermedia se devolverá un número negativo, dando lugar a un overflow

Oscar Llorente
Acerca de
Investigador de DT
Expertise: Scam, program analysis