Aller au contenu

Lire un rapport de crash : signaux, backtraces, core dumps

Trier les crashs natifs en défenseur : sens de SIGSEGV et SIGABRT, messages d'abort de glibc, gdb et core dumps, rapports ASan, WinDbg, et priorisation.

Publié le 7 min de lecture

Quand un programme natif plante, les premières minutes d'analyse décident si le crash est classé comme du bruit ou reconnu comme un bug de sûreté mémoire. Ce guide est un flux de travail de triage pratique pour défenseurs, développeurs et intervenants en réponse aux incidents : ce que le signal vous apprend, comment lire les messages de glibc et des sanitizers, comment extraire un backtrace utile d'un core dump, et comment prioriser les crashs par gravité. Il suppose le modèle mémoire présenté dans comment un processus organise sa mémoire.

Étape 1 : lire le signal

Sur Linux, un crash est presque toujours la remise d'un signal fatal. Le signal restreint la cause immédiatement.

SignalSens habituelPertinence pour la sûreté mémoire
SIGSEGVAccès à une page non mappée, ou violation de permission (écriture en lecture seule, exécution de non-exécutable), ou violation de shadow stack sous CETÉlevée : pointeur invalide, débordement, use-after-free
SIGBUSAccès mal aligné sur architectures strictes, ou accès à un fichier mappé en mémoire tronquéMoyenne : souvent un problème de fichier ou d'alignement
SIGABRTLe programme a appelé abort() : assert échoué, vérification de tas glibc, canari de pile, vérification FORTIFY, sanitizerÉlevée quand levé par glibc ou le protecteur de pile
SIGILLInstruction illégale : ud2 d'un trap du compilateur (__builtin_trap, mode trap d'UBSan, CFI), ou exécution d'orduresÉlevée si inattendue : possible corruption du flux de contrôle
SIGFPEDivision entière par zéro ou débordement dans une divisionFaible à moyenne : généralement un bug de validation d'entrée
SIGTRAPPoint d'arrêt ou instruction de trapDébogage, ou trap délibéré dans du code durci

Le journal du noyau enregistre les segfaults avec l'adresse fautive et le pointeur d'instruction :

server[4182]: segfault at 0 ip 000055d0c3a1b2f4 sp 00007ffd2c1e8a90 error 4 in server[55d0c3a1a000+5000]

segfault at 0 est un déréférencement de NULL. Sur x86, le champ error est le code d'erreur de faute de page, affiché en hexadécimal : 0x1 signifie que la page était présente (une violation de permission plutôt qu'une page manquante), 0x2 une écriture, 0x4 le mode utilisateur, et 0x10 une récupération d'instruction. Ainsi error 4 est une lecture en mode utilisateur d'une page non mappée, error 6 une écriture en mode utilisateur vers une page non mappée, error 7 une écriture en mode utilisateur vers une page en lecture seule, et error 15 (0x15) une tentative en mode utilisateur d'exécuter une page non exécutable.

Étape 2 : lire le message d'abort

Quand la bibliothèque C ou l'instrumentation du compilateur détecte une corruption, elle affiche un message sur stderr avant d'avorter. Ces messages sont des indicateurs précis :

MessageCe qui l'a détectéCe que cela signifie habituellement
*** stack smashing detected ***: terminatedProtecteur de pile (__stack_chk_fail)Un débordement de tampon de pile a écrasé le canari
*** buffer overflow detected ***: terminatedFORTIFY_SOURCEUn memcpy, strcpy, sprintf, etc. vérifié a dépassé la taille de destination
free(): double free detected in tcache 2tcache de glibcLe même pointeur a été libéré deux fois
free(): invalid pointerglibcfree a reçu un pointeur non renvoyé par malloc, ou les métadonnées étaient corrompues
malloc(): corrupted top sizeglibcUn débordement de tas a corrompu la taille du top chunk
malloc(): unaligned tcache chunk detectedsafe-linking de glibcUn pointeur de liste libre a été corrompu
corrupted size vs. prev_sizeglibcLes en-têtes de chunk sont incohérents, typiquement à cause d'un débordement
*** %n in writable segment detected ***FORTIFY_SOURCEUn bug de chaîne de format avec un format contrôlé par l'utilisateur

Les messages de tas indiquent où la corruption a été détectée, ce qui est souvent bien après qu'elle s'est produite. Ne déboguez pas l'appel free qui a avorté ; reproduisez le crash sous ASan, qui s'arrête à l'écriture fautive d'origine. Voir corruption du tas et use-after-free et débordements de tampon de pile pour les bugs sous-jacents.

Étape 3 : obtenir un core dump

Un core dump est un instantané de la mémoire et des registres du processus au moment du crash. Assurez-vous d'en avoir un :

# Autoriser les cores pour ce shell et ses enfants
ulimit -c unlimited

# Où vont les cores ?
cat /proc/sys/kernel/core_pattern
# |/usr/lib/systemd/systemd-coredump ...   -> géré par systemd-coredump

# Sur les systèmes systemd
coredumpctl list server
coredumpctl info server          # métadonnées, signal, résumé du backtrace
coredumpctl debug server         # ouvre gdb sur le core le plus récent

Les core dumps contiennent la mémoire du processus, qui peut inclure des secrets et des données personnelles. Traitez-les comme tout autre artefact d'incident sensible : restreignez l'accès, fixez une rétention et évitez de les copier dans des tickets partagés.

Étape 4 : trier dans gdb

Avec le core et le binaire et les infos de débogage correspondants :

gdb ./server core.4182
(gdb) bt                        # backtrace du thread qui plante
(gdb) info registers rip rsp rbp
(gdb) x/6i $rip                 # instructions au site du crash
(gdb) frame 2                   # sélectionner un cadre
(gdb) info locals               # locales dans ce cadre (nécessite les infos de débogage)
(gdb) thread apply all bt       # chaque thread, pour les bugs de concurrence
(gdb) info proc mappings        # carte mémoire (processus vivant) ; 'info files' pour un core

Ce qu'il faut chercher :

  • Où est rip ? À l'intérieur de votre code ou d'une bibliothèque connue est normal. Une adresse qui n'est dans aucun mapping, ou à l'intérieur d'une région de données, suggère que le pointeur d'instruction lui-même a été corrompu.
  • Qu'a-t-on accédé ? x/i $rip montre l'instruction fautive ; le registre qu'elle déréférence vous dit quel pointeur était mauvais.
  • Le backtrace a-t-il un sens ? Un backtrace qui se termine par des cadres ?? ou répète des adresses absurdes suggère une corruption de pile, des infos de débogage manquantes ou des infos de déroulement manquantes.

Les extensions de débogueur GEF et pwndbg ajoutent une vue de contexte plus conviviale (registres, pile, code) que beaucoup d'analystes préfèrent pour ce genre d'inspection.

Étape 5 : reproduire sous sanitizers

Une fois que vous avez l'entrée ou un moyen de déclencher le crash, recompilez avec -fsanitize=address,undefined -g -O1 et reproduisez. ASan signale le premier accès invalide avec les piles d'allocation et de libération, ce qui transforme un crash ambigu en diagnostic. Le guide des sanitizers explique chaque partie du rapport. Si le crash vient d'un fuzzer, l'entrée est déjà sur disque ; minimisez-la d'abord comme décrit dans fuzzing avec libFuzzer et AFL++.

Les crashs Windows, brièvement

Sur Windows, les équivalents sont les violations d'accès (0xC0000005), la corruption de tas (0xC0000374), le dépassement de tampon de pile / fail-fast (0xC0000409, levé par les vérifications /GS et d'autres conditions fail-fast) et les violations CFG. Les vidages de crash proviennent de Windows Error Reporting ou de procdump. Dans WinDbg :

!analyze -v        analyse automatisée : exception, cadre fautif, bucket ID
k                  backtrace de la pile
r                  registres
!heap -p -a <addr> détails du page heap (activer avec gflags /p /enable app.exe /full)

Le page heap (gflags) joue un rôle similaire aux redzones d'ASan, plaçant les allocations à côté de pages de garde afin que les débordements fautent à l'instruction exacte.

Étape 6 : prioriser

Un défenseur qui trie des centaines de crashs a besoin d'une estimation de gravité rapide et fondée sur des preuves. Les questions ci-dessous sont les mêmes que celles que posent des outils comme le plugin gdb exploitable ou les heuristiques de gravité de sécurité de ClusterFuzz. Elles éclairent la priorité des correctifs ; elles ne remplacent pas une véritable analyse de cause racine.

PreuvePriorité suggéréeRaisonnement
Lecture NULL ou proche de NULLFaible à moyenneGénéralement déni de service seulement
Lecture hors-limites, tas ou pileMoyenne à élevéeDivulgation d'information, peut saper l'ASLR
Écriture hors-limites, tas ou pileÉlevéeCorruption mémoire avec impact sur l'intégrité
Use-after-free ou double freeÉlevéeConfusion de type, fréquemment exploitable
Canari de pile, FORTIFY ou abort de tas glibcÉlevéeProuve qu'une corruption a eu lieu, même si la mitigation l'a arrêtée
rip fautif hors du code, violation CET/CFICritiqueLe flux de contrôle était déjà corrompu
Crash atteignable depuis une entrée réseau non fiableMonter d'un niveauLa surface d'attaque compte autant que le type de bug

Gardez la règle simple : les écritures l'emportent sur les lectures, l'atteignable par l'attaquant l'emporte sur le local, et tout ce qui a corrompu des données de contrôle est corrigé en premier.

Déduplication et bissection

Les grandes campagnes produisent de nombreux crashs de même cause racine. Regroupez-les par type de bug plus les quelques cadres de tête de la pile dans votre propre code (en ignorant les cadres de l'allocateur et de la libc). Pour les régressions, git bisect run avec un script qui compile et rejoue l'entrée qui plante identifie le commit qui a introduit le bug, ce qui pointe généralement droit sur le changement fautif.

Checklist

  • Notez le signal, l'adresse fautive, le pointeur d'instruction et le code error du noyau pour chaque crash.
  • Traitez les aborts de tas glibc, du protecteur de pile et de FORTIFY comme une corruption mémoire confirmée.
  • Conservez les infos de débogage pour chaque version afin que les cores de binaires dépouillés puissent être symbolisés.
  • Reproduisez avec ASan et UBSan avant de deviner la cause racine.
  • Priorisez par type d'accès, atteignabilité et selon que des données de contrôle ont été corrompues ; dédupliquez par cadres de tête.