Variables non initialisées

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

Informations de base

L’idée principale ici est de comprendre ce qui se passe avec les variables non initialisées car elles auront la valeur qui était déjà présente dans la mémoire qui leur est assignée. Exemple :

  • Fonction 1 : initializeVariable : Nous déclarons une variable x et lui assignons une valeur, disons 0x1234. Cette action revient à réserver un emplacement en mémoire et à y placer une valeur spécifique.
  • Fonction 2 : useUninitializedVariable : Ici, nous déclarons une autre variable y mais ne lui assignons aucune valeur. En C, les variables non initialisées ne sont pas automatiquement mises à zéro. Elles conservent plutôt la valeur qui a été stockée en dernier à leur emplacement mémoire.

Lorsque nous exécutons ces deux fonctions séquentiellement :

  1. Dans initializeVariable, x se voit assigner une valeur (0x1234), qui occupe une adresse mémoire spécifique.
  2. Dans useUninitializedVariable, y est déclarée mais non initialisée, elle prend donc l’emplacement mémoire juste après x. En ne initialisant pas y, elle finit par “hériter” de la valeur provenant du même emplacement mémoire utilisé par x, car c’est la dernière valeur qui s’y trouvait.

Ce comportement illustre un concept clé en programmation bas niveau : La gestion de la mémoire est cruciale, et les variables non initialisées peuvent entraîner des comportements imprévisibles ou des vulnérabilités de sécurité, puisqu’elles peuvent contenir involontairement des données sensibles laissées en mémoire.

Les variables non initialisées sur la pile peuvent poser plusieurs risques de sécurité, tels que :

  • Data Leakage : Des informations sensibles telles que des mots de passe, des clés de chiffrement ou des données personnelles peuvent être exposées si elles sont stockées dans des variables non initialisées, permettant à des attaquants de potentiellement lire ces données.
  • Information Disclosure : Le contenu des variables non initialisées peut révéler des détails sur la disposition mémoire du programme ou ses opérations internes, aidant les attaquants à développer des exploits ciblés.
  • Crashes and Instability : Les opérations impliquant des variables non initialisées peuvent entraîner un comportement indéfini, provoquant des plantages du programme ou des résultats imprévisibles.
  • Arbitrary Code Execution : Dans certains scénarios, des attaquants pourraient exploiter ces vulnérabilités pour altérer le flux d’exécution du programme, leur permettant d’exécuter du code arbitraire, ce qui pourrait inclure des menaces d’exécution de code à distance.

Exemple

#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;
}

Comment cela fonctionne :

  • initializeAndPrint Function : Cette fonction déclare une variable entière initializedVar, lui assigne la valeur 100, puis affiche à la fois l’adresse mémoire et la valeur de la variable. Cette étape est simple et montre le comportement d’une variable initialisée.
  • demonstrateUninitializedVar Function : Dans cette fonction, on déclare une variable entière uninitializedVar sans l’initialiser. Lorsque l’on tente d’afficher sa valeur, la sortie peut montrer un nombre aléatoire. Ce nombre représente les données qui se trouvaient auparavant à cet emplacement mémoire. Selon l’environnement et le compilateur, la sortie réelle peut varier, et parfois, par sécurité, certains compilateurs peuvent initialiser automatiquement les variables à zéro ; cependant, il ne faut pas s’y fier.
  • main Function : La fonction main appelle successivement les deux fonctions ci‑dessus, montrant le contraste entre une variable initialisée et une variable non initialisée.

Schémas d’exploitation pratiques (2024–2025)

Le classique bug “read-before-write” reste pertinent car les mitigations modernes (ASLR, canaries) reposent souvent sur le secret. Surfaces d’attaque typiques :

  • Partially initialized structs copied to userland : Le kernel ou les drivers font fréquemment un memset seulement d’un champ de longueur puis appellent copy_to_user(&u, &local_struct, sizeof(local_struct)). Le padding et les champs inutilisés leak des moitiés de stack canary, des saved frame pointers ou des kernel pointers. Si le struct contient un function pointer, le laisser non initialisé peut aussi permettre un controlled overwrite lorsqu’il est réutilisé.
  • Uninitialized stack buffers reused as indexes/lengths : Un size_t len; non initialisé utilisé pour borner read(fd, buf, len) peut donner aux attaquants des lectures/écritures hors limites ou permettre de contourner les vérifications de taille lorsque la case de stack contient encore une grande valeur d’un appel précédent.
  • Compiler-added padding : Même lorsque les membres individuels sont initialisés, les octets de padding implicites entre eux ne le sont pas. Copier l’ensemble du struct vers userland leak du padding qui contient souvent du contenu de stack précédent (canaries, pointers).
  • ROP/Canary disclosure : Si une fonction copie un struct local sur stdout pour le debugging, le padding non initialisé peut révéler le stack canary, permettant une exploitation de stack overflow ultérieure sans brute-force.

Patron minimal de PoC pour détecter ces problèmes lors d’une revue :

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;
}

Atténuations & options du compilateur (à garder en tête lors du contournement)

  • Clang/GCC auto-init: Recent toolchains expose -ftrivial-auto-var-init=zero or -ftrivial-auto-var-init=pattern, filling every automatic (stack) variable at function entry with zeros or a poison pattern (0xAA / 0xFE). Cela ferme la plupart des info leaks liés aux variables non initialisées sur la pile et rend l’exploitation plus difficile en convertissant les secrets en valeurs connues.
  • Linux kernel hardening: Kernels built with CONFIG_INIT_STACK_ALL or the newer CONFIG_INIT_STACK_ALL_PATTERN zero/pattern-initialize every stack slot at function entry, wiping canaries/pointers qui autrement se retrouveraient dans un leak. Cherchez les distros fournissant des kernels compilés avec Clang et ces options activées (courant dans les configs de hardening 6.8+).
  • Opt-out attributes: Clang now allows __attribute__((uninitialized)) on specific locals/structs to keep performance-critical areas uninitialized even when global auto-init is enabled. Passez en revue ces annotations attentivement — elles marquent souvent une surface d’attaque délibérée pour les side channels.

Du point de vue d’un attaquant, savoir si le binaire a été compilé avec ces flags détermine si les primitives de stack-leak sont viables ou si vous devez pivoter vers des divulgations du heap / des sections de données.

Trouver rapidement des bugs de variables non initialisées sur la pile

  • Compiler diagnostics: Build with -Wall -Wextra -Wuninitialized (GCC/Clang). Pour du code C++, clang-tidy -checks=cppcoreguidelines-init-variables corrigera automatiquement de nombreux cas en initialisant à zéro et est pratique pour repérer des locaux oubliés lors d’un audit.
  • Dynamic tools: -fsanitize=memory (MSan) in Clang or Valgrind’s --track-origins=yes signalent de manière fiable les lectures d’octets de pile non initialisés lors du fuzzing. Instrumentez les harness de test avec ces outils pour faire ressortir de subtils padding leaks.
  • Grepping patterns: Lors des revues, recherchez des appels copy_to_user / write de structs entiers, ou memcpy/send de données sur la pile où seule une partie du struct est initialisée. Faites attention aux chemins d’erreur où l’initialisation est ignorée.

Exemple ARM64

Cela ne change pas du tout sur ARM64 puisque les variables locales sont également gérées sur la pile ; vous pouvez check this example où cela est montré.

Références

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks