L'exécution symbolique (symbolic execution) est une technique d'analyse de programme qui explore systématiquement tous les chemins d'exécution possibles en utilisant des valeurs symboliques au lieu de valeurs concrètes. Au lieu d'exécuter un programme avec des entrées spécifiques, le moteur d'exécution symbolique représente les entrées comme des variables mathématiques et résout les contraintes de chemin pour déterminer quelles entrées atteignent un état particulier — comme un crash, un buffer overflow, ou un appel à system(). Les outils angr, Triton et KLEE automatisent la découverte de vulnérabilités et la génération d'exploits dans les binaires compilés. Les chercheurs en rétro-ingénierie, les développeurs d'exploits et les analystes de vulnérabilités trouveront ici une méthodologie complète pour l'analyse automatisée de programmes et la découverte d'exploits avec des exemples de code pratiques.
En bref
- Théorie de l'exécution symbolique : valeurs symboliques, contraintes de chemin, solveurs SMT
- angr : architecture, projet, états, exploration strategies et exemples pratiques
- Triton : instrumentation dynamique symbolique avec Pin/Unicorn
- Applications offensives : découverte automatique de vulnérabilités et génération d'exploits
- Limitations : path explosion, environnement modeling, et techniques de mitigation
Principes de l'Exécution Symbolique
L'exécution symbolique repose sur trois composants fondamentaux :
| Composant | Rôle | Implémentation |
|---|---|---|
| Symbolic State | État complet du programme (registres, mémoire, contraintes) | SimState dans angr |
| Path Constraints | Contraintes accumulées le long du chemin | Formules SMT (bitvectors) |
| SMT Solver | Résolution des contraintes pour trouver des entrées concrètes | Z3, CVC5, Boolector |
angr : Framework d'Analyse Binaire
angr est le framework d'exécution symbolique le plus utilisé en sécurité offensive. Développé par le groupe Shellphish (UC Santa Barbara), il combine l'exécution symbolique avec l'analyse statique, l'émulation concrète et la résolution de contraintes :
#!/usr/bin/env python3
# angr — Résolution automatique d'un crackme
import angr, claripy
# Charger le binaire
proj = angr.Project('./crackme', auto_load_libs=False)
# Créer un état initial avec une entrée symbolique
password_len = 16
password = claripy.BVS('password', password_len * 8) # 16 octets symboliques
state = proj.factory.entry_state(
args=['./crackme'],
stdin=angr.SimFile('/dev/stdin', content=password)
)
# Configurer l'exploration
simgr = proj.factory.simulation_manager(state)
# Explorer : trouver le chemin vers "Access Granted" (0x401234)
# en évitant "Access Denied" (0x401256)
simgr.explore(find=0x401234, avoid=0x401256)
if simgr.found:
found_state = simgr.found[0]
solution = found_state.solver.eval(password, cast_to=bytes)
print(f"Password: {solution}")
else:
print("No solution found")
Génération Automatique d'Exploits avec angr
angr peut automatiser la découverte de vulnérabilités et la génération d'exploits. En configurant l'exploration pour atteindre des états dangereux (déréférence de pointeur contrôlé, appel à system(), écriture dans la GOT), le solveur SMT génère les entrées déclenchant la vulnérabilité :
# Découverte automatique de buffer overflow
import angr
from angr import sim_options as so
proj = angr.Project('./vuln_binary')
# État initial avec options de détection
state = proj.factory.entry_state(
add_options={so.SYMBOL_FILLS_UNCONSTRAINED_MEMORY,
so.SYMBOL_FILLS_UNCONSTRAINED_REGISTERS}
)
simgr = proj.factory.simulation_manager(state)
# Explorer jusqu'à un état "unconstrained" (RIP symbolique = contrôle du flux)
simgr.explore(
find=lambda s: s.solver.symbolic(s.regs.rip), # RIP symbolique !
avoid=lambda s: b"error" in s.posix.dumps(1)
)
if simgr.unconstrained:
crash_state = simgr.unconstrained[0]
# RIP est symbolique — on peut le résoudre vers n'importe quelle adresse
crash_state.add_constraints(crash_state.regs.rip == 0xdeadbeef)
exploit_input = crash_state.solver.eval(
crash_state.posix.stdin.load(0, 200), cast_to=bytes
)
print(f"Exploit input (RIP=0xdeadbeef): {exploit_input}")
Triton : Exécution Symbolique Dynamique
Triton (développé par Quarkslab) combine l'instrumentation dynamique (via Pin ou l'émulation Unicorn) avec l'exécution symbolique. Contrairement à angr qui analyse le binaire statiquement puis simule l'exécution, Triton observe l'exécution réelle et construit les contraintes dynamiquement — ce qui évite les problèmes de modélisation de l'environnement.
# Triton — analyse d'un binaire avec instrumentation dynamique
from triton import *
ctx = TritonContext(ARCH.X86_64)
# Charger le binaire en mémoire
binary = open('./target', 'rb').read()
ctx.setConcreteMemoryAreaValue(0x400000, binary)
# Symboliser l'entrée
for i in range(32):
ctx.symbolizeMemory(MemoryAccess(input_addr + i, 1), f'input_{i}')
# Émuler instruction par instruction
pc = entry_point
while pc:
inst = Instruction(pc, ctx.getConcreteMemoryAreaValue(pc, 16))
ctx.processing(inst)
# À chaque branchement, enregistrer la contrainte de chemin
if inst.isBranch():
constraint = ctx.getPathPredicate()
# Inverser la contrainte pour explorer l'autre chemin
model = ctx.getModel(ctx.getAstContext().lnot(constraint))
if model:
print(f"Alternative path input: {model}")
pc = ctx.getConcreteRegisterValue(ctx.registers.rip)
KLEE : Exécution Symbolique sur LLVM IR
KLEE opère sur le bytecode LLVM (IR) plutôt que sur les binaires compilés. Avantages : résolution de type plus précise, meilleure modélisation de l'environnement (libc symbolique), et intégration avec les suites de tests. KLEE a découvert des bugs dans Coreutils (GNU), des bibliothèques open-source, et des firmwares IoT. Limitation : nécessite le code source ou le bytecode LLVM.
Concolic Testing : Le Meilleur des Deux Mondes
Le concolic testing (concrete + symbolic) combine l'exécution concrète avec l'analyse symbolique. Le programme est d'abord exécuté avec une entrée concrète, les contraintes de chemin sont collectées symboliquement, puis le solveur inverse une contrainte pour générer une nouvelle entrée explorant un chemin différent. Cette approche atténue le problème d'explosion de chemins en guidant l'exploration par l'exécution concrète.
Path Explosion et Stratégies de Mitigation
Le problème majeur de l'exécution symbolique est l'explosion de chemins : à chaque branchement conditionnel, le nombre de chemins possibles double. Un programme avec N branches a potentiellement 2^N chemins. Les stratégies de mitigation :
- Merging : fusionner les états à des points de convergence (réduire le nombre d'états actifs)
- Pruning : élaguer les chemins qui ne mènent pas vers les cibles (guided exploration)
- Symbolic summaries : résumer les fonctions fréquemment appelées plutôt que les ré-analyser
- Veritesting : basculer entre exécution symbolique statique et dynamique selon le contexte
- Fuzzing hybride : combiner exécution symbolique et fuzzing (AFL+QSYM, SymCC+AFL++)
Fuzzing Hybride : Symbolic + Coverage-Guided
Le fuzzing hybride combine la puissance du fuzzing coverage-guided (AFL++, libFuzzer) avec l'exécution symbolique pour maximiser la couverture de code. Le fuzzer explore rapidement les chemins simples, tandis que l'exécution symbolique résout les contraintes complexes (checksums, magic bytes, comparaisons multi-octets) que le fuzzing seul ne peut pas franchir.
angr-doc contient des exemples pour chaque type de challenge (crackme, exploitation, reversing). Le mode auto_load_libs=False accélère l'analyse en évitant de symboliser la libc complète.À retenir
- L'exécution symbolique explore systématiquement TOUS les chemins en utilisant des valeurs symboliques et un solveur SMT
- angr est le framework standard : chargement binaire, émulation, exploration et résolution de contraintes
- Triton combine instrumentation dynamique et exécution symbolique — plus précis mais plus lent
- La génération automatique d'exploits cible les états où RIP est symbolique (contrôle du flux)
- L'explosion de chemins est le problème majeur — les techniques hybrides (fuzzing + symbolic) le mitigent
- Le concolic testing (QSYM, SymCC) est l'approche la plus efficace en pratique
FAQ — Questions Fréquentes
Quelle est la différence entre angr et Triton ?
angr analyse le binaire statiquement et émule l'exécution dans son propre moteur (VEX IR + SimEngine). Triton instrumenté l'exécution réelle (via Pin/Unicorn) et construit les contraintes dynamiquement. angr est plus automatisé et meilleur pour l'exploration exhaustive. Triton est plus précis car il observe l'exécution réelle, mais plus lent et moins adapté à l'exploration large.
L'exécution symbolique peut-elle trouver des 0-days ?
Oui, l'exécution symbolique a trouvé des vulnérabilités réelles dans des logiciels en production. KLEE a découvert des bugs dans GNU Coreutils, S2E a trouvé des vulnérabilités dans des drivers Windows, et les outils hybrides (AFL+QSYM) sont utilisés par les équipes de fuzzing de Google (OSS-Fuzz). Cependant, l'exécution symbolique seule est limitée par l'explosion de chemins — les approches hybrides sont plus efficaces en pratique.
Comment choisir entre fuzzing et exécution symbolique ?
Le fuzzing (AFL++, libFuzzer) est meilleur pour la couverture large et les programmes avec peu de contraintes complexes sur les entrées. L'exécution symbolique est meilleure pour les programmes avec des vérifications strictes (checksums, magic bytes, protocoles). En pratique, combinez les deux (fuzzing hybride) : le fuzzer explore les chemins simples, l'exécution symbolique résout les contraintes bloquantes.
Besoin d'un accompagnement expert ?
Nos consultants spécialisés en analyse de vulnérabilités et sécurité applicative vous accompagnent dans l'évaluation de votre posture de sécurité.
Contactez-nousTélécharger cet article en PDF
Format A4 optimisé pour l'impression et la lecture hors ligne
À propos de l'auteur
Ayi NEDJIMI
Expert Cybersécurité Offensive & Intelligence Artificielle
Ayi NEDJIMI est consultant senior en cybersécurité offensive et intelligence artificielle, avec plus de 20 ans d'expérience sur des missions à haute criticité. Il dirige Ayi NEDJIMI Consultants, cabinet spécialisé dans le pentest d'infrastructures complexes, l'audit de sécurité et le développement de solutions IA sur mesure.
Ses interventions couvrent l'audit Active Directory et la compromission de domaines, le pentest cloud (AWS, Azure, GCP), la rétro-ingénierie de malwares, le forensics numérique et l'intégration d'IA générative (RAG, agents LLM, fine-tuning). Il accompagne des organisations de toutes tailles — des PME aux grands groupes du CAC 40 — dans leur stratégie de sécurisation.
Contributeur actif à la communauté cybersécurité, il publie régulièrement des analyses techniques, des guides méthodologiques et des outils open source. Ses travaux font référence dans les domaines du pentest AD, de la conformité (NIS2, DORA, RGPD) et de la sécurité des systèmes industriels (OT/ICS).
Ressources & Outils de l'auteur
Articles connexes
Frida et DynamoRIO : Instrumentation Binaire
Analyse Mémoire Forensique : Volatility pour Malware
Analyse mémoire forensique avec Volatility pour la détection de malware : extraction de processus, injection de code, ro
Analyse de Shellcode : Techniques de Rétro-Ingénierie
Rétro-ingénierie de shellcode : analyse statique et dynamique, émulation avec unicorn, extraction de payloads et dévelop
Commentaires
Aucun commentaire pour le moment. Soyez le premier à commenter !
Laisser un commentaire