Bestiario de una memoria mal gestionada (IV)

David García    18 mayo, 2020
Bestiario de una memoria mal gestionada (IV)

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. En la primera entrada introdujimos los problemas que puede acarrear una memoria mal gestionada, en la segunda hablamos del double free y en la tercera abordamos los punteros colgantes y las fugas de memoria. Terminamos esta serie con el uso de memoria no inicializada y las conclusiones.

Uso de memoria no inicializada

Por eficiencia, cuando llamamos a “malloc” o utilizamos el operador “new” en C++, la zona de memoria que nos asignan no está inicializada. ¿Qué quiere decir esto? Que no contiene un valor por defecto, sino datos que nos parecerán aleatorios y que no tienen sentido en el contexto de nuestro proceso. Veamos:

Obtenemos un fragmento de 10.000 enteros, lo llenamos de enteros aleatorios y lo liberamos. En teoría, según el estándar de la librería C, la memoria que proviene de ‘malloc’ no debería estar inicializada, pero en determinados sistemas (sobre todo modernos) es probable que venga inicializada a cero. Esto es, toda la zona reservada está llena de ceros.

En el programa, hacemos uso de una zona de memoria reservada y, a continuación, la liberamos. Pero cuando volvemos a hacer uso de este tipo de memoria, el sistema nos devuelve ese mismo fragmento con el contenido que ya poseía. Este contenido probablemente no tenga sentido en el contexto de ejecución actual.

Observemos la salida:

¿Qué ocurriría si utilizamos esos datos de forma accidental? Vamos a verlo, de nuevo, con código. Modificamos el programa para que la segunda reserva se haga para una estructura que hemos definido:

Como vemos, hacemos uso en ‘p’ llenando esa zona de datos aleatorios. Liberamos ese fragmento y ahora reclamamos uno para una estructura que debería contener un puntero a una cadena y dos valores enteros. Veamos una serie de ejecuciones:

Como vemos, la estructura se inicializa con “basura” y hacer uso de esta “basura” es problemático, cuando no preocupante, y completamente inseguro. Imaginad que esos valores son usados para una aplicación crítica.

Además de los ya mencionados, los problemas de gestión manual de memoria no acaban aquí. Lo que hemos visto ha sido sólo una pequeña muestra y la lista sería interminable: aritmética de punteros, escritura fuera de límites, etc.

Los nuevos mecanismos de gestión

C++ ha mejorado mucho la gestión manual de forma que si ya en los primeros pasos del lenguaje se eliminó la necesidad de usar funciones a través de operadores (“new” y “delete”), el nuevo estándar amplía y mejora la gestión de memoria a través de punteros “inteligentes”. Es decir, contenedores de memoria que invocan su propio destructor cuando detectan que ya no son útiles. Por ejemplo, cuando un objeto sale de un ámbito y ya no es referenciado por ninguna otra variable u objeto.

Aun así, incluso con punteros inteligentes, queda espacio para la sorpresa e incluso para casos en los que tenemos que hacer uso del método tradicional, ya sea por eficiencia o por limitación en las librerías usadas por una aplicación.

Otro método de gestión de memoria que no precisa de recolector es el sistema usado por lenguajes como Swift o Rust. El primero usa un tipo de recolector de memoria “inmediato” que no precisa de pausas, ARC o Automatic Reference Counting. Se trata de un método que se apoya en el compilador para insertar en el código las instrucciones adecuadas para liberar memoria cuando ésta ya no va a ser usada. Rust, un lenguaje relativamente moderno, utiliza un método basado en los conceptos de “préstamos” y “propiedad” de los objetos creados con memoria dinámica. Un compromiso intermedio entre no tener que llevar la mochila de un recolector de memoria y la molestia de que el programador se deba preocupar mínimamente en la lógica de “prestar” un objeto a otros métodos.

Conclusiones

Está claro que el manejo de la memoria de forma manual causa un tremendo vórtice de problemas que pueden (y normalmente lo hacen) derivar en serios problemas de seguridad. Por otro lado, se requiere una buena capacidad, atención y experiencia en los programadores que usan lenguajes como C o C++. Esto no quiere decir que se deba abandonar este tipo de lenguajes por resultar complejo su uso en ciertos aspectos. Como ya dijimos al principio, no se puede evitar su uso en cierto tipo de aplicaciones.


No te pierdas las anteriores entradas de este post:

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *