Bestiario de una memoria mal gestionada (I)David García 14 abril, 2020 Si tuviéramos que elegir una vulnerabilidad especialmente dañina sería, con mucha probabilidad, la ejecución de código arbitrario, y más aún si puede ser explotada en remoto. Las consecuencias pueden ser fatales, como ya hemos visto en muchos episodios de este tipo (Conficker para los analistas de malware, MS08-067 y EternalBlue para los pentesters, WannaCry para todo el mundo, etc.). La ejecución de código arbitrario ha sido y sigue siendo uno de los errores de programación que más pérdidas y reparaciones ha causado a lo largo y ancho de la historia del silicio. Por cierto, lo llamamos arbitrario porque, en realidad, la CPU ya está ejecutando código; la gracia de lo arbitrario es que se deja al arbitrio del atacante decidir qué código se ejecuta, puesto que es quien toma el control del proceso. De eso trata una explotación de este tipo: desviar la ejecución normal y determinada de un proceso a un agente extraño introducido en aquel de forma arbitraria por un atacante a través de un exploit. ¿Cómo sucede esto exactamente? Existen muchísimas formas de ejecutar código (a partir de aquí entenderemos arbitrario). La definición, por cierto, no está circunscrita a los ejecutables nativos. Un cross-site scripting no deja de ser una inyección de código extraño que, de nuevo, desvía la ejecución de un script al fragmento de código inyectado. Uno de los factores en la ejecución de código a nivel nativo es la derivada de los fallos en la gestión de memoria. Vamos a ver los tipos de errores más comunes, centrándonos en cómo ocurren y en cómo están evolucionando los sistemas operativos y lenguajes de programación para paliar el efecto que estos fallos suponen cuando son explotados maliciosamente. Remontándonos atrás en el tiempo, no todos los lenguajes poseían una gestión manual del uso que hacían de la memoria. De hecho, John McCarthy, uno de los padres de la Inteligencia Artificial y creador de LISP, inventó el concepto de recolección automática de basura (memoria liberada a lo largo de la ejecución de un proceso) en la década de los sesenta. No obstante, a pesar de que los recolectores de basura hacían más fácil la vida a los programadores (abstrayéndolos de la gestión manual), era una sobrecarga en el consumo de recursos que algunos sistemas no podían permitirse. Para hacernos una idea sería como si el seguimiento de los vuelos de una torre de control de un aeropuerto en tiempo real se detuviese unos segundos a eliminar la memoria liberada. Es por ello por lo que lenguajes como C o C++ mantienen un peso enorme a la hora de programar aplicaciones de sistemas. Son lenguajes sin recolector de basura (aunque es posible hacer uso de ellos a través de librerías) en los que el peso de la gestión de memoria cae íntegramente en el programador. Y claro, cuando dejas el trabajo de una máquina en manos de un humano… Por el contrario, liberar los recursos que consume un recolector supone un incremento enorme en el rendimiento y respuesta del programa y eso se traduce en un menor coste en hardware. ¿Es tan difícil gestionar la memoria de forma manual? Evidentemente, es una pregunta muy abierta y la respuesta dependerá de nuestro nivel de familiaridad con este tipo de programación y de las propias facilidades que el lenguaje nos dé, sumadas al empleo de herramientas externas y tecnología implementada en el compilador. Vamos a poner un ejemplo: supongamos que deseamos asociar una cadena de texto a una variable. Una operación que es trivial en lenguajes con gestión automática de la memoria, por ejemplo, en Python (es código de ejemplo, no vamos a molestarnos en su corrección): def asociar_cadena(cadena): mi_cadena = input() # … # procesamos mi_cadena # … return mi_cadena Bien, pues esto en lenguaje C posee unos interesantes añadidos. En primer lugar, no sabemos la longitud de la cadena. Esa cantidad no viene “de serie” con la cadena, debemos encontrarla o añadirla como parámetro a la función. Segundo, dado que no disponemos de su longitud, tampoco sabemos que memoria vamos a necesitar para guardarla y, tercero: ¿quién se hace cargo de avisar cuando ya no necesitemos esa memoria? Veamos un fragmento de código (existen múltiples formas de implementar esto, más seguras y mejores, pero esta nos servirá para ilustrar lo que queremos decir, por ejemplo, usando strdup, “%ms”, etc.): Como vemos, ni tan siquiera hemos comenzado a manipular la cadena cuando ya hemos de escribir código para detectar el fin de una cadena, reservar memoria, vigilar los límites del array en la pila, etc. Sin embargo, lo importante es fijarnos en la línea 28, esa función «free», usada para indicarle al sistema que libere el trozo de memoria que habíamos reservado en la función «leer». Aquí la situación es clara: ya no usamos esa memoria y la devolvemos. En un ejemplo de código es fácil hacer uso de la memoria pero, ¿y si seguimos haciendo uso de esa memoria reservada 200 líneas de código después? ¿Y si tenemos que pasar ese puntero por varias funciones? ¿Cómo queda claro quien se hace cargo de la memoria, la función llamada o quien llama a esa función? En las sucesivas entradas veremos ciertos escenarios que se transforman en vulnerabilidades por este tipo de descuidos: double free, uso de memoria no inicializada, memory leaks o fuga de memoria y dangling pointers o punteros descolgados (o colgantes). No te pierdas la serie completa de este artículo: Bestiario de una memoria mal gestionada (II) Bestiario de una memoria mal gestionada (III) Bestiario de una memoria mal gestionada (IV) DataCOVID-19: luchando contra el coronavirus con los datos de posición aproximada de tu móvilElevenPaths y Chronicle se unen para crear nuevos servicios avanzados de seguridad gestionada
Telefónica Tech Boletín semanal de Ciberseguridad, 18 – 24 de marzo HinataBot: nueva botnet dedicada a ataques de DDoS El equipo de investigadores de Akamai ha publicado un informe en el que señala que han identificado una nueva botnet denominada HinataBot que dispondría...
Telefónica Tech Qué es el Esquema Nacional de Seguridad (ENS 2.0) La Ciberseguridad, la privacidad y la protección de los datos y de la información sensible son aspectos cada vez más importantes en la sociedad actual. Tanto para empresas y...
Nacho Palou 5G: cuatro casos de uso reales y prácticos El último informe “La Sociedad Digital en España 2022” [1] de Fundación Telefónica confirma la consolidación de los procesos de digitalización en la sociedad española. En este sentido, cabe...
Susana Alwasity Ciberseguridad: eventos “cisne negro” en un mundo conectado En la sociedad actual, la tecnología ha transformado la forma en que vivimos, trabajamos y nos relacionamos. Con el aumento del uso de dispositivos y redes conectados a internet,...
Telefónica Tech Boletín semanal de Ciberseguridad, 11 – 17 de marzo Nueva versión del troyano bancario Xenomorph Investigadores de ThreatFabric han detectado una nueva variante del troyano bancario para Android Xenomorph. Esta familia de malware fue detectada por primera vez en febrero...
Gonzalo Álvarez Marañón Matemáticas contra el cibercrimen: cómo detectar fraude, manipulaciones y ataques aplicando la Ley de Benford Cómo aplicar la ley de Benford para luchar contra el cibercrimen. La respuesta, en este post que utiliza las matemáticas para ayudar a la ciberseguridad.