Skip to article frontmatterSkip to article content

Cadenas seguras

El Dilema de las Cadenas en C, De la Simplicidad a la Inseguridad

Las cadenas de caracteres en el lenguaje C son una de sus características más fundamentales y, a la vez, una de sus mayores fuentes de vulnerabilidades de seguridad. Entender su diseño histórico es clave para comprender por qué son inseguras y por qué surgieron las “cadenas seguras”.

Cadenas Tradicionales en C: El Legado del NUL

Historia y Diseño

Cuando Dennis Ritchie y Ken Thompson diseñaron el lenguaje C en la década de 1970, lo hicieron pensando en la eficiencia y el hardware de la época (como la PDP-11). Los recursos eran extremadamente limitados. Para representar una secuencia de caracteres, adoptaron una convención simple y eficaz:

Este estilo se conoce como cadena terminada en nulo (o null-terminated string). Por ejemplo, la cadena “HOLA” se almacena en memoria como:

Hola\0

Esta simplicidad era genial para los sistemas de esa era. No se necesitaba almacenar metadatos adicionales como la longitud; la función que leía la cadena simplemente avanzaba hasta encontrar el \0.

La Raíz de la Inseguridad: Ausencia de Información de Tamaño

La principal debilidad de este diseño es que la propia cadena no sabe cuál es su longitud ni la capacidad del buffer que la contiene. Para obtener su longitud, una función como strlen() debe recorrerla carácter por carácter desde el inicio hasta encontrar el \0, una operación de complejidad O(n)O(n).

El verdadero peligro surge con funciones de manipulación como strcpy() (copiar), strcat() (concatenar) o gets() (leer desde la entrada estándar). Estas funciones escriben datos en una memoria destino asumiendo ciegamente que hay espacio suficiente.

El Ataque Clásico: Buffer Overflow (Desbordamiento de Búfer)

Imagina este código:

#include <stdio.h>
#include <string.h>

void vulnerable() {
    char buffer_pequeno[10]; // Buffer con capacidad para 9 caracteres + \0
    char entrada_maliciosa[] = "AAAAAAAAAAAAAAAAAAAA"; // 20 caracteres

    //strcpy no sabe que buffer_pequeno solo tiene 10 bytes.
    //Copiará los 20 caracteres de la entrada, más el \0.
    strcpy(buffer_pequeno, entrada_maliciosa);

    printf("Contenido del buffer: %s\n", buffer_pequeno);
}

Al ejecutar strcpy(), se escriben 21 bytes (20 ‘A’ más el \0) en un espacio de solo 10 bytes. Los 11 bytes extra se escriben fuera de los límites del buffer_pequeno, sobrescribiendo otras áreas de la memoria en el stack.

Esto puede causar desde un simple fallo del programa hasta las peores vulnerabilidades de seguridad, como la ejecución de código arbitrario, si un atacante diseña la entrada_maliciosa para sobrescribir la dirección de retorno de la función con la dirección de su propio código malicioso.

Cadenas Seguras: La Solución del Tamaño Explícito

Para solucionar estos problemas, surgieron las “cadenas seguras”. El concepto fundamental es simple pero poderoso: la estructura de datos de la cadena debe contener información sobre su tamaño.

Una cadena segura generalmente almacena dos piezas clave de metadatos junto a los datos de los caracteres:

  1. Longitud (length): El número actual de caracteres en la cadena.

  2. Capacidad (capacity): El tamaño total del bloque de memoria asignado para la cadena, indicado como un size_t.

Con esta información, cualquier operación de escritura puede verificar primero si longitud < capacidad.

Ventajas Clave de las Cadenas Seguras

Implementaciones y Alternativas

Existen varias formas de implementar cadenas seguras en C y otros lenguajes.

1. Anexo K de C11 (Funciones _s)

El estándar C11 introdujo un anexo opcional (Anexo K) con versiones “seguras” de las funciones de cadena tradicionales. Estas funciones, como strcpy_s(), strcat_s() y snprintf_s(), generalmente toman un argumento adicional: el tamaño del búfer de destino.

// Ejemplo con strcpy_s
char buffer[10];
const char* origen = "Texto largo";

// strcpy_s verifica que el tamaño del buffer (10)
// es suficiente para copiar el origen.
// En este caso, devolvería un error en lugar de desbordar.
errno_t resultado = strcpy_s(buffer, sizeof(buffer), origen);

if (resultado != 0) {
    // Manejar el error de copia
}

Problema: Este anexo es opcional y su adopción ha sido muy limitada. Compiladores populares como GCC y Clang no lo implementan por defecto, y la comunidad lo ha criticado por su diseño y usabilidad. Microsoft sí lo implementa en su compilador MSVC.

2. Bibliotecas de Terceros (La Solución Práctica en C)

La forma más común y recomendada de usar cadenas seguras en C es a través de bibliotecas de alta calidad.

Tabla Comparativa

CaracterísticaCadenas NUL-TerminadasAnexo K de C11 (_s)Bibliotecas (SDS) / C++ std::string
SeguridadMuy Baja (propensa a overflows)Mejorada (requiere disciplina del programador)Alta (seguridad por diseño)
Obtener LongitudO(n) - LentoO(n) - LentoO(1) - Instantáneo
Gestión de MemoriaManual y propensa a erroresManual (el programador provee el buffer)Automática y gestionada por la biblioteca/objeto
Facilidad de UsoSimple para tareas básicas, compleja para seguridadVerbosa y propensa a errores de usoMuy alta, interfaz de alto nivel
Datos BinariosNo (el \0 actúa como terminador)No (la longitud determina el final)

Conclusión

Aunque las cadenas tradicionales de C son un pilar histórico de la programación de sistemas, su diseño es inherentemente inseguro para el desarrollo de software moderno. Ignorar la información sobre el tamaño del búfer fue una decisión comprensible en su momento, pero ha sido la causa de innumerables vulnerabilidades.