Incident de sécurité ? Suspicion de compromission ? 09 71 18 27 69csirt@synacktiv.com

Dites bonjour à Pike!

Rédigé par Maxime Desbrus - 23/04/2026 - dans Développement, Outils, Système - Téléchargement

Dans cet article, nous allons présenter Pike, un agent LLM expérimental capable de générer et d'analyser des traces d'exécution de programmes Linux. Nous montrerons qu'avec une architecture simple couplée à un LLM performant, Pike permet de déboguer rapidement un crash, d'identifier un malware ou de fournir des analyses de haut niveau pertinentes, le tout via une interface de chat naturelle.

Vous souhaitez améliorer vos compétences ? Découvrez nos sessions de formation ! En savoir plus

De nos jours, "l'IA", les LLM et les agents sont sur toutes les lèvres, et chaque entreprise annonce à grand bruit son engagement envers ces technologies. De nouveaux modèles, architectures et pratiques de développement sont dévoilés chaque mois, rendant les précédents obsolètes en un clin d'œil. Pourtant, une conséquence intéressante de la progression de la qualité des modèles ces six derniers mois est que, dans bien des cas, la création d'agents s'en trouve grandement simplifiée.

Nous allons illustrer cela en construisant un agent simple, mais immédiatement utile. L'objectif est d'analyser l'exécution de programmes Linux en boîte noire. Cela peut s'avérer utile dans divers scénarios. Voici quelques exemples de requêtes que nous souhaitons supporter :

  • Pourquoi ce programme plante-t-il lorsque je suis hors ligne ?
  • Donne-moi la liste de tous les fichiers persistants dans lesquels ce programme écrit.
  • Ce programme présente-t-il une race condition TOCTOU exploitable lorsqu'il écrit le fichier téléchargé dans /tmp ?

Comme source principale de données, nous utiliserons strace pour enregistrer tous les appels système (syscalls) effectués vers le noyau Linux. N'importe quel programme non trivial peut rapidement générer des centaines de milliers d'appels système, ce qui se traduit par des gigaoctets de journaux. C'est bien au-delà de ce qu'un analyste humain peut traiter avec précision, mais c'est précisément là que les agents LLM peuvent exceller, s'ils sont conçus intelligemment.

Choix de la stack

Notre agent, baptisé Pike, exposera une interface de chat simple dans un terminal (TUI). Nous avons choisi de l'écrire en Python, car l'écosystème pour les agents y est très riche. Pour notre agent, nous avons besoin d'un LLM, d'outils exposés et d'une structure pour lier le tout et piloter la boucle de l'agent.

Pour le construire, nous voulons nous appuyer sur des bibliothèques de haut niveau nous permettant de nous concentrer sur la logique métier plutôt que sur la plomberie de la boucle de l'agent. Nous souhaitons particulièrement faire abstraction des différences entre les fournisseurs d'inférence et leurs API, afin de pouvoir supporter, par exemple, les modèles de Google Gemini, Anthropic Claude, ou l'inférence locale avec llama.cpp via un simple changement de fichier de configuration. À part cela, nos exigences sont assez souples, car nous n'avons pas besoin, par exemple, de support pour les sous-agents, le MCP (Model Context Protocol), la planification, etc.

Nous avons choisi Pydantic AI, une bibliothèque populaire et établie qui répond parfaitement à nos besoins. Elle s'appuie sur les types et les décorateurs pour définir les outils de l'agent, leur description transmise au LLM, leurs dépendances au moment de l'exécution, etc. L'utilisation de types communs pour l'agent et tous les messages d'inférence s'est également révélée très pratique pour construire notre interface utilisateur.

Voici à quoi ressemble l'agent final à l'œuvre lors de l'analyse d'un binaire suspect :

Pike lançant une analyse
Pike lançant une analyse sur un binaire suspect

 

Définition des outils

Nous devons d'abord trouver un moyen d'exposer les données de log strace au LLM via un outil. Exposer les fichiers bruts ne mènerait nulle part : les premières lignes de log généreraient suffisamment de tokens pour saturer même les modèles dotés d'une fenêtre de 1 000 000 de tokens. Les données doivent être disponibles de manière structurée et pouvoir être interrogées efficacement. Par exemple, le modèle doit pouvoir lister toutes les adresses IP contactées via un seul appel d'outil. La solution naturelle est une base de données SQL, et SQLite est un excellent candidat pour de petites bases de données locales. Il est aussi parfaitement adapté à notre besoin : écriture seule avec un débit élevé pendant le traçage, puis lecture seule lors de l'exécution de l'agent.

Nous devons ensuite décider de la structure des tables. Les lignes de log strace peuvent être très diverses et complexes. Heureusement, nous avons déjà l'expérience de leur manipulation grâce au projet SHH. Chaque appel système est composé d'un nombre arbitraire d'arguments de types différents, potentiellement imbriqués (comme une structure C avec de nombreux attributs).

Notre schéma de base de données s'est rapidement orienté vers ceci :

CREATE TABLE syscalls (
    id        INTEGER PRIMARY KEY,
    pid       INTEGER NOT NULL,
    timestamp REAL    NOT NULL,
    name      TEXT    NOT NULL,
    ret_val   INTEGER,
    errno     TEXT
);

CREATE TABLE syscall_args (
    id         INTEGER PRIMARY KEY,
    syscall_id INTEGER NOT NULL REFERENCES syscalls(id),
    position   INTEGER NOT NULL,
    raw        TEXT    NOT NULL,
    type       INTEGER NOT NULL
);

C'est simple et efficace, mais pas encore suffisant pour notre agent. Par exemple, si nous voulons lister toutes les lectures dans le répertoire /home/user, nous pourrions faire :

SELECT syscall_args.raw
FROM syscall_args
  INNER JOIN syscalls
  ON syscalls.id = syscall_args.syscall_id
WHERE syscalls.name = "read" AND
  syscall_args.raw LIKE "%/home/user/%"

C'est très inefficace car la partie LIKE "%/home/user/%" nécessite de scanner chaque ligne. Pour améliorer cela, nous utilisons le module SQLite FTS5 (Full Text Search), qui résout exactement ce problème en créant un index supplémentaire.

Maintenant que la structure des données est définie, nous avons besoin d'un pipeline efficace pour les traiter en temps réel. Pour cela, nous supportons deux modes d'invocation en ligne de commande :

  • pike-agent run COMMAND : pour exécuter une commande, la tracer et générer une base de données indexée à la volée
  • pike-agent attach PID :  pour s'attacher à un programme déjà en cours d'exécution

Ensuite pike-agent chat peut être utilisé pour interroger le LLM sur une trace précédemment générée.

 

Pike main flow
Workflow de tracing et analyse

Prenons l'exemple d'un workflow complet, nous voulons lister toutes les adresses IP auxquelles Firefox se connecte pendant la navigation :

# generate trace by running Firefox
pike-agent run -o firefox.db firefox

# ask a single prompt on the generated trace
pike-agent chat firefox.db -p "Give me an exhaustive list of all IPv4 and IPv6 addresses this program sends traffic to."

 

Pour permettre au LLM d'interroger ces données structurées, nous devons définir une API d'outil et sa description. C'est un choix crucial qui déterminera avec quelle efficacité le modèle peut extraire les données, et exploiter toute leur complexité.

Une façon de faire est de définir une API de haut niveau avec par exemple une fonction pour obtenir les index de tous les syscalls d’un type donné, puis une autre pour obtenir les arguments de chaque syscall à partir de leur index. Cela fonctionnerait mais nécessiterait deux appels d’outils et un échange avec le modèle pour la plupart des requêtes, pour ce qui est essentiellement une opération très mécanique. Nous pourrions ensuite améliorer cela et récupérer tous les arguments des syscalls avec un JOIN SQL en une seule opération. Ce serait probablement plus efficace pour la partie base de données, avec le risque d’envoyer trop de données au modèle. Nous pourrions alors ajouter un argument de filtrage pour être plus efficace en tokens et permettre au modèle de ne conserver que certains arguments, ou ceux correspondant à un pattern donné.

À ce stade, il devrait devenir évident que ce n'est pas l'approche idéale. Nous serions en train de materner le modèle en exposant une API de haut niveau qui imite les fonctionnalités SQL, tout en étant finalement moins puissante et flexible. Nous pensons que l'approche idéale est plutôt d'exposer une connexion en lecture seule à la base de données au modèle, et de le laisser interroger les données avec du SQL brut comme il le souhaite.

En plus de l'API de base de données, nous fournissons au modèle :

  • une copie du schéma de la base de données dans le system prompt, afin que le modèle sache déjà quelles données il a à disposition et sous quel format
  • une définition d'outil qui décrit comment interroger la base de données avec quelques exemples

Nous avons également ajouté des outils permettant au modèle de lire et de rechercher dans les pages man, au cas où il aurait besoin de vérifier la définition d'un flag obscur ou d'un cas particulier.

Tests et évaluations

Tester un agent est notoirement difficile car les modèles sont par nature non déterministes. Il existe des approches à grande échelle impliquant des centaines de requêtes ou un agent dédié pour juger les réponses, mais pour notre agent simple, nous avons choisi une solution différente.

Nous avons défini plusieurs scénarios réalistes, d'abord les plus simples :

  • « Liste tous les fichiers auxquels ce programme accède » : requête large mais simple, facile à valider
  • « Quel contenu ce programme écrit-il dans le fichier /un/chemin/ » : teste la capacité du modèle à effectuer une requête FTS ciblée

Puis, des prompts nécessitant plus de raisonnement :

  • « Ce programme est-il conforme à la spécification XDG Base Directory ? » : le modèle doit lire les variables d’environnement par exemple à partir du syscall execv et interpréter les chemins utilisés dans les lectures/écritures pour voir s’ils respectent la spécification (que le modèle doit connaître). Nous pouvons tester un cas positif et un cas négatif pour nous assurer que le modèle ne déduit pas la réponse de signaux non pertinents.
  • « Ce programme présente-t-il une vulnérabilité TOCTOU au niveau fichier lors de la création de fichiers dans /tmp ? » : nécessite de croiser les résultats de plusieurs accès aux fichiers sur les mêmes chemins.
  • « Dis-moi ce qui s'est mal passé lors de l'exécution de ce programme. Explique ton raisonnement. » : avec une trace d'un programme qui mappe un fichier en mémoire avec mmap, lequel se retrouve tronqué par un autre processus, provoquant un crash SIGBUS lorsque le premier processus accède à la région invalide. Cela nécessite que le modèle raisonne sur le séquençage des événements, connaisse les causes de SIGBUS et filtre le bruit des nombreux appels mmap.

Et enfin, pour tester les limites des modèles et séparer les bons des excellents, nous utilisons un prompt très large : « Dis-moi si tu serais inquiet de trouver ce programme en cours d'exécution sur l'une de tes machines. Explique ton raisonnement. ». Ensuite, nous le testons avec des traces d'exécution provenant de :

  • Un programme qui détourne l'invocation de sudo en écrivant un wrapper dans ~/.local/bin/sudo, qui contient un pattern curl URL | sudo sh, puis appelle l'exécutable sudo original. Cela devrait déclencher toutes les alarmes pour des raisons évidentes.
  • Notre précédent binaire Rust “TwoFace”. Le modèle devrait idéalement signaler l'exécution ELF en mémoire et le fingerprinting du système.

Pour chacun de ces scénarios, un prompt soigneusement rédigé a été utilisé, pas trop vague pour garantir une réponse utile, mais pas trop précis pour éviter de donner des « indices » sur la réponse attendue au modèle. Les agents de codage peuvent beaucoup aider à rédiger le prompt idéal ici, comme ils le font pour définir la formulation du system prompt et de la définition des outils. L'entrée optimale pour un LLM est souvent proche mais pas identique à la façon dont un humain la comprendrait, et les agents de codage peuvent donner des conseils éclairés sur ce qui est une prose ambiguë pour un modèle, ou comment orienter vers une réponse ou une utilisation d'outil spécifique avec des exemples.

Ensuite, pour juger la réponse, nous utilisons un ou plusieurs de ces éléments :

  • correspondance des chemins de fichiers : c'est facile car le modèle doit les sortir textuellement
  • correspondance d'une réponse simple (c'est-à-dire "oui" / "non" / "incertain") : nous disons explicitement au modèle de suivre un format de sortie spécifique, ne pas respecter le format exact serait de toute façon un échec du modèle à respecter les instructions du prompt
  • correspondance d'un ensemble de signaux attendus regroupés par mots-clés synonymes : cela évite les décalages si la formulation est légèrement différente, mais garantit que le modèle signale réellement les faits correctement liés

Ensuite, nous avons exécuté les tests pour quelques modèles différents et les avons ajustés jusqu'à ce qu'ils passent pour au moins deux modèles différents.

Voici une capture d'écran d'une session où Pike identifie avec succès le détournement de sudo :

Pike identifiant le détournement de sudo
Pike identifiant une activité malveillante

 

Modèles

Nous n’avons pas fait de comparaison exhaustive des modèles, car cela prendrait un temps déraisonnable et serait rapidement obsolète. Cependant, en comparant quelques modèles Gemini, Claude Sonnet 4.6 et Qwen 3.5 (variante 32B s'exécutant localement avec llama.cpp), quelques faits émergent déjà :

  • Tous les modèles testés peuvent écrire du SQL de manière fiable. Notre approche consistant à n’exposer que du SQL brut comme outil s’est avérée être un bon choix. Même les modèles open-weight de taille moyenne peuvent écrire des requêtes SQL avec des clauses JOIN correctement et efficacement.
  • Claude Sonnet 4.6 et Gemini 3/3.1 Pro sont très bons pour nos besoins et passent tous les tests d’évaluation.
  • Qwen 3.5 32B a des difficultés avec le FTS5 de SQLite. Dans nos tests, soit il ne l’utilisait pas, soit il écrivait des requêtes FTS incorrectes la plupart du temps. Nous avons atténué cela en ajoutant des exemples FTS à la définition de l'outil. Nous n’avons pas testé d’autres variantes de taille du même modèle, mais comme cette limitation est probablement due à des données manquantes à ce sujet dans son ensemble d’entraînement, nous ne pensons pas que cela se serait beaucoup amélioré.
  • Le même modèle Qwen rate le coche sur le raisonnement et échoue à la plupart de nos tests sauf les plus simples. Par exemple, il nous a affirmé avec assurance que rien d'anormal ne s'était produit avec la trace du crash SIGBUS mmap.
  • Gemini 2.5 Flash n’est pas un bon candidat, car il rate des faits importants et hallucine même parfois une réponse sans exécuter un seul appel d’outil. Ce n'est pas surprenant, car il est présenté comme un modèle “rapide” pas idéal pour les agents qui ont besoin de raisonnement.
  • Aucun des modèles n’a utilisé l’outil man que nous avons exposé. C’est en fait un signal positif car cela prouve que les modèles ont été entraînés sur les noms des syscalls, leurs arguments et leur sémantique. Malgré cela, nous avons gardé l’outil man exposé, le pire scénario étant qu’il ne soit jamais nécessaire et consomme quelques tokens dans chaque requête, cependant il peut être nécessaire pour des cas spécifiques très rares, par exemple où une valeur de flag a un comportement différent sur une version récente du noyau.

Conclusion

Les récents progrès dans la qualité des modèles ont ouvert de nouveaux cas d'usage pour la construction d'agents. Ce qui aurait auparavant nécessité plusieurs sous-agents et une architecture plus complexe peut désormais être réalisé avec des agents simples et spécialisés, armés d'outils soigneusement définis.

Le code de Pike est disponible sur https://github.com/synacktiv/pike-agent