Seguridad y hardening
Las mitigaciones del compilador y el enlazador que endurecen tu binario contra la explotación, y los principios de secure coding para reducir la superficie de ataque.
C te da control total, y con él, la responsabilidad de la seguridad. Un buffer overflow no es solo un crash: puede ser una puerta a ejecutar código arbitrario. El hardening es el conjunto de defensas —del compilador, del enlazador y de tus hábitos— que hacen que, aunque cometas un error, sea mucho más difícil de explotar.
- Por qué C es una superficie de ataque.
- Mitigaciones del compilador y el enlazador.
- Defensa en profundidad.
- Principios de secure coding.
C y la superficie de ataque
Los errores de memoria de C no son solo bugs de fiabilidad: son vulnerabilidades. Un buffer overflow puede sobrescribir la dirección de retorno y redirigir la ejecución; un use-after-free puede manipularse para ejecutar código del atacante. Décadas de exploits explotan exactamente estos fallos. El hardening asume que cometerás errores y levanta barreras para que sean inofensivos.
Mitigaciones del compilador y el enlazador
Un conjunto de flags convierte tu binario en un objetivo mucho más duro:
gcc -std=c23 -O2 \
-fstack-protector-strong \ # canarios: detecta overflow del stack
-D_FORTIFY_SOURCE=3 \ # comprobaciones extra en funciones de C
-fstack-clash-protection \ # contra ataques de colisión de pila
-fPIE -pie \ # ejecutable independiente de posición (ASLR)
-Wl,-z,relro,-z,now \ # GOT de solo lectura (contra GOT overwrite)
-Wl,-z,noexecstack \ # pila no ejecutable
prog.c -o prog
Stack canaries
-fstack-protector: coloca un valor centinela antes de la dirección de retorno; si un overflow lo altera, aborta.
ASLR + PIE
-fPIE -pie: el binario se carga en direcciones aleatorias, dificultando que el atacante sepa dónde saltar.
FORTIFY_SOURCE
Reemplaza funciones peligrosas (memcpy, sprintf) por versiones que comprueban tamaños cuando puede.
RELRO
Hace de solo lectura estructuras que un atacante querría sobrescribir (como la GOT).
La filosofía del hardening es humilde y poderosa: da por hecho que tu código tiene bugs, y añade capas para que explotarlos sea lo más difícil posible. Un stack canary no evita el overflow, pero lo detecta antes de que haga daño. ASLR no arregla el bug, pero le quita al atacante el mapa de memoria. Ninguna mitigación es perfecta sola; juntas forman una defensa en capas donde el atacante debe vencerlas todas a la vez. Esto es ingeniería de seguridad madura: no la arrogancia de “mi código es perfecto”, sino el realismo de “mi código fallará, y cuando lo haga, quiero que el fallo sea inofensivo”. Actívalas por defecto en cualquier binario que procese datos no confiables.
Secure coding
Las mitigaciones son la última línea; la primera eres tú. Principios que reducen la superficie de ataque:
Valida toda entrada
Nunca confíes en datos externos (red, archivos, usuario). Comprueba tamaños, rangos y formato antes de usarlos.
Funciones con límite
Usa las variantes que reciben tamaño (snprintf, no sprintf). Nunca escribas sin conocer el espacio.
Mínimo privilegio
Menos código, menos permisos, menos superficie. Lo que no existe no se puede explotar.
Auditoría continua
Sanitizers, análisis estático y fuzzing (niveles 24-25) sobre cada cambio. La seguridad es un proceso, no un estado.
Un error común es pensar que las mitigaciones de producción hacen innecesarios los sanitizers. Al revés: los sanitizers (desarrollo/CI) encuentran y eliminan los bugs; el hardening (producción) contiene los que se escaparon. Necesitas ambos: caza bugs con ASan/UBSan/fuzzing antes de publicar, y despliega con mitigaciones activadas por si alguno sobrevivió. Juntos, hacen del C —un lenguaje sin red de seguridad por diseño— una herramienta con la que se puede escribir software seguro de verdad.
- Compila un programa con el set completo de flags de hardening.
- Provoca un stack overflow y observa cómo el canario (
-fstack-protector-strong) aborta con “stack smashing detected”. - Comprueba las protecciones activas de un binario (con
checksec, si lo tienes, oreadelf). - Revisa un programa tuyo: ¿validas toda la entrada externa antes de usarla?