Problemas comunes en la gestión de memoria usando C

Vamos a ver en esta entrada los errores comunes con los que podemos encontrarnos en la gestión de memoria durante el desarrollo de aplicaciones usando el lenguaje C. Es frecuente utilizar el termino bug para denominar a este tipo de fallos que pueden permanecer ocultos en código durante años pero latentes y con la capacidad de provocar en cualquier momento un comportamiento errático que puede derivar en una explotación del sistema.

Hay que tener claro que lo que se denomina bug, no es otra cosa que defectos en el código. Por otro lado, con frecuencia se utiliza el termino "unsafe language" (lenguajes no seguros) cuando se hace referencia a los lenguajes C y C++, ya que estos no ofrecen ayudas en la gestión de la memoria. Trato de evitar este término ya puede llevar a la conclusión de que el lenguaje es intrínsecamente no seguro, lo que es un error. Lo que ocurre con estos lenguajes es que recae sobre el programador toda la gestión de la memoria del programa y en caso de fallo en la codificación de la aplicación, esta fallará. Como vamos a ver en una serie de artículos cometer fallos en la gestión de memoria es muy sencillo y este tipo de errores tienen una repercusión grave en la aplicación y en la seguridad del sistema.

Muchos de los errores, y de las explotaciones de estos errores, se basan en la implementación, digamos bienintencionada, del ejecutable y del sistema donde corre. El Sistema Operativo delega en la aplicación la gestión de si misma en caso de error. Los lenguajes de programación que no ofrecen ayuda en la gestión de la memoria, delegan en el programador el uso correcto de ésta.

Estos sistemas al igual que los protocolos de red primarios, se desarrollaron sin tener en cuenta su posible explotación maliciosa. De esta forma, explotar un error en un protocolo originalmente era trivial, al igual que un fallo de memoria era una forma directa de poder ejecutar cualquier código que se inyectara en la aplicación. Por supuesto se han añadido muchos tipos de protecciones en el sistema y en los ejecutables como el bit NX, ASLR, RELRO, PIE, Stack Protector o Fortify Source que hemos visto en el blog. Esto hace que explotar un error sea más difícil, pero no reduce la posibilidad de cometer los errores en la gestión de memoria en el código. 

Los problemas derivados del incorrecto uso de la gestión de memoria forman una de las principales causas de errores graves en las aplicaciones. Empresas de la talla de Google, en su equipo de desarrollo de Chromium tienen una regla para limitar el uso de lenguajes donde recaiga en los programadores la gestión de la memoria, en determinadas zonas del código. Esta regla, denominada la Regla del 2, crea 3 grupos:

  • Código que procesa entras no confiables
  • Código que corre sin sandbox (sandbox es un entorno controlado que limita la exposición y regula el acceso a los recursos)
  • Código escrito en lenguajes sin protección en la gestión de memoria (C y C++)

Y evitan a toda costa que un código pertenezca a los tres grupos.

Regla del 2

 

Pero, ¿que es lo que ocurre para que esta zona sea tan peligrosa?

Vamos a ver cuales son estos errores en la gestión de memoria, como se producen y que herramientas tenemos para la detección temprana de éstos.

Comportamiento indefinido

Las distintas implementaciones de los compiladores de C para los múltiples entornos para los que existen compiladores del lenguaje, tienen diferencias. Los compiladores del lenguaje evolucionan y se adaptan a los diferentes estándares, pudiendo o no dar soporte a un estándar concreto, por ejemplo C99, C11, C17, C2x. Como resultado estas diferencias generan problemas de portabilidad entre las diferentes implementaciones en diferentes sistemas e incluso entre diferentes versiones. El anexo J (Problemas de Portabilidad) del International Standard sobre el lenguaje C divide los problemas de portabilidad que se encuentran en el lenguaje en los siguientes apartados:

  • Comportamiento definido por la implementación
  • Comportamiento no especificado
  • Comportamiento indefinido
  • Comportamiento específico de la configuración regional y extensiones comunes
  • Extensiones comunes

El Comportamiento definido por la implementación es un comportamiento de una aplicación que no esta especificado por el Estándar de C, pero esta definido en una implementación concreta. Esto generará diferencias entre implementaciones que hay que tener en cuenta en la portabilidad.

En el Comportamiento no especificado implica que el Estándar C ofrece dos o más posibles opciones de implementación. Por tanto la ejecución de una expresión puede tener diferentes resultados.

El Comportamiento indefinido es un comportamiento no definido en el Estándar, de tal forma que en la construcción de un programa erróneo o no portátil, o en el uso de datos erróneos, el Estándar no impone ningún requerimiento. Lo que es peor aún, muchos de estos errores no muestran ningún efecto secundario directamente perceptible, aunque el proceso es inestable y tarde o temprano colapsará.

Tipos de Errores

Los tipos de error que tenemos en la gestión de memoria en C se muestran a continuación. Aunque incluyo un punto titulado "Comportamiento Indefinido" en realidad todos los problemas relatados en la lista van a desencadenar en lo que se conoce como comportamiento indefinido. En la lista incluyo algunas referencias que teniendo en cuenta: los códigos CWE (Common Weakness Enumeration), las reglas del CERT Coding Standards referentes, y las reglas y directrices del MISRA C: Guidelines for the use of the C language in critical systems.:

  • Acceso incorrecto a la memoria
    • Uso de variables no inicializadas
      • CWE
        • CWE-457: Use of Uninitialized Variable
        • CWE-824: Access of Uninitialized Pointer
      • CERT
        • EXP33-C. Do not read uninitialized memory
      • MISRA C
        • Rule 9.1: The value of an object with automatic storage duration shall not be read before it has been set
        • Rule 9.3: Arrays shall not be partially initialized
        • Rule 9.4: An element of an object shall not be initialized more than one
        • Rule 9.5: Where designated initializers are used to initialize an array object the size of the array shall be specified explicitly
    • Acceso a memoria fuera de los límites: errores de lectura/escritura overflow/underflow
      • CWE
        • CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer)
        • CWE-787: Out-of-bounds Write
        • CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')
        • CWE-124: Buffer Underwrite ('Buffer Underflow')
        • CWE-121: Stack-based Buffer Overflow
        • CWE-122: Heap-based Buffer Overflow
        • CWE-123: Write-what-where Condition
        • CWE-125: Out-of-bounds Read
        • CWE-126: Buffer Over-read
        • CWE-127: Buffer Under-read)
      • CERT
        • INT32-C. Ensure that operations on signed integers do not result in
          overflow
        • ARR30-C. Do not form or use out-of-bounds pointers or array
          subscripts
        • STR31-C. Guarantee that storage for strings has sufficient space
          for character data and the null terminator
        • MEM35-C. Allocate sufficient memory for an object
      • MISRA C
        • Dir 4.11: The validity of values passed to library functions shall be checked
        • Dir 4.12: Dynamic memory allocation shall not be used
        • Rule 21.17 : Use of the string handling funcions from <string.h> shall not result in accesses beyond the bounds of the objects referenced by their pointer parameters
        • Rule 21.18: The size_t argument passed to any funtion in  <string.h> shall have an appropiate value
    • Uso de memoria fuera de alcance: después-de-liberar/después-de-retornar
      • CWE
        • CWE-416: Use After Free
      • CERT
        • DCL30-C. Declare objects with appropriate storage durations
        • MEM34-C. Only free memory allocated dynamically
        • MEM30-C. Do not access freed memory
    • Doble liberación 
      • CWE
        • CWE-415: Double Free
      • CERT
        • MEM30-C. Do not access freed memory
      • MISRA C
        • Rule 22.2: A block of memory shall only be freed if was allocated by means of a Standard Libray function
  • Fugas de memoria
    • CWE
      • CWE-401: Missing Release of Memory after Effective Lifetime
    • CERT
      • MEM31-C. Free dynamically allocated memory when no longer
        needed
    • MISRA C
      • Rule 22.1: All resources obtained dynamically by means of Standard Libray functions shall be explicitly released
  • Comportamiento indefinido
    • CWE
      • CWE-758: Reliance on Undefined, Unspecified, or Implementation-Defined Behavior
    • CERT
      • EXP34-C. Do not dereference null pointers
      • EXP35-C. Do not modify objects with temporary lifetime
      • EXP43-C. Avoid undefined behavior when using restrict-qualified
        pointers
    • MISRA C
      • Appendix H: Undefined and critical unspecified behaviour

En Resumen

Los problemas en la gestión de la memoria son una certeza y el simple hecho de programar en lenguajes como C y C++ que delegan en el usuario la responsabilidad de una buena gestión de la memoria nos exponen a errores que van a desencadenar, lo que en la jerga del compilador se conoce como un “comportamiento indefinido”.

Tenemos una exposición del problema y una perspectiva desde donde enfocarlo. Ahora nos toca ir viendo los diferentes errores en detalle con algún ejemplo y probar algunos de los sistemas que se usan para detectarlos de forma temprana.

Por ahora esto es todo. Espero que pueda resultar de utilidad, les invito a que dejen sus comentarios, compartan en sus redes libres y les emplazo hasta la siguiente entrada.


 

 

Modificado por última vez enLunes, 03 Mayo 2021 09:50
(0 votos)
Etiquetado como :

Deja un comentario

Asegúrese de introducir toda la información requerida, indicada por un asterisco (*). No se permite código HTML.