Variables no inicializadas

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks

Información básica

La idea central aquí es entender qué ocurre con las variables no inicializadas, ya que tendrán el valor que ya estaba en la memoria asignada a ellas. Ejemplo:

  • Function 1: initializeVariable: Declaramos una variable x y le asignamos un valor, digamos 0x1234. Esta acción es equivalente a reservar un espacio en memoria y poner un valor específico en él.
  • Function 2: useUninitializedVariable: Aquí declaramos otra variable y pero no le asignamos ningún valor. En C, las variables no inicializadas no se ponen automáticamente a cero. En su lugar, conservan el valor que se almacenó por última vez en su dirección de memoria.

Cuando ejecutamos estas dos funciones de manera secuencial:

  1. En initializeVariable, a x se le asigna un valor (0x1234), que ocupa una dirección de memoria específica.
  2. En useUninitializedVariable, y se declara pero no se le asigna un valor, por lo que ocupa el espacio de memoria inmediatamente después de x. Al no inicializar y, termina “heredando” el valor de la misma ubicación de memoria usada por x, porque ese es el último valor que había allí.

Este comportamiento ilustra un concepto clave en programación de bajo nivel: la gestión de memoria es crucial, y las variables no inicializadas pueden llevar a comportamientos impredecibles o vulnerabilidades de seguridad, ya que pueden contener sin querer datos sensibles dejados en memoria.

Las variables de pila no inicializadas podrían plantear varios riesgos de seguridad como:

  • Data Leakage: Información sensible, como contraseñas, claves de cifrado o datos personales, puede exponerse si se almacena en variables no inicializadas, permitiendo a atacantes leer potencialmente esos datos.
  • Information Disclosure: El contenido de variables no inicializadas podría revelar detalles sobre el layout de memoria del programa u operaciones internas, ayudando a atacantes a desarrollar exploits dirigidos.
  • Crashes and Instability: Operaciones que involucran variables no inicializadas pueden resultar en comportamiento indefinido, provocando fallos del programa o resultados impredecibles.
  • Arbitrary Code Execution: En ciertos escenarios, los atacantes podrían explotar estas vulnerabilidades para alterar el flujo de ejecución del programa, permitiéndoles ejecutar código arbitrario, lo que podría incluir amenazas de ejecución remota de código.

Ejemplo

#include <stdio.h>

// Function to initialize and print a variable
void initializeAndPrint() {
int initializedVar = 100; // Initialize the variable
printf("Initialized Variable:\n");
printf("Address: %p, Value: %d\n\n", (void*)&initializedVar, initializedVar);
}

// Function to demonstrate the behavior of an uninitialized variable
void demonstrateUninitializedVar() {
int uninitializedVar; // Declare but do not initialize
printf("Uninitialized Variable:\n");
printf("Address: %p, Value: %d\n\n", (void*)&uninitializedVar, uninitializedVar);
}

int main() {
printf("Demonstrating Initialized vs. Uninitialized Variables in C\n\n");

// First, call the function that initializes its variable
initializeAndPrint();

// Then, call the function that has an uninitialized variable
demonstrateUninitializedVar();

return 0;
}

Cómo funciona:

  • initializeAndPrint Function: Esta función declara una variable entera initializedVar, le asigna el valor 100, y luego imprime tanto la dirección de memoria como el valor de la variable. Este paso es directo y muestra cómo se comporta una variable inicializada.
  • demonstrateUninitializedVar Function: En esta función declaramos una variable entera uninitializedVar sin inicializarla. Cuando intentamos imprimir su valor, la salida puede mostrar un número aleatorio. Este número representa los datos que previamente estaban en esa ubicación de memoria. Dependiendo del entorno y del compilador, la salida real puede variar, y a veces, por seguridad, algunos compiladores podrían inicializar automáticamente las variables a cero, aunque no se debe confiar en ello.
  • main Function: La función main llama a ambas funciones anteriores en secuencia, demostrando el contraste entre una variable inicializada y una no inicializada.

Patrones prácticos de explotación (2024–2025)

El clásico bug “read-before-write” sigue siendo relevante porque las mitigaciones modernas (ASLR, canaries) a menudo dependen del secreto. Superficies de ataque típicas:

  • Partially initialized structs copied to userland: El padding y los campos sin usar pueden leak stack canary halves, saved frame pointers o kernel pointers. Si la struct contiene un function pointer, dejarlo sin inicializar también puede permitir un controlled overwrite cuando se reutilice más tarde.
  • Uninitialized stack buffers reused as indexes/lengths: Un size_t len; sin inicializar usado para acotar read(fd, buf, len) puede dar a los atacantes lecturas/escrituras fuera de los límites o permitir eludir comprobaciones de tamaño cuando la ranura de la pila todavía contiene un valor grande de una llamada previa.
  • Compiler-added padding: Incluso cuando miembros individuales están inicializados, los bytes de padding implícito entre ellos no lo están. Copiar toda la struct a userland leaks padding que a menudo contiene contenido previo de la pila (canaries, pointers).
  • ROP/Canary disclosure: Si una función copia una struct local a stdout para depuración, el padding sin inicializar puede revelar el stack canary, permitiendo una explotación de stack overflow subsiguiente sin brute-force.

Minimal PoC pattern to detect such issues during review:

struct msg {
char data[0x20];
uint32_t len;
};

ssize_t handler(int fd) {
struct msg m;              // never fully initialized
m.len = read(fd, m.data, sizeof(m.data));
// later debug helper
write(1, &m, sizeof(m));   // leaks padding + stale stack
return m.len;
}

Mitigaciones y opciones del compilador (ten en cuenta al evadir)

  • Clang/GCC auto-init: Toolchains recientes exponen -ftrivial-auto-var-init=zero o -ftrivial-auto-var-init=pattern, rellenando todas las variables automáticas (stack) al entrar en la función con ceros o un patrón de poison (0xAA / 0xFE). Esto cierra la mayoría de los uninitialized-stack info leaks y hace la explotación más difícil al convertir secretos en valores conocidos.
  • Linux kernel hardening: Kernels compilados con CONFIG_INIT_STACK_ALL o el más nuevo CONFIG_INIT_STACK_ALL_PATTERN inicializan a cero/por patrón cada slot del stack al entrar en la función, borrando canaries/pointers que de otro modo leakearían. Busca distros que distribuyan kernels compilados con Clang con estas opciones activadas (común en configuraciones de hardening 6.8+).
  • Opt-out attributes: Clang ahora permite __attribute__((uninitialized)) en locales/structs específicos para mantener zonas críticas para rendimiento sin inicializar incluso cuando el auto-init global está activado. Revisa dichas anotaciones cuidadosamente—con frecuencia marcan una attack surface deliberada para side channels.

Desde la perspectiva de un atacante, saber si el binario fue compilado con estas flags determina si stack-leak primitives son viables o si debes pivotar a heap/data-section disclosures.

Finding uninitialized-stack bugs quickly

  • Compiler diagnostics: Compila con -Wall -Wextra -Wuninitialized (GCC/Clang). Para código C++, clang-tidy -checks=cppcoreguidelines-init-variables auto-arreglará muchos casos para zero-init y es útil para detectar locales omitidos durante una auditoría.
  • Dynamic tools: -fsanitize=memory (MSan) en Clang o Valgrind’s --track-origins=yes marcan de forma fiable lecturas de bytes de stack no inicializados durante fuzzing. Instrumenta harnesses de prueba con estas herramientas para sacar a la luz sutiles padding leaks.
  • Grepping patterns: En revisiones, busca llamadas a copy_to_user / write que envíen structs completos, o memcpy/send de datos del stack donde solo parte del struct está inicializado. Presta especial atención a las rutas de error donde la inicialización se salta.

ARM64 Example

Esto no cambia en absoluto en ARM64 ya que las variables locales también se gestionan en el stack, puedes check this example were this is shown.

References

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks