Intermediovulnerabilidadesinteger-overflowmemoriafundamentos

Integer Overflow: Cuando los Números Mienten

Análisis técnico de integer overflow: cómo la aritmética binaria con wraparound se convierte en vulnerabilidad, la diferencia entre overflow, underflow y truncation, CVEs históricos desde SSH CRC-32 hasta el Boeing 787, y por qué C/C++ son especialmente vulnerables.

MalwareIntel Research··13 min lectura

La aritmética que no cuadra

Los humanos pensamos en números como una recta infinita. Los ordenadores no tienen esa opción. Un entero de 32 bits solo puede representar 4.294.967.296 valores distintos. Cuando una operación aritmética produce un resultado fuera de ese rango, el número no "crece" ni genera un error. Simplemente da la vuelta. Como un cuentakilómetros de coche que pasa de 999.999 a 000.000.

Eso es un integer overflow. Y es una de las clases de vulnerabilidades más subestimadas y peligrosas en software de sistemas.

#include <stdint.h>
#include <stdio.h>

int main() {
    uint32_t a = 4294967295;  // Valor máximo de uint32: 2^32 - 1
    uint32_t b = a + 1;       // Overflow: da la vuelta a 0

    printf("a = %u\n", a);    // 4294967295
    printf("a + 1 = %u\n", b); // 0

    int32_t c = 2147483647;   // Valor máximo de int32 con signo
    int32_t d = c + 1;        // Overflow: pasa al negativo más bajo

    printf("c = %d\n", c);    // 2147483647
    printf("c + 1 = %d\n", d); // -2147483648

    return 0;
}

Ese segundo caso es particularmente traicionero. 2.147.483.647 + 1 da como resultado -2.147.483.648. Un número positivo enorme se convierte en el número negativo más bajo posible. Si ese valor se usa para calcular el tamaño de un búfer, para validar un límite, o para indexar un array, el programa se comportará de formas que el programador nunca anticipó.

Representación binaria: por qué ocurre el wraparound

Para entender el overflow hay que ver los números como el procesador los ve: secuencias de bits.

Enteros sin signo (unsigned)

Un uint8_t (8 bits) representa valores de 0 a 255:

00000000 = 0
00000001 = 1
01111111 = 127
10000000 = 128
11111110 = 254
11111111 = 255

Si sumamos 1 a 255 (11111111 + 00000001), el resultado binario es 100000000 (9 bits). Pero el tipo solo tiene 8 bits. El bit más alto se descarta. Queda 00000000 = 0.

Es aritmética modular: (255 + 1) mod 256 = 0. El estándar C define este comportamiento para enteros sin signo. No es un bug del lenguaje; es una especificación deliberada.

Enteros con signo (signed) y complemento a dos

Los enteros con signo usan complemento a dos. En un int8_t:

01111111 = +127  (valor máximo positivo)
10000000 = -128  (valor mínimo negativo)
11111111 = -1

El bit más alto (MSB) indica el signo: 0 = positivo, 1 = negativo. Sumar 1 a 01111111 (+127) da 10000000 (-128). El número "cruza" del máximo positivo al mínimo negativo.

En C, el overflow de enteros con signo es comportamiento indefinido (undefined behavior, UB). El compilador puede asumir que nunca ocurre y optimizar basándose en esa asunción. En la práctica, la mayoría de arquitecturas hacen wraparound, pero el compilador puede eliminar código que dependa de ese wraparound.

Las cuatro variantes del problema

1. Integer overflow (desbordamiento por arriba)

El resultado excede el valor máximo del tipo.

uint32_t size = 0xFFFFFFFF;  // 4.294.967.295
uint32_t new_size = size + 1; // 0x00000000 = 0

2. Integer underflow (desbordamiento por abajo)

El resultado es menor que el valor mínimo del tipo.

uint32_t count = 0;
uint32_t result = count - 1;  // 0xFFFFFFFF = 4.294.967.295

Un contador que "nunca debería ser negativo" se convierte en el número más grande posible.

3. Truncation (truncamiento)

Un valor se asigna a un tipo más pequeño, perdiendo bits altos.

uint32_t big_value = 0x10001;  // 65.537
uint16_t small_value = (uint16_t)big_value;  // 1 (se pierden los bits altos)

65.537 en 32 bits es 0x00010001. Truncado a 16 bits queda 0x0001 = 1. El programa cree que tiene 1 elemento cuando en realidad hay 65.537.

4. Signedness bug (error de signo)

Un valor se interpreta con el signo incorrecto.

int check_size(int length) {
    if (length > 1024) return -1;  // Rechazar si es demasiado grande

    // El atacante pasa length = -1
    // -1 < 1024, así que pasa el check
    // Pero como size_t (unsigned):
    memcpy(dest, src, (size_t)length);  // length = 4.294.967.295 en 32 bits
    // Copia 4 GB de datos. Buffer overflow catastrófico.
}

El patrón mortal: integer overflow a buffer overflow

La vulnerabilidad de integer overflow rara vez es el objetivo final del atacante. Es el medio para provocar un buffer overflow. El patrón es predecible:

// Código vulnerable
void process_items(uint32_t num_items, uint32_t item_size) {
    // Cálculo del tamaño total del búfer
    uint32_t total_size = num_items * item_size;

    // Si num_items = 0x10000 y item_size = 0x10001:
    // total_size real = 0x100010000 (supera 32 bits)
    // total_size almacenado = 0x00010000 = 65.536

    char *buffer = malloc(total_size);  // Asigna solo 65.536 bytes

    // Pero copia num_items * item_size bytes REALES de datos
    for (uint32_t i = 0; i < num_items; i++) {
        read_item(buffer + (i * item_size), item_size);
        // Escribe mucho más de 65.536 bytes → heap overflow
    }
}

El atacante controla num_items y item_size. Elige valores que, multiplicados, desbordan el entero de 32 bits y producen un total_size pequeño. malloc() asigna un búfer pequeño. El bucle escribe la cantidad real de datos. Heap overflow.

Este patrón se repite en decenas de CVEs históricos.

CVEs históricos: integer overflow en producción

SSH CRC-32 Compensation Attack (CVE-2001-0144)

Una de las primeras vulnerabilidades de integer overflow con impacto masivo. El código de detección de ataques CRC-32 en SSH1 contenía un bug de truncamiento.

El código recibía la longitud del paquete SSH como un entero de 32 bits, pero la asignaba a una variable de 16 bits. Para paquetes grandes, el valor se truncaba:

// Pseudocódigo del bug
uint32_t packet_length = receive_packet_length();  // Valor grande: 65.536
uint16_t buffer_size = (uint16_t)packet_length;    // Truncado: 0
// buffer_size = 0 o valor muy pequeño

Con buffer_size = 0, la función asignaba un búfer diminuto (o de tamaño por defecto 65.536 bytes, según la implementación), pero procesaba un paquete del tamaño original. El resultado: escritura arbitraria en memoria.

Afectó a OpenSSH 1.2.2 hasta 2.2, SSH Communications Security SSH 1.2.24 hasta 1.2.31 y F-Secure SSH 1.3.5 hasta 1.3.10. Fue corregido en OpenSSH 2.3.0 (noviembre de 2000), pero la vulnerabilidad fue ampliamente explotada contra sistemas sin parchear.

Windows Animated Cursor (CVE-2007-0038)

Una vulnerabilidad elegante en su simplicidad. Los archivos de cursor animado (.ANI) en Windows usan formato RIFF con chunks. Cada chunk anih contiene metadatos sobre la animación, incluyendo el tamaño de los datos.

El código de Windows (USER32.DLL) validaba correctamente el primer chunk anih de un archivo. Pero la función LoadAniIcon procesaba todos los chunks del archivo. Un atacante podía crear un archivo con dos chunks anih: uno válido (que pasaba la validación) y uno malformado con un valor de tamaño manipulado.

La función ReadChunk leía el segundo chunk anih con el tamaño declarado en el propio chunk. Si ese tamaño era mayor que el búfer de destino, se producía un buffer overflow en el stack, sobrescribiendo la dirección de retorno de LoadAniIcon.

El vector de ataque era devastador: cualquier aplicación que usara la API estándar de Windows para cargar cursores era vulnerable. Eso incluía Windows Explorer (abrir una carpeta con un archivo .ANI malicioso), Internet Explorer (visitar una web), Outlook (abrir un email HTML) y Firefox.

Microsoft había parcheado una vulnerabilidad similar en 2005 (MS05-002), pero el parche fue incompleto. Los investigadores de Determina demostraron que podían bypass el fix y explotar sistemas "completamente parcheados". El parche definitivo llegó en MS07-017 en abril de 2007.

Boeing 787 Dreamliner: el contador que reiniciaba un avión

En 2015, la FAA (Federal Aviation Administration) emitió una directiva de aeronavegabilidad para el Boeing 787 Dreamliner. El motivo: un integer overflow en las unidades de control de generadores eléctricos (GCUs).

El bug era conceptualmente simple. Un contador interno de software en cada GCU se incrementaba cada 10 milisegundos. El contador era (probablemente) un entero de 32 bits con signo, lo que le daba un rango máximo de 2.147.483.647.

Cálculo del tiempo hasta el overflow:
2^31 - 1 = 2.147.483.647 centésimas de segundo
2.147.483.647 * 0,01 segundos = 21.474.836,47 segundos
21.474.836,47 / 86.400 = 248,55 días

Después de exactamente 248 días de funcionamiento continuo, el contador desbordaba y la GCU entraba en modo de seguridad (failsafe). Si las cuatro GCUs principales (asociadas a los generadores montados en los motores) se habían encendido al mismo tiempo, las cuatro entrarían en failsafe simultáneamente.

El resultado: pérdida total de energía eléctrica de corriente alterna, independientemente de la fase de vuelo. En pleno vuelo, a 12.000 metros de altitud.

La solución temporal de la FAA fue obligar a reiniciar los 787 cada 120 días como máximo. Boeing desarrolló una actualización de software que estuvo lista a finales de 2015.

Este caso es un recordatorio de que el integer overflow no es solo una vulnerabilidad de "hackers". Es un defecto de ingeniería que puede tener consecuencias en el mundo físico.

OpenSSH Challenge-Response (CVE-2002-0639)

Otro caso donde integer overflow en OpenSSH permitía ejecución remota de código. La función de autenticación challenge-response contenía un bug donde el número de respuestas (nresp) se usaba para calcular el tamaño de un búfer sin validar que la multiplicación no desbordara:

nresp = packet_get_int();  // Controlado por el atacante
// Si nresp es un valor enorme:
response = xmalloc(nresp * sizeof(char*));  // overflow: tamaño pequeño
// Pero después se escriben nresp punteros reales → heap overflow

El atacante enviaba un valor nresp que, multiplicado por sizeof(char*), desbordaba y producía un tamaño de asignación pequeño. Después, el bucle de lectura escribía nresp punteros reales en el búfer, provocando un heap overflow masivo.

Por qué C y C++ son especialmente vulnerables

Sin protección aritmética nativa

En C y C++, las operaciones aritméticas sobre enteros no generan excepciones ni traps cuando desbordan. La suma de dos uint32_t siempre produce otro uint32_t, sin importar si el resultado real cabe en 32 bits.

Java y C# generan excepciones con la palabra clave checked. Python usa enteros de precisión arbitraria (nunca desbordan). Rust genera panic en modo debug. C no tiene nada.

Conversiones implícitas peligrosas

C permite conversiones implícitas entre tipos de diferente tamaño y signo:

int32_t signed_val = -1;
uint32_t unsigned_val = signed_val;  // 4.294.967.295 (sin warning)

uint32_t big = 300;
uint8_t small = big;                 // 44 (truncado, sin warning)

int negative = -5;
if (negative < sizeof(buffer)) {     // sizeof devuelve size_t (unsigned)
    // -5 se convierte a unsigned para la comparación
    // Se convierte en un número enorme
    // La condición es FALSE, no se ejecuta el bloque
    // El programador esperaba TRUE
}

El compilador puede advertir sobre algunas de estas conversiones (con -Wconversion), pero no es un error por defecto.

El compilador optimiza asumiendo que no hay overflow

Dado que el overflow de enteros con signo es undefined behavior en C, el compilador puede asumir que nunca ocurre y optimizar agresivamente:

// Código original del programador
int x = get_value();
if (x + 1 > x) {
    // El programador quiere detectar overflow
    do_something();
}

// Lo que el compilador puede generar (con optimización):
int x = get_value();
do_something();  // Siempre ejecuta: "x + 1 > x" es siempre true sin overflow

El compilador razona: "si no hay overflow, x + 1 siempre es mayor que x". Elimina la condición. El check de overflow que el programador escribió explícitamente desaparece del binario.

Prevención: herramientas y técnicas

Bibliotecas de aritmética segura

La solución más directa es usar funciones que detecten overflow antes de que ocurra:

// Microsoft SafeInt (C++)
#include <safeint.h>
SafeInt<uint32_t> safe_num_items(num_items);
SafeInt<uint32_t> safe_item_size(item_size);
SafeInt<uint32_t> total = safe_num_items * safe_item_size;
// Lanza excepción si la multiplicación desborda

// GCC/Clang built-ins
uint32_t total;
if (__builtin_mul_overflow(num_items, item_size, &total)) {
    // Overflow detectado
    return ERROR;
}
char *buffer = malloc(total);  // Ahora es seguro

Flags del compilador

  • -ftrapv (GCC/Clang): genera un trap (SIGABRT) cuando ocurre overflow de enteros con signo. Penalización de rendimiento significativa.
  • -fwrapv (GCC/Clang): define el overflow de enteros con signo como wraparound (en lugar de undefined behavior). Previene las optimizaciones agresivas del compilador, pero no detecta el overflow.
  • -Wconversion: advierte sobre conversiones implícitas que pueden perder datos.
  • /GS (MSVC): aunque no detecta integer overflow directamente, los stack cookies ayudan a mitigar buffer overflows derivados.

Aritmética checked en Rust

Rust aborda el problema desde el diseño del lenguaje:

let a: u32 = u32::MAX;  // 4.294.967.295

// En modo debug: panic!
// En modo release: wraparound (0)
let b = a + 1;

// Alternativas explícitas:
let checked = a.checked_add(1);       // Returns None
let saturated = a.saturating_add(1);  // Returns u32::MAX (4.294.967.295)
let wrapped = a.wrapping_add(1);      // Returns 0 (wraparound explícito)
let (result, overflowed) = a.overflowing_add(1);  // (0, true)

El programador elige el comportamiento. No hay silencio. No hay UB.

Validación de inputs

Antes de cualquier operación aritmética con datos del usuario, validar rangos:

// Patrón seguro
if (num_items > MAX_ITEMS || item_size > MAX_ITEM_SIZE) {
    return ERROR_INVALID_INPUT;
}
// Validar que la multiplicación no desborda
if (num_items > SIZE_MAX / item_size) {
    return ERROR_OVERFLOW;
}
size_t total = num_items * item_size;

La segunda condición (num_items > SIZE_MAX / item_size) es el check canónico de overflow para multiplicación: si a * b desborda, entonces a > MAX / b.

Integer overflow fuera de la seguridad

Vale la pena recordar que no todos los integer overflows son vulnerabilidades. Algunos son defectos funcionales con consecuencias graves fuera del ámbito de la ciberseguridad:

  • Ariane 5 (1996): un valor de aceleración horizontal de 64 bits se convirtió a un entero con signo de 16 bits. El valor excedía 32.767 y la conversión falló. El software de navegación interpretó el resultado como un giro brusco y activó los boosters de corrección. El cohete se autodestruyó 37 segundos después del lanzamiento. Coste: 370 millones de dólares.
  • Pac-Man nivel 256: el contador de nivel es un uint8_t. Al llegar a 256, desborda a 0. El código que dibuja las frutas calcula la mitad derecha de la pantalla basándose en el número de nivel, produciendo el famoso "kill screen" con basura gráfica.
  • Civilización de Gandhi: se atribuye (aunque Sid Meier lo ha desmentido como leyenda urbana) a un underflow donde la agresividad de Gandhi (valor 1) se reducía al adoptar la democracia (-2), produciendo un valor de 255 (máximo) en un uint8_t. Gandhi nuclear.

Conclusión: números que no se pueden confiar

El integer overflow es, en cierto sentido, la vulnerabilidad más "honesta". No depende de condiciones de carrera, de estados complejos ni de interacciones entre componentes. Es pura aritmética: el programador asumió que un número siempre cabría en su caja, y estaba equivocado.

La prevención es relativamente directa comparada con otras clases de vulnerabilidades: validar inputs, usar aritmética checked, compilar con flags protectores, y preferir lenguajes que no permitan overflow silencioso. Pero la realidad es que millones de líneas de código C y C++ siguen en producción sin estas protecciones, y seguirán estándolo durante décadas.

Cada vez que un programa calcula un tamaño de búfer multiplicando dos valores que un atacante puede influenciar, hay una oportunidad de integer overflow. Y cada integer overflow es potencialmente un buffer overflow esperando a ser explotado.

Recursos adicionales

Preguntas frecuentes

Artículos relacionados

Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.