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.
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.
| Signal | Sens habituel | Pertinence pour la sûreté mémoire |
|---|---|---|
SIGSEGV | Accè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 |
SIGBUS | Accè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 |
SIGABRT | Le 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 |
SIGILL | Instruction 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 |
SIGFPE | Division entière par zéro ou débordement dans une division | Faible à moyenne : généralement un bug de validation d'entrée |
SIGTRAP | Point d'arrêt ou instruction de trap | Dé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 :
| Message | Ce qui l'a détecté | Ce que cela signifie habituellement |
|---|---|---|
*** stack smashing detected ***: terminated | Protecteur de pile (__stack_chk_fail) | Un débordement de tampon de pile a écrasé le canari |
*** buffer overflow detected ***: terminated | FORTIFY_SOURCE | Un memcpy, strcpy, sprintf, etc. vérifié a dépassé la taille de destination |
free(): double free detected in tcache 2 | tcache de glibc | Le même pointeur a été libéré deux fois |
free(): invalid pointer | glibc | free a reçu un pointeur non renvoyé par malloc, ou les métadonnées étaient corrompues |
malloc(): corrupted top size | glibc | Un débordement de tas a corrompu la taille du top chunk |
malloc(): unaligned tcache chunk detected | safe-linking de glibc | Un pointeur de liste libre a été corrompu |
corrupted size vs. prev_size | glibc | Les en-têtes de chunk sont incohérents, typiquement à cause d'un débordement |
*** %n in writable segment detected *** | FORTIFY_SOURCE | Un 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 $ripmontre 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.
| Preuve | Priorité suggérée | Raisonnement |
|---|---|---|
| Lecture NULL ou proche de NULL | Faible à moyenne | Généralement déni de service seulement |
| Lecture hors-limites, tas ou pile | Moyenne à élevée | Divulgation d'information, peut saper l'ASLR |
| Écriture hors-limites, tas ou pile | Élevée | Corruption mémoire avec impact sur l'intégrité |
| Use-after-free ou double free | Élevée | Confusion de type, fréquemment exploitable |
| Canari de pile, FORTIFY ou abort de tas glibc | Élevée | Prouve qu'une corruption a eu lieu, même si la mitigation l'a arrêtée |
rip fautif hors du code, violation CET/CFI | Critique | Le flux de contrôle était déjà corrompu |
| Crash atteignable depuis une entrée réseau non fiable | Monter d'un niveau | La 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
errordu 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.