- FlowTracker est un agent Java qui suit la manière dont les programmes Java lisent, manipulent et écrivent les données, en montrant à quelles entrées, fichiers, réseaux ou constantes du code chaque sortie est reliée
- En observant un programme en cours d’exécution, il affiche les E/S fichier et réseau et, surtout, suit la correspondance entre les entrées et les sorties pour aider à comprendre ce que signifie la sortie d’un programme Java et pourquoi elle a été produite
- Dans la démo Spring PetClinic, il est possible de remonter depuis les en-têtes de la réponse HTTP jusqu’aux templates Thymeleaf, aux valeurs de base de données et même aux scripts d’insertion SQL, ce qui permet d’explorer plusieurs couches de la pile logicielle
- En interne, il combine l’instrumentation du bytecode au chargement par la JVM, des hooks sur des méthodes du JDK, l’analyse de flux de données, le suivi des appels basé sur
ThreadLocaletClassOriginTrackerafin de maintenir une cartographie des origines centrée sur les chaînes, caractères et octets - Son état actuel est plus proche d’une preuve de concept que d’un outil prêt pour la production : il a bien fonctionné sur certains programmes d’exemple, mais ne convient pas à tous les programmes et entraîne un fort surcoût qui ralentit nettement l’exécution
Ce que FlowTracker suit
- FlowTracker est un agent Java qui suit comment les données sont lues, transmises, transformées et écrites dans un programme Java
- Il ne se contente pas d’afficher les E/S fichier et réseau, il montre aussi à quelles entrées correspond la sortie du programme
- L’objectif est de comprendre ce que signifie la sortie d’un programme Java, et pourquoi le programme a produit cette sortie
- Le projet actuel est une proof-of-concept qui explore les enseignements que peut apporter cette manière d’observer le comportement d’un programme
Démo : remonter l’origine d’une réponse HTTP dans Spring PetClinic
- La démo FlowTracker PetClinic permet de voir dans le navigateur comment Spring PetClinic traite une requête HTTP et génère une page HTML à partir de templates et de données de base de données
- L’écran affiche la réponse HTTP envoyée par PetClinic sur le réseau, et il est possible de cliquer sur une partie du corps de la réponse pour voir, dans la vue du bas, d’où elle provient
- On peut sélectionner les entrées et origines suivies, ou les sorties et sinks, depuis l’arborescence de gauche ou via le bouton en bas à gauche sur mobile
-
Couche de traitement HTTP
- En cliquant sur
"HTTP/1.1"ou sur un en-tête HTTP, on peut voir que cette partie de la réponse a été générée dans des classes Apache Coyote du packageorg.apache.coyote - FlowTracker indique quel code a produit quelle sortie
- En cliquant sur
-
Couche des templates Thymeleaf
- En cliquant sur des noms de balises HTML comme
"html"ou"head", on peut voir que cette partie du HTML provient du fichierlayout.html - Après avoir cliqué sur
layout.html, le bouton coloré+en bas permet de mettre dans la même couleur toutes les parties issues de ce fichier - En faisant défiler plus bas, on peut constater qu’une partie de la réponse provient d’un autre fichier,
ownerDetails.html - En cliquant sur le caractère
<ou>, on peut voir que ce caractère a été écrit par la bibliothèque de templates Thymeleaf
- En cliquant sur des noms de balises HTML comme
-
Origine des valeurs de base de données
- Le tableau de la page HTML contient des informations issues de la base de données
- En cliquant sur
Georgedans le tableau, on remonte non seulement au fait que la valeur vient de la base de données, mais aussi jusqu’au script SQL qui a initialement inséré cette valeur - Si, dans cette démo, le suivi remonte jusqu’au script SQL, c’est parce qu’elle utilise une base de données en mémoire et que son contenu n’est pas sorti de la JVM
Démo MySQL et indépendance vis-à-vis des frameworks
- Si l’on exécute la même démo PetClinic avec une base de données MySQL, les valeurs sont suivies jusqu’au point de connexion à la base
- Dans ce cas, on peut voir les requêtes SQL envoyées précédemment pour créer ces valeurs, ainsi que les détails de la manière dont le pilote MySQL JDBC communique avec la base de données
- La démo FlowTracker PetClinic mysql montre aussi que FlowTracker intercepte le contenu déchiffré transmis via la connexion SSL à la base de données
- Spring PetClinic n’est qu’un exemple, et FlowTracker ne dépend d’aucun framework ou bibliothèque spécifique
- La démo javac montre comment FlowTracker peut aider à comprendre le format du fichier class généré par le compilateur Java et le bytecode qu’il contient
Utilisation et précautions
- À l’heure actuelle, FlowTracker est plus proche d’une proof-of-concept que d’un outil prêt pour la production
- Il a bien fonctionné sur plusieurs programmes d’exemple, mais rien ne garantit qu’il fonctionnera correctement avec tous les programmes
- Il ajoute un surcoût important, ce qui ralentit fortement l’exécution des programmes
- Procédure d’utilisation :
- télécharger le jar d’agent
flowtracker-*.jardepuis les pages de releases Github - ajouter
-javaagent:path/to/flowtracker.jarà la ligne de commande Java - ajouter aussi à la ligne de commande la sortie de
java -jar flowtracker.jar jvmopts, afin de désactiver certaines optimisations de la JVM qui perturbent FlowTracker - par défaut, FlowTracker démarre un serveur web sur le port 8011 ; il suffit donc d’ouvrir
http://localhost:8011/dans le navigateur
- télécharger le jar d’agent
- Des options de configuration plus détaillées sont décrites dans
USAGE.md
Fonctionnement interne : instrumentation du bytecode et modèle Tracker
- FlowTracker est un agent d’instrumentation qui injecte du code dans les fichiers class, c’est-à-dire le bytecode, lorsque la JVM charge les classes
- Le code injecté maintient un mapping entre les données en mémoire et leurs origines pendant que le programme lit, transmet et écrit les données
- Le suivi se concentre surtout sur les données textuelles et binaires comme
String,charetbyte[], et non principalement sur les nombres, les données structurées ou les données calculées - Méthodes utilisées :
- certains appels de méthodes du JDK sont remplacés par des appels vers les versions FlowTracker de ces méthodes
- du code est injecté à des emplacements clés du JDK pour suivre les entrées et les sorties
- une analyse de flux de données et une instrumentation plus poussée sont appliquées pour suivre les variables locales et les valeurs de pile à l’intérieur des méthodes
- du code est ajouté avant et après les appels de méthode, ainsi qu’au début et à la fin des méthodes appelées, afin de suivre arguments et valeurs de retour via
ThreadLocal
-
Modèle de données Tracker
Tracker: stocke le contenu de l’objet suivi et les informations d’originecontent: les données, par exemple tous les octets passés par unInputStreamou unOutputStreamsource: relie une plage précise du contenu à une plage précise d’un autre trackerTrackerRepository: conserve une grandeMap<Object, Tracker>globale qui associe les objets d’intérêt à leurTrackerTrackerPoint: désigne une position dans un tracker et représente une valeur primitive unique, comme l’origine d’unbyte
Instrumentation de base : hooks JDK et ASM
- FlowTracker insère des appels à des méthodes hook lorsque certaines méthodes du JDK sont appelées afin de maintenir
Trackerà jour - L’exemple le plus simple est
System.arraycopy- Remplace l’appel à
java.lang.System.arraycopypar un appel àcom.coekie.flowtracker.hook.SystemHook.arraycopy SystemHookappelle ensuite le véritablearraycopy, puis récupère dansTrackerRepositoryles trackers des tableaux source et destination, et met à jour le tracker de destination pour qu’il pointe vers la source
- Remplace l’appel à
- Cette instrumentation utilise la bibliothèque de manipulation de bytecode ASM
- La plupart des hooks sont ajoutés du côté appelé, à l’intérieur des méthodes du JDK, plutôt que du côté appelant
- Par exemple, un appel à
FileInputStreamHook.afterReadByteArrayest ajouté à la fin deFileInputStream.read(byte[]) - Cette instrumentation est implémentée avec un micro-framework maison basé sur des annotations et utilisant
AdviceAdapterd’ASM
- Par exemple, un appel à
- FlowTracker ajoute des hooks à des classes du JDK liées aux E/S comme
java.io.FileInputStream,java.io.FileOutputStream,sun.nio.ch.FileChannelImpl,sun.nio.ch.IOUtiletsun.nio.ch.NioSocketImpl - Implémentations associées :
Suivi des valeurs primitive et analyse du flux de données à l’intérieur des méthodes
- Les valeurs primitive comme
byten’ont pas d’identité comme les objets, elles ne peuvent donc pas être suivies en toute sécurité comme clés deMapdansTrackerRepository - FlowTracker réécrit le code pour stocker séparément l’origine des valeurs primitive dans les variables locales à l’intérieur des méthodes
- Par exemple, après
byte b = x[1], il récupère le tracker debavecArrayHook.getElementTracker(x, 1), puis lors dey[2] = b, il enregistre l’origine dans le tableau cible avecArrayHook.setElementTracker(y, 2, bTracker) - Pour cela, FlowTracker effectue une interprétation symbolique au-dessus des fonctionnalités d’analyse d’ASM
- Il modélise, à chaque point d’une méthode, d’où viennent les valeurs des variables locales et de la pile, et où elles vont
- Implémentations associées :
- FlowValue et ArrayLoadValue
- MergedValue : gère les situations où une valeur peut provenir de plusieurs emplacements à cause du flux de contrôle, comme avec des
ifou des boucles - FlowInterpreter : extension de
Interpreterd’ASM qui interprète les instructions bytecode et crée lesFlowValueappropriés - Store et ArrayStore
- FlowTransformer : pilote l’ensemble du processus d’analyse et d’instrumentation
- Toutes les valeurs primitive ne sont pas suivies : l’accent est mis sur
byteetchar, tandis queintetlongsont gérés de façon plus limitée
Flux de données au-delà des appels de méthode
- L’analyse à l’intérieur d’une méthode ne suffit pas à gérer les cas où des valeurs primitive circulent via les arguments et les valeurs de retour d’autres méthodes
- FlowTracker stocke dans
InvocationlesPointTrackerdes arguments et des valeurs de retour, puis les place dans unThreadLocaljuste avant l’appel de méthode - Au point d’entrée de la méthode appelée,
Invocation.start(...)récupère les informations duThreadLocal, ce qui permet d’utiliser l’origine des arguments primitive - Avec cette approche, même lorsqu’une valeur primitive est transmise à une méthode comme dans
out.write(b), le tracker devaluepeut être repris à l’intérieur dewrite(byte value) - Implémentations associées :
Traiter le code lui-même comme source de données
- Les principales sources suivies par FlowTracker sont les E/S et les valeurs issues du code lui-même
- Les valeurs issues du code incluent des constantes primitives et
Stringcomme'a'et"abc" - Pour ces constantes, un
ClassOriginTrackerest créé par classe et conserve une représentation textuelle de la classe et des références aux constantes - Lorsqu’une constante est référencée, son tracker pointe vers sa position dans cette représentation textuelle
- Ce modèle traite les constantes comme si elles étaient lues depuis la représentation textuelle du code, ce qui le rapproche du modèle de suivi des E/S
- Pour des raisons de performance, ConstantDynamic (JEP 309) est utilisé afin que la méthode
constantPointne soit pas appelée à chaque exécution de méthode - Implémentations liées :
Traitement des littéraux String et limites
- Pour les littéraux
String, une nouvelle copie deStringest créée, puis lebyte[]dansString.valueest relié àClassOriginTracker - Une instruction comme
String s = "abc";est réécrite sous la formeString s = StringHook.constantString("abc", 1234, 81); - Cette approche casse la garantie d’interpréteur de chaînes (String interning) normalement fournie par la JVM
- À l’origine, toutes les occurrences d’une même constante
Stringdoivent référencer la même instance - Après instrumentation, du code qui dépend de cette garantie peut ne plus fonctionner
- À l’origine, toutes les occurrences d’une même constante
- FlowTracker prévoit plusieurs mécanismes pour limiter ce problème
- Grâce à ConstantDynamic, même si le même littéral
Stringsur une même ligne est exécuté plusieurs fois, la même instance est renvoyée à chaque fois - Certaines expressions
stringA == stringBsont réécrites enObjects.equals(stringA, stringB)afin qu’à certains égards elles se comportent comme si elles visaient la même instance - Le suivi des littéraux
Stringest désactivé dans certains packages commejava.lang.* - Ce comportement peut être configuré via
breakStringInterningdansUSAGE.md
- Grâce à ConstantDynamic, même si le même littéral
- Implémentations liées :
Fallback pour les valeurs non suivies
- FlowTracker ne suit pas toutes les valeurs du programme
- Cela s’explique par des préoccupations de performance, des parties pas encore implémentées, des valeurs jugées peu pertinentes, et le fait qu’exprimer des valeurs issues de la combinaison de plusieurs sources nécessiterait un modèle de données plus complexe
- Lorsqu’une valeur jusque-là non suivie atteint un point où le suivi doit commencer, elle est reliée à
ClassOriginTrackercomme une constante, et cette position est représentée par"<?>" - Par exemple, la longueur d’un tableau n’est pas suivie, donc si
write(array.length)est appelé, l’Invocationreçoit unPointTrackerpointant vers l’emplacement du code de l’appel àwrite - En conséquence, même si la sortie en format binaire ne permet pas de voir sa source d’origine, il est parfois possible d’interpréter rapidement le sens de la valeur grâce aux chaînes suivies autour et à la position du code
Autres sujets d’implémentation à couvrir
- MergedValue traite le suivi des valeurs à travers les branches et les boucles, souvent considéré comme la partie la plus difficile de l’analyse de flux de données
- La concaténation de chaînes est gérée en ajoutant des hooks au
MethodHandlerenvoyé parStringConcatFactoryvia indification (JEP 280) - La recherche du code source, la décompilation avec Vineflower et la liaison entre bytecode et lignes de source font aussi partie de l’implémentation
- La configuration du ClassLoader vise à éviter les dépendances au bootclasspath et les conflits avec l’application, tout en conservant un cycle de développement rapide sans shading ni nested jar
- Le suivi des valeurs primitives stockées dans les champs fait aussi partie de l’implémentation
- Le front-end se compose d’un serveur web basé sur Jetty et JAX-RS, ainsi que d’une interface web basée sur Svelte
1 commentaires
Avis sur Hacker News
Pour l’instrumentation, il utilise un fork du compilateur Clojure officiel plutôt qu’un agent d’instrumentation, et s’appuie sur la facilité qu’offre Clojure de changer de compilateur pendant le développement pour injecter du bytecode supplémentaire.
Ce qui est intéressant dans l’historique d’exécution des programmes Clojure, c’est que la plupart des valeurs sont immutables, donc conserver uniquement les pointeurs suffit à capturer des instantanés.
Comme la démo de l’article d’origine porte sur l’exploration d’une appli web, je laisse aussi, pour les personnes intéressées, une démo de débogage d’une appli web avec FlowStorm : https://www.youtube.com/watch?v=h8AFpZkAwPo
J’aime beaucoup les outils de l’écosystème Java/JVM. La dernière fois que j’ai été aussi surpris, c’était quand j’ai découvert jitwatch : https://github.com/AdoptOpenJDK/jitwatch
FlowTracker me fait un peu penser à l’analyse de contamination : suivre la façon dont des entrées utilisateur non fiables ou des valeurs secrètes circulent dans un programme, pour éviter qu’elles ne fuitent ou soient utilisées sans validation.
Le mot-clé à chercher est « dynamic taint tracking/analysis ».
https://github.com/gmu-swe/phosphor
https://github.com/soot-oss/SootUp
https://github.com/feliam/klee-taint
J’imagine très bien ce genre d’outil devenir à l’avenir une première ligne de défense pour traquer les bugs.
En développant FlowTracker, une grande partie du travail a consisté à faire fonctionner le traçage sur des programmes d’exemple précis.
Je connaissais le résultat attendu, mais il était difficile de prévoir quels mécanismes de bas niveau il faudrait prendre en charge pour qu’un exemple donné fonctionne ; cela dépendait souvent des détails d’implémentation internes du JDK ou des bibliothèques par lesquels les données passaient.
Mais le fait que l’élément HTML soit relié au script SQL qui avait inséré ces données dans la DB ne s’est pas produit de cette manière.
Ce n’était ni attendu ni conçu intentionnellement : c’est simplement arrivé, ce qui m’a pas mal surpris moi aussi et m’a donné envie de voir ce qu’on peut encore faire avec cette approche.
Ce serait aussi utile d’avoir un moyen de savoir si une donnée est temporaire ou si elle doit être réécrite.
Plus on peut décrire ce type de contraintes en amont, mieux c’est.
Dans Smalltalk, tout est objet et message, ce qui permet de remonter les traces et d’interagir avec les éléments.
Je n’ai pas réussi à dégager le temps nécessaire pour pousser l’idée plus loin, mais je pense que les outils de développement web auraient beaucoup à gagner avec ce genre de traçage d’attribution full-stack.
Cela dit, intégrer ce type de solution dans les frameworks existants me paraît être un gros défi.
[1] HTML Source Maps - https://github.com/connorjclark/html-source-maps https://docs.google.com/document/d/19XYWiPL9h9vA6QcOrGV9Nfkr...
https://www.youtube.com/watch?v=TWAMr72VaaU&t=164s et https://witheve.com/
En étendant ce que fait FlowTracker, on pourrait aussi détecter les vulnérabilités SQL ou d’autres failles d’injection. Il est donc possible que l’outil auquel tu penses ait utilisé une approche similaire.
Ou encore poser des questions du type : « qu’a vu cette chaîne de caractères entre le moment où elle a été créée et son arrivée sur mon écran ? »
Cela ressemble à un pas dans cette direction.
Je dois faire une pause pour l’instant, mais j’ai hâte de le faire fonctionner et de pouvoir l’explorer un peu.