Seminario web destacado: Presentación de la prueba CT de Parasoft C/C++ para pruebas continuas y excelencia en el cumplimiento | Vea ahora

Detectar daños en la memoria en C y C ++

Foto de cabeza de Ricardo Camacho, Director de Cumplimiento de Seguridad y Seguridad
22 de noviembre.
7 min leer

Consulte esta breve explicación de por qué la corrupción de la memoria en C y C++ es tan difícil de detectar mediante análisis de código e instrucciones para usar una herramienta de detección de fallas de memoria que le ahorrará largas horas de sesiones de depuración.

Los programadores continúan usando los lenguajes de programación C y C++ porque pueden interactuar fácilmente con la memoria, trabajar estrechamente con el hardware y ofrecer la potencia, el rendimiento y la eficiencia necesarios en el desarrollo integrado. Sin embargo, estos lenguajes son propensos a tener problemas sutiles de memoria, como pérdidas de memoria, desbordamiento del búfer, desbordamiento numérico y más.

Desafortunadamente, es muy común que estos errores permanezcan ocultos durante las pruebas normales. El software con problemas sutiles, como corrupción de memoria, puede ejecutarse sin problemas en una máquina pero fallar en otra o puede funcionar bien durante un cierto período de tiempo solo para fallar inesperadamente cuando el sistema ha estado activo durante un largo número de días.

Este tipo de corrupción de memoria, así como otros errores comunes como problemas de manipulación de cadenas, inicialización incorrecta y errores de puntero, provocan fallos en la producción. Con el aumento actual del software integrado en aviones, automóviles, dispositivos médicos y el creciente mercado de IoT, las consecuencias del software defectuoso se han convertido en algo más que clientes insatisfechos. Pueden poner en peligro la vida.

¿Qué es la corrupción de la memoria?

Los errores de corrupción de memoria son desagradables, especialmente si están bien disfrazados. Cuando se manifiestan, pueden ser engañosamente difíciles de reproducir y localizar. Como ejemplo de lo que puede suceder, considere el programa que se muestra a continuación.

Este programa concatena los argumentos dados en la línea de comando e imprime la cadena resultante:

/*
 * File: hello.c
 */
#include <string.h>
#include <string.h>
int main(argc, argv)
    int argc;
    char *argv[];
{
    int i;
    char str[16];
    str[0] = '\0';
    for(i=0; i<; argc; i++) {
        strcat(str, argv[i]);
        if(i < (argc-1)) strcat(str, “ “);
    }
    printf("You entered: %s\n", str);
    return (0);
}

Si compila y ejecuta este programa con su compilador normal, probablemente no verá nada interesante. Por ejemplo:

c:\source> cc -o hello hello.c 
    c:\source> cc -o hello  
    You entered: hello
    c:\source> hello world
    You entered: hello world
    c:\source> hello cruel world
    You entered: hello cruel world

Si este fuera el alcance de sus procedimientos de prueba, probablemente concluiría que este programa funciona correctamente, a pesar de que tiene un error de corrupción de memoria muy grave, simplemente no se manifestó produciendo una salida incorrecta. Esto es común con los problemas de memoria: a menudo pueden pasar desapercibidos porque es posible que no afecten la salida directamente y, por lo tanto, no serán detectados por pruebas unitarias normales o pruebas funcionales.

Este tipo de error parece bastante simple cuando se encuentra en un pequeño programa de ejemplo donde no se pasará por alto, pero cuando se oculta dentro de un código complicado con cientos de miles de líneas y mucha asignación dinámica, puede evitar fácilmente la detección hasta después del lanzamiento.

Tipos de corrupción de la memoria en C y C++

La corrupción de la memoria es un problema crítico en la programación, particularmente en C y C++, ya que estos lenguajes brindan acceso directo a la administración de la memoria, lo que aumenta la posibilidad de errores. Como señalamos anteriormente, la corrupción de la memoria ocurre cuando un programa accede o modifica la memoria de manera no deseada, lo que provoca corrupción de datos, fallas o vulnerabilidades de seguridad.

Comprender los distintos tipos de corrupción de la memoria es crucial para detectando problemas de memoria y también allana el camino para mitigarlos. Las siguientes secciones brindan información sobre cuatro tipos comunes de corrupción de la memoria.

1. Desbordamientos del búfer

Los desbordamientos del búfer ocurren cuando un programa intenta escribir más datos en un búfer de los que fue diseñado para contener. Esto puede provocar que los datos se sobrescriban en ubicaciones de memoria adyacentes, lo que podría dañar otros datos o provocar que el programa falle. Los desbordamientos de búfer son una fuente común de vulnerabilidades de seguridad, ya que pueden explotarse para ejecutar código arbitrario.
Hay muchas formas en que pueden ocurrir desbordamientos del búfer. Por ejemplo, si un programa copia una cadena en un búfer sin verificar la longitud de la cadena, puede sobrescribir la memoria más allá del final del búfer. Además, si un programa utiliza un índice de matriz que está fuera de límites, puede acceder a memoria que no le pertenece y dañar los datos.

Para evitar desbordamientos del búfer, los programadores siempre deben verificar el tamaño del búfer de destino antes de copiar datos en él. También deben utilizar prácticas de programación seguras, como el uso de las funciones strcpy_s y strncpy_s en C o la clase std::string en C++, que proporciona verificación de límites. Los estándares de codificación como MISRA C/C++ identifican el uso de funciones inseguras del sistema y brindan alternativas para remediar estas vulnerabilidades identificadas.

2. Uso después de la liberación

Los errores de uso después de la liberación surgen cuando un programa intenta acceder o modificar la memoria que ya ha sido liberada. Esto puede suceder si un puntero a la memoria liberada no se invalida adecuadamente o si el puntero se pasa a otra parte del programa que no sabe que la memoria ya no es válida. Este tipo de error de corrupción de memoria puede provocar un comportamiento impredecible, ya que la memoria liberada puede reasignarse para un propósito diferente. Las causas comunes incluyen no establecer punteros en NULL después de liberar la memoria asociada y usar incorrectamente un puntero después de pasarlo a una función que libera la memoria asociada.

Los errores de uso después de la liberación pueden ser difíciles de detectar y depurar, ya que puede parecer que el programa funciona correctamente hasta que se reutiliza la memoria liberada. Esto puede provocar comportamientos impredecibles, como fallos o corrupción de datos.

Los desarrolladores pueden mitigar este tipo de corrupción de la memoria liberando memoria cuando ya no sea necesaria. También deben utilizar técnicas adecuadas de gestión de punteros, como establecer punteros en NULL después de liberar la memoria a la que apuntan.

3. Doble gratis

Los errores de doble liberación, también conocidos como errores de doble eliminación, ocurren cuando un programa intenta liberar el mismo bloque de memoria dos veces. Esto puede suceder si el programa gestiona varios punteros al mismo bloque de memoria y lo libera varias veces, o si pasa el mismo puntero a la función libre varias veces.

Los errores de doble liberación son problemas graves de corrupción de la memoria que pueden provocar un comportamiento impredecible del programa, fallas y vulnerabilidades de seguridad si no se resuelven. Para evitar errores de doble liberación, los programadores deben mantener registros adecuados de los bloques de memoria asignados y asegurarse de que se desasignen solo una vez. Antes de liberar un bloque de memoria, deben validar el puntero para comprobar si ya ha sido liberado, evitando intentar liberar la misma memoria dos veces.

4. Fugas de memoria

Las pérdidas de memoria ocurren cuando un programa asigna memoria pero no logra desasignarla o liberarla cuando ya no es necesaria. Este tipo de corrupción de la memoria puede provocar un agotamiento gradual de la memoria disponible, lo que provoca problemas de rendimiento. Olvidarse de liberar la memoria asignada dinámicamente y perder todas las referencias a la memoria asignada sin liberarla son causas comunes de pérdidas de memoria.

Al igual que los otros tipos de problemas de corrupción de memoria, también pueden ser difíciles de detectar y depurar, ya que el programa puede parecer que funciona correctamente durante algún tiempo antes de quedarse sin memoria.
Para evitar pérdidas de memoria, los programadores siempre deben liberar memoria explícitamente cuando ya no sea necesaria. También deberían utilizar un depurador de memoria automatizado para C y C++, como Parasoft Insure ++.

Causas comunes y consecuencias de la corrupción en la memoria C++

La corrupción de la memoria en C++ puede deberse a diversas causas, que van desde errores de programación hasta prácticas inseguras, y sus consecuencias pueden afectar negativamente el comportamiento del programa, la seguridad y la integridad de los datos. Considere asignar memoria al inicio de la aplicación y administrar ese bloque de memoria con funciones nuevas y gratuitas sobrecargadas para controlar y solucionar problemas de corrupción de memoria.

Las siguientes secciones proporcionan una descripción general de algunas causas y consecuencias comunes de la corrupción de la memoria en C++.

1. Comportamiento indefinido

El comportamiento indefinido es una de las causas notables de corrupción de la memoria en C y C++. Ocurre cuando el programa ejecuta código que no se ajusta a las especificaciones del lenguaje. En el contexto de la memoria, acceder a la memoria no inicializada, leer/escribir más allá de los límites de la matriz y desreferenciar punteros nulos o colgantes pueden conducir a un comportamiento indefinido. Las consecuencias del comportamiento indefinido son impredecibles, lo que lo convierte en una preocupación fundamental para los desarrolladores.

2. Vulnerabilidades de seguridad

La corrupción de la memoria representa una grave amenaza para la seguridad de los programas C y C++. La explotación de las vulnerabilidades de la memoria es una técnica común que utilizan los atacantes para comprometer los sistemas. Los desbordamientos de búfer, el uso después de la liberación y otros problemas relacionados con la memoria pueden aprovecharse para ejecutar código arbitrario, inyectar cargas útiles maliciosas o manipular el comportamiento del programa. Comprender y mitigar estas vulnerabilidades es esencial para desarrollar software seguro.

3. Fallos e inestabilidad del programa

La corrupción de la memoria a menudo se manifiesta en fallas e inestabilidad del programa. Cuando se accede o se manipula una memoria corrupta, se puede producir un comportamiento inesperado y provocar que el programa falle. Identificar la causa raíz de tales fallas puede ser un desafío y requiere una depuración y un análisis exhaustivos. Las prácticas y herramientas adecuadas de gestión de la memoria pueden ayudar a prevenir estos problemas y mejorar la estabilidad del programa.

4. Corrupción de datos

La corrupción de la memoria puede resultar en la corrupción de estructuras de datos y variables críticas dentro de un programa. Esto puede provocar cálculos incorrectos, pérdida de datos o comportamiento no deseado. Por ejemplo, los desbordamientos del búfer pueden sobrescribir estructuras de control importantes, lo que provoca corrupción de datos. Prevenir la corrupción de datos implica una gestión cuidadosa de la memoria, una verificación adecuada de los límites y el cumplimiento de prácticas de codificación segura.

¿Cómo detectar errores de memoria?

La mejor manera de abordar la búsqueda de defectos de memoria complejos es utilizar un herramienta de detección de errores de memoria o “depurador en tiempo de ejecución”. Es fácil de usar. Simplemente reemplace el nombre de su compilador (cc) con "asegurar" como se muestra a continuación.

cc -o hello hello.c

se convierte en

insure -o hello hello.c

A continuación, ejecute el programa. Si tiene un archivo MAKE bien formateado, puede usar Parasoft Insure++ configurando el comando del compilador para asegurar:

make CC=insure hello

Una vez que haya compilado con el depurador en tiempo de ejecución, puede ejecutar el comando:

hello cruel world

Generará los errores que se muestran a continuación porque la cadena que se concatena supera los 16 caracteres asignados en la declaración en la línea 11:

[hello.c:14] **WRITE_OVERFLOW**
&gt;&gt;         strcat(str, argv[i]);
  Writing overflows memory: &lt;argument 1&gt;
          bbbbbbbbbbbbbbbbbbbbbbbbbb
          |           16           | 2 |
          wwwwwwwwwwwwwwwwwwwwwwwwwwwwww
   Writing  (w) : 0xbfffeed0 thru 0xbfffeee1 (18 bytes)
   To block (b) : 0xbfffeed0 thru 0xbfffeedf (16 bytes)
                 str, declared at hello.c, 11
  Stack trace where the error occurred:
                          strcat()  (interface)
                            main()  hello.c, 14
**Memory corrupted.  Program may crash!!**
[hello.c:17] **READ_OVERFLOW**
&gt;&gt;     printf("You entered: %s\n", str);
  String is not null terminated within range: str
  Reading   : 0xbfffeed0
  From block: 0xbfffeed0 thru 0xbfffeedf (16 bytes)
             str, declared at hello.c, 11
  Stack trace where the error occurred:
                            main()  hello.c, 17
You entered: hello cruel world    

Probablemente hayas notado algo interesante en el resultado, es decir, que hay dos errores derivados de este problema:

  1. La escritura se desborda cuando intentas poner demasiados bytes en el búfer de cadena.
  2. Un desbordamiento de lectura cuando lees desde el búfer de cadena.

Como puedes ver, el error puede manifestarse de diferentes maneras en diferentes lugares, así que imagina lo que puede suceder dentro de un programa real. Es casi evidente que todos los programas C y C++ que funcionan tienen pérdidas de memoria y otros errores de memoria.

Si desea encontrar estos errores sin pasar semanas persiguiendo problemas oscuros, eche un vistazo a Parasoft Insure ++. Puede encontrar todos los problemas relacionados con la sobrescritura de memoria o la lectura más allá de los límites legales de un objeto, independientemente de si está asignado estáticamente, es decir, una variable global, localmente en la pila, dinámicamente con malloc o new, o incluso como memoria compartida. bloquear. Incluso puede detectar situaciones en las que un puntero cruza de un bloque de memoria a otro y comienza a sobrescribir la memoria allí, incluso si los bloques de memoria son adyacentes. La detección de errores en tiempo de ejecución con Insure++ reforzará su aplicación y le evitará esas sesiones de depuración que duran toda la noche.

Consulte el depurador de memoria definitivo para C y C++.

“MISRA”, “MISRA C” y el logotipo del triángulo son marcas comerciales registradas de The MISRA Consortium Limited. © The MISRA Consortium Limited, 2021. Todos los derechos reservados.