Saltar al contenido

Vulnerabilidades de desbordamiento de enteros. Seguridad en el Software

Compartir en:

Imagina un mundo donde los números, como actores en un escenario matemático, de repente olvidan sus límites y rompen el guion. Así son las vulnerabilidades de desbordamiento de enteros en el mundo del software: un pequeño error en la gestión de estos ‘actores numéricos’ puede desencadenar un caos en el sistema, como una escena de una obra que se sale de control. Estos errores, a menudo subestimados, pueden abrir puertas traseras a ciberataques, poniendo en jaque la seguridad de toda una infraestructura.

En este artículo, nos adentraremos en el laberinto de los desbordamientos de enteros, desvelando cómo un simple fallo puede convertirse en la piedra angular de brechas de seguridad significativas.

En nuestro viaje por el complejo mundo de la seguridad del software, nos basaremos en un programa en C que he desarrollado (wow, qué pasada de software), un laboratorio virtual donde se manifiestan dos vulnerabilidades clásicas pero desconocidas por las nuevas remesas: el desbordamiento de buffer y el desbordamiento de enteros. Aquí, examinaremos como errores aparentemente menores en el código pueden convertirse en portales para ataques maliciosos, desafiando nuestra percepción de lo que consideramos un software «seguro».

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#include <locale.h>
#define BUFFER_SIZE 10
// Función vulnerable a desbordamiento de buffer
void vulnerableBufferFunction() {
    char buffer[BUFFER_SIZE];
    printf("Ingrese una cadena de texto (más de 10 caracteres causara un desbordamiento):\n");
    fgets(buffer, BUFFER_SIZE, stdin); // 'fgets' limita la entrada, pero aún se puede provocar un desbordamiento si se manipula mal
    buffer[strcspn(buffer, "\n")] = 0; // Eliminar el salto de línea
    printf("Usted ingresó: %s\n", buffer);
}
// Función vulnerable a desbordamiento de entero
void vulnerableIntegerFunction() {
    unsigned int a, b, result;
    printf("Ingrese dos numeros para sumar (valores muy grandes pueden causar un desbordamiento de enteros):\n");
    scanf("%u %u", &a, &b);
    result = a + b; // Riesgo de desbordamiento si 'a + b' excede el valor máximo de un unsigned int
    printf("Resultado: %u\n", result);
}
int main() {
    int choice;
    printf("Programa con Vulnerabilidades\n");
    printf("1. Funcion con desbordamiento de buffer\n");
    printf("2. Funcion con desbordamiento de enteros\n");
    printf("Elija una opcion (1 o 2): ");
    scanf("%d", &choice);
    while ((getchar()) != '\n'); // Limpiar el buffer de entrada
    switch (choice) {
    case 1:
        vulnerableBufferFunction();
        break;
    case 2:
        vulnerableIntegerFunction();
        break;
    default:
        printf("Opcion invalida.\n");
        break;
    }
    return 0;
}

Además, usaremos el Software de ingeniería inversa IDA para desmembrar el código y analizar las acciones. Podemos ver nuestras funciones vulnerables y el código en ensamblador:

Desbordamiento de enteros

Pongamos el foco en lo que nos interesa, fijaos en la siguiente parte del código:

// Función vulnerable a desbordamiento de entero
void vulnerableIntegerFunction() {
    unsigned int a, b, result;
    printf("Ingrese dos numeros para sumar (valores muy grandes pueden causar un desbordamiento de enteros):\n");
    scanf("%u %u", &a, &b);
    result = a + b; // Riesgo de desbordamiento si 'a + b' excede el valor máximo de un unsigned int
    printf("Resultado: %u\n", result);
}

La función suma dos números enteros sin verificar si la suma excede el valor máximo que puede almacenar un unsigned int.

Si los valores de a y b son lo suficientemente grandes, su suma puede exceder el valor máximo que un unsigned int puede almacenar (generalmente 2^32 – 1 en sistemas de 32 bits). Esto causará un desbordamiento, donde el resultado será mucho menor que el esperado, o incluso negativo si se interpretará como un entero con signo.

El desbordamiento de enteros puede llevar a resultados incorrectos y comportamientos impredecibles en el programa. En contextos de seguridad, esto puede ser explotado para evadir controles de seguridad o causar fallos en el sistema.

Para mejorar la seguridad debemos implementar controles para verificar si la suma de dos enteros excederá el valor máximo permitido. Si se detecta que la suma podría causar un desbordamiento, el programa debe manejar esta situación de manera adecuada, por ejemplo, emitiendo un mensaje de error y no realizando la operación.

Vamos a verlo en el depurador, primer hacemos que se produzca el error:

Ponemos un punto de interrupción y vemos las operaciones de asignación y operación que nos interesan:

Vemos en los registros que los valores que hemos introducido no se corresponden:

RAX: Tiene el valor 0x00000000FFFFFFFF, que es el máximo valor que puede tener un número entero sin signo de 32 bits.

RDX: También muestra 0x00000000FFFFFFFF.

Estos valores están al límite del rango de un unsigned int de 32 bits. La suma de dos valores 0xFFFFFFFF excedería el máximo valor representable, que es 0xFFFFFFFF (4294967295 en decimal). La suma debería ser 0x1FFFFFFFE (8589934590 en decimal), pero como el resultado se almacena en un unsigned int de 32 bits, el registro no puede representar este valor y se produce un desbordamiento.

Al realizar la suma con la instrucción add eax, edx, si ambos registros EAX y EDX contienen el valor 0xFFFFFFFF, el resultado esperado no cabrá en un registro de 32 bits.

En lugar de almacenar el valor correcto, el registro EAX contendrá el resultado de 0xFFFFFFFE sin los bits que exceden los 32 bits. Esto significa que el resultado es efectivamente 0xFFFFFFFE – 0xFFFFFFFF + 1 (debido a la aritmética modular), lo cual es 0x00000000 (0 en decimal), y el bit de acarreo (CF) se establecerá a 1 para indicar que se ha producido un desbordamiento.

El registro RAX muestra el valor 0x00000000FFFFFFFE. Este es el resultado de la operación de suma que se ha realizado en la función vulnerableIntegerFunction.

Para interpretar esto en términos de EAX:

  • EAX representa la parte de 32 bits de menor orden del registro RAX.
  • Por tanto, el valor de EAX sería 0xFFFFFFFE.

Esto indica que después de sumar dos valores de 0xFFFFFFFF, el registro EAX tiene un valor de 0xFFFFFFFE. Esto es una demostración clara de un desbordamiento de enteros, ya que la suma de los dos valores debería haber resultado en 0x1FFFFFFFE, pero debido a que EAX solo puede contener 32 bits, solo podemos ver los 32 bits menos significativos del resultado real, y el bit más significativo se ha perdido. Esto resulta en el valor de EAX mostrando 0xFFFFFFFE.

Además, puedes observar el registro de banderas EFL (Registro de Banderas de EFLAGS), el cual tiene el bit de acarreo (CF) establecido a 1. El bit CF se utiliza como indicador de desbordamiento para operaciones aritméticas. Cuando CF es 1 después de una operación de suma o resta, indica que la operación ha dado como resultado un desbordamiento o un préstamo, respectivamente, para valores sin signo. En este caso, el bit CF establecido confirma que el desbordamiento ocurrió durante la suma.

Este es un ejemplo de desbordamiento de enteros, cuando el resultado de una operación aritmética excede el rango representable del tipo de dato que se supone debe almacenar el resultado.

En la práctica, si este valor se utilizara para la asignación de memoria, acceso a índices de arrays, o cualquier otro propósito significativo, podría causar comportamientos inesperados, incluyendo corrupción de memoria, errores de lógica, o incluso brechas de seguridad si un atacante puede predecir o controlar el desbordamiento.

Desbordamiento de Bufer en acción

Vamos a ver otra parte relevante del código:

// Función vulnerable a desbordamiento de buffer
void vulnerableBufferFunction() {
    char buffer[BUFFER_SIZE];
    printf("Ingrese una cadena de texto (más de 10 caracteres causara un desbordamiento):\n");
    fgets(buffer, BUFFER_SIZE, stdin); // 'fgets' limita la entrada, pero aún se puede provocar un desbordamiento si se manipula mal
    buffer[strcspn(buffer, "\n")] = 0; // Eliminar el salto de línea
    printf("Usted ingresó: %s\n", buffer);
}

El buffer está definido con un tamaño de 10 caracteres (BUFFER_SIZE 10). Sin embargo, la función fgets está configurada para leer hasta BUFFER_SIZE caracteres, incluyendo el carácter nulo \0 al final de la cadena. Esto significa que el buffer solo puede almacenar efectivamente 9 caracteres legibles más el carácter nulo.

Si un usuario ingresa una cadena de más de 9 caracteres, fgets leerá hasta el máximo permitido (10 caracteres) y almacenará el carácter nulo al final, lo que provocará que se sobrescriba el último byte del buffer. Esto puede causar comportamientos impredecibles y potencialmente permitir a un atacante sobrescribir partes críticas de la memoria.

Aunque fgets es más seguro que gets, sigue siendo vulnerable si no se maneja correctamente el tamaño del buffer. Un desbordamiento de buffer puede llevar a la corrupción de datos, fallos del programa y, en casos extremos, ejecución de código arbitrario.

Para mejorar la seguridad se debe asegurar que el tamaño del buffer sea suficiente para almacenar todos los caracteres esperados, incluyendo el carácter nulo. Además, se debe validar la entrada del usuario para asegurarse de que no excede el tamaño del buffer.

Conclusión

Tras profundizar en el análisis de las vulnerabilidades de desbordamiento de enteros y Stack Overflow, es evidente que ambos representan serias amenazas para la seguridad en el software. El desbordamiento de enteros, ilustrado en el programa en C, resalta como operaciones aritméticas aparentemente inofensivas pueden exceder los límites de almacenamiento de los datos numéricos, llevando a resultados erróneos y potencialmente peligrosos. Este tipo de vulnerabilidad subraya la importancia de una programación cuidadosa y la implementación de controles adecuados para prevenir resultados impredecibles y ataques maliciosos.

Por otro lado, el Stack Overflow, aunque distinto en mecanismo, comparte la gravedad en términos de las consecuencias de seguridad. Esta vulnerabilidad ocurre cuando los datos exceden el espacio de memoria asignado, sobrescribiendo así información crítica y posiblemente permitiendo la ejecución de código arbitrario. Ambas vulnerabilidades exigen una mayor atención a la gestión de la memoria y la validación de datos, destacando la necesidad de prácticas de codificación seguras y una comprensión profunda de cómo los errores de programación pueden ser explotados.

Es curioso como si consultas a desarrolladores sobre estos problemas, mucho te dirán que los conocen, otros no han oído hablar ni siquiera de la pila de memoria, y la gran mayoría confían en los lenguajes y framewoks para encargarse de esto. Mi consejo, no nos confiemos, seamos profesionales y pensemos mal, pensemos que siempre hay alguien vigilando nuestro trabajo, para aprovecharse de nuestros errores, que seguro cometemos. Curiosamente, cuando pensamos que alguien nos mira, hacemos las cosas mejor.


Juan Ibero

Inmerso en la Evolución Tecnológica. Ingeniero Informático especializado en la gestión segura de entornos TI e industriales, con un profundo énfasis en seguridad, arquitectura y programación. Siempre aprendiendo, siempre explorando.

Compartir en:

Deja una respuesta

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