guest@6admin.io:~$

augure et lictor : décider une exploitation et en rendre compte

Tous les frameworks d’exploitation répondent à la même question, “comment je lance cette technique”. La question qui compte vraiment est l’autre, celle que personne n’automatise, “laquelle lancer et pourquoi” et surtout “pourquoi de façon prouvable”. Je tourne autour depuis un prototype Perl greffé sur gdb en 2006, que j’ai enrichi plutôt que remplacé au fil des ans, d’abord d’un SWI-Prolog en 2009, puis d’un algorithme génétique et le cheminement mérite son propre récit. Aujourd’hui elle porte deux noms, augure et lictor et tout est dans le partage, l’un décide et rend compte, l’autre descend de la décision aux octets et exécute.

augure : l'augure lit le vol des oiseaux, les faits convergent, le verdict est une étoile
augure
lictor : les faisceaux de fibre, la piste derrière, la porte ouverte devant
lictor

augure, la couche qui décide et le prouve

Posez la question à un LLM et il vous recommandera un ret2libc contre une cible sans libc, avec aplomb, parce qu’il génère du texte, pas des preuves. augure génère des preuves, pas du texte, en gardant à l’esprit que ces preuves portent sur l’applicabilité et la faisabilité numérique d’une technique, pas sur la réussite finale de l’exploitation, qui ne se constate qu’à l’exécution et il le fait en trois temps qu’on peut relire un à un.

Il propose d’abord. Des règles Datalog sur la base de faits de la cible sélectionnent toutes les techniques applicables, de façon décidable, terminante et lisible et une règle peut interdire structurellement l’absurde, si bien que le ret2libc sans libc n’est pas “déconseillé”, il est impossible à proposer.

Il vérifie ensuite. Un chemin arithmétique rapide, puis une émission SMT-LIB vers un solveur externe au choix (z3, bitwuzla ou cvc5), confirment que la technique retenue est numériquement faisable et pas seulement logiquement applicable et un timeout rend unknown, jamais un crash.

Il explique enfin. Chaque décision sort avec sa provenance, quelle règle a tiré, quels faits l’ont justifiée, quel prior et quel tirage amorcé l’ont classée et la même entrée avec la même graine rend toujours la même décision.

Le classement n’est pas un vulgaire poids plat. Un bandit de Thompson adapte les priors cible par cible, mais il traite les choix comme indépendants et c’est précisément son angle mort, puisqu’un leak ne vaut rien en soi, il vaut ce qu’il débloque ensuite. Au-dessus du bandit, un MCTS planifie donc des trajectoires multi-étapes sur le graphe de transition des techniques et c’est lui qui préfère tirer d’abord un fmtstr_leak parce que la table dit ce qu’il ouvre, là où un poids plat rangerait bêtement le ret2libc au-dessus. Sur ce terrain augure ne se contente pas d’un corpus jouet, il reproduit une chaîne noyau documentée de l’extérieur, les sept étapes de PinTheft et l’exercice n’est pas de l’inventer mais de vérifier que le planificateur en retrouve l’ordre.

Le cœur de l’idée, je la traîne depuis longtemps. Quand un exploit dev tranche, “là c’est un cas de ret2plt”, ce choix est une heuristique et une heuristique est un optimum local, rapide, souvent juste, jamais testée systématiquement. augure est le banc d’essai de ces optima locaux, on y fait tourner son intuition contre un corpus CTF gelé et on regarde les inversions de pondération, l’humain surévalue le shellcode et le ret2libc, la donnée les sous-évalue et le ret2plt, l’ennuyeux, le fiable, était massivement sous-coté.

Rien n’est câblé en dur, pas même les techniques. Une technique nouvelle s’enseigne comme de la donnée, un pack YAML qui porte ses règles, ses entrées de base de connaissance, sa transition MCTS et ses priors, avec son auteur et sa source en provenance et un garde du corpus refuse tout pack qui réordonnerait une technique existante ou volerait le top-1 d’une cible déjà documentée, on étend le savoir, on ne réécrit pas l’histoire en douce.

Sur les chiffres, je joue cartes sur table, c’est tout l’intérêt d’un outil auditable. augure reproduit le corpus de référence gelé au trait près, trente-trois classifications sur trente-trois et cinq plans multi-étapes sur cinq, chaîne noyau comprise et la CI casse la build en dessous de cent pour cent, le même corpus servant de test de conformité entre le moteur Ruby et un moteur Datalog compilé en Soufflé qui doivent tomber d’accord au trait. Mais lisez la phrase suivante aussi attentivement que la précédente, ce sont des prédictions contre une vérité-terrain gelée, pas des résultats de terrain et les probabilités de succès des priors sont écrites, pas mesurées. Ce n’est pas de l’autonomie que je vends, c’est de la reddition de comptes et l’audit commence par cette phrase-là.

De la décision aux octets, sans jamais deviner un nombre

Le vieux prototype finissait sur une charge utile, le padding, l’adresse de retour, le shellcode. lictor build en reprend l’héritage, à une différence près qui change tout, chaque nombre est désormais mesuré ou sourcé, jamais supposé.

L’offset est mesuré. La voie rapide est statique, metasm décode l’ELF, résout les stubs PLT des imports dangereux, désassemble .text depuis chaque symbole de fonction, repère l’appel vers le sink vulnérable et lit le déplacement du buffer par rapport à rbp pour en déduire l’offset, puis valide contre les nombres canoniques de ROP Emporium. Quand la lecture statique ne trouve pas de retour contrôlé, la voie dynamique prend le relais, un cyclic pattern fait crasher le binaire sous le débogueur et l’offset se relit dans le motif, sans brute force ni constante magique et un offset introuvable est une BuildError bruyante, jamais un nombre inventé.

L’adresse de retour vient des faits avec leur provenance, le symbole gagnant ou le classique jmp rsp nommé d’après le registre de pile de la cible. Le gadget n’est pas cru sur parole, la chasse parcourt chaque offset des sections exécutables, décode une chaîne qui se termine sur ret et la classe sémantiquement, y compris les gadgets multi-instructions que les byte-patterns ne voient jamais, re-décode les octets à l’adresse annoncée et un gadget menteur est refusé.

Le shellcode, enfin, est du source assembleur, pas un blob hexadécimal ni un générateur externe, des stubs lisibles et versionnés, position-independent par construction et le test d’exécution réelle de la suite fait tourner nos propres octets. L’obfuscation est une couche au-dessus, un self-decoder à clé dont le get-pc évite le 0x00 par construction, des saveurs xor et polymorphe, des clés tirées par graine, l’héritage shikata mais auditable, la même graine reproduisant les octets exacts et une charge trop large pour un canal sans octet nul est refusée honnêtement plutôt que tronquée en silence.

Je tiens à la lignée, toute publiée, metasm (Guillot, SSTIC 2007), la déprotection semi-automatique de binaire (Guillot et Gazet, SSTIC 2008), l’exploitation automatique avec metasm (esec-lab, 2010), miasm (Desclaux, SSTIC 2012) et le ghost writing (2015) dont nos stubs sont l’écho en source assembleur. La boucle d’exploitation automatique de 2010, je ne prétends pas l’inventer, je la re-dérive, auditable cette fois.

Observer, pas seulement modéliser

Le profiler fait le gros du travail statique en Ruby pur et sans sous-processus, checksec, les gadgets, l’identification du crash par un motif de contrôle retrouvé dans les registres plutôt que par une forme en 0x41414141.

Le dynamique vient compléter et c’est là que l’outil quitte le modèle pour observer le réel. Un watchpoint sur le slot de retour attrape l’instruction qui plante l’overflow, la taint est prouvée et non inférée, une exécution vivante relève la libc et la pile et côté réseau un harvest se connecte au service, lit la bannière qu’il crache et en extrait des adresses vues en direct, de provenance “observed-live”, pas modélisées.

Entre les deux, le fuzzing est la couche de découverte pour les binaires sans write-up, un length sweep et des mutations avec un mutateur intégré sans dépendance, radamsa en option, des backends gdb, rizin ou lldb pour observer le crash qui redescend en faits et aucune émulation là-dedans, les tests d’exécution réelle sont la vérité.

lictor, l’exécution qui rend des comptes

augure décide, lictor exécute et le nom n’est pas un hasard, le licteur romain portait les faisceaux et appliquait les jugements, il ne jugeait pas. La plupart des automatisations offensives sont une boîte noire qui tire et dont on découvre la raison après coup ou jamais. lictor renverse ça, chaque choix de technique vient d’une règle vérifiée avec ses preuves et chaque action laisse une ligne JSONL attribuable et rejouable.

La chaîne va du scan au rapport et deux maillons méritent qu’on s’arrête. Le mirror d’abord, parce qu’une cible distante ne se profile pas toujours, alors on monte le même service en local, lictor profile le mirror, construit sur ses faits et une vérification différentielle prouve que le distant faute comme le modèle local avant que quoi que ce soit ne parte pour de vrai. L’exploitation automatique ensuite, mais au sens exact, quand la découverte ne trouve aucun exploit pour la technique décidée lictor en rédige un, une classe qui porte les constantes mesurées et le chargeur de la charge construite et le fichier atterrit en staging où l’opérateur le relit avant de le commiter. La boucle run ferme le cycle, décider, exécuter, relever le résultat, re-décider, avec un budget de tentatives et les résultats réinjectent les priors, l’automatisation vivant là, dans l’enchaînement et le réamorçage, pas dans un génie qui écrirait l’exploit à votre place.

Le vrai argument de vente, c’est la trace

Les deux ensemble, c’est “generate-then-verify” incarné, un LLM propose des hypothèses, augure dispose au sens strict, lictor exécute sous la seule responsabilité de l’opérateur et c’est l’exact inverse du théâtre que je passe mon temps à dénoncer. Ce que vous emportez n’est pas un outil qui frappe plus vite, c’est un outil dont vous pouvez répondre de chaque coup. Le handoff est le défaut, plan imprime la recette et n’exécute rien, la boucle qui exécute vraiment est opt-in et budgétée, la trace est append-only, décision puis provenance de règle puis action puis résultat, parce qu’un engagement qu’on ne peut pas rejouer est un engagement qu’on ne peut pas défendre. Et quand c’est un agent LLM qui pilote par MCP, la rigueur ne bouge pas, les outils exposés valident côté serveur, l’armure et le garde du corpus tournent là où l’agent ne peut pas les affaiblir, l’agent propose, augure dispose, lictor exécute, les freins ne sont pas un mode mais le design.

Côté implémentation, tout est en Ruby natif, non par nostalgie mais parce que l’écosystème le permet enfin, ronin et ses exploits comme des classes, les gems rex qui portent nativement les primitives internes de msf en gem install et sans démon, là où Metasploit reste un monolithe piloté par un RPC mort, le profiler sur metasm en Ruby pur et augure sans aucune dépendance runtime, chaque capacité externe entrant par une frontière de sous-processus ou HTTP qui garde à zéro la surface d’attaque des extensions natives.

Un mot d’éthique, parce qu’il porte. augure est une couche de décision, il ne contient aucun code d’exploitation, n’exécute rien et ne touche aucune cible, il encode des métadonnées de technique, la même classe de savoir qu’un write-up public. lictor n’est volontairement pas distribué en gem à portée du premier venu, il n’invente aucune technique d’exploitation, il assemble un artefact à partir de constantes mesurées et de briques existantes, déposé en staging pour relecture et orchestre l’outillage choisi par l’opérateur, le savoir vivant dans les dépôts et pas ici. L’outil existe pour du travail de sécurité autorisé, périmètre signé, labos qu’on possède, environnements CTF et vous êtes responsable de la légalité là où vous vous tenez.

Le présage et la hache

Une vingtaine d’années à enrichir le même raisonnement, de l’if/else greffé sur gdb au Prolog, au génétique, au bandit, au planificateur, ont atterri sur deux outils dont le but n’est pas de frapper plus vite mais de pouvoir répondre de chaque coup. augure sépare la décision de l’exécution, la rend prouvable et reproductible, nomme ce qui est mesuré et ce qui est seulement écrit et descend jusqu’aux octets sans jamais deviner un nombre. lictor exécute ce qui a été décidé, sous la seule responsabilité de l’opérateur et consigne tout dans une trace rejouable. L’augure lit les signes et rend compte, le licteur porte les faisceaux et exécute le jugement, ni l’un ni l’autre ne prétend décider à votre place et c’est précisément pour ça que je m’en sers.