2 points par GN⁺ 2024-09-15 | 1 commentaires | Partager sur WhatsApp
  • 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 ThreadLocal et ClassOriginTracker afin 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 package org.apache.coyote
    • FlowTracker indique quel code a produit quelle sortie
  • 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 fichier layout.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
  • 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 George dans 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-*.jar depuis 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
  • 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, char et byte[], 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’origine
    • content : les données, par exemple tous les octets passés par un InputStream ou un OutputStream
    • source : relie une plage précise du contenu à une plage précise d’un autre tracker
    • TrackerRepository : conserve une grande Map<Object, Tracker> globale qui associe les objets d’intérêt à leur Tracker
    • TrackerPoint : désigne une position dans un tracker et représente une valeur primitive unique, comme l’origine d’un byte

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.arraycopy par un appel à com.coekie.flowtracker.hook.SystemHook.arraycopy
    • SystemHook appelle ensuite le véritable arraycopy, puis récupère dans TrackerRepository les trackers des tableaux source et destination, et met à jour le tracker de destination pour qu’il pointe vers la source
  • 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.afterReadByteArray est ajouté à la fin de FileInputStream.read(byte[])
    • Cette instrumentation est implémentée avec un micro-framework maison basé sur des annotations et utilisant AdviceAdapter d’ASM
  • 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.IOUtil et sun.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 byte n’ont pas d’identité comme les objets, elles ne peuvent donc pas être suivies en toute sécurité comme clés de Map dans TrackerRepository
  • 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 de b avec ArrayHook.getElementTracker(x, 1), puis lors de y[2] = b, il enregistre l’origine dans le tableau cible avec ArrayHook.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 if ou des boucles
    • FlowInterpreter : extension de Interpreter d’ASM qui interprète les instructions bytecode et crée les FlowValue approprié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 byte et char, tandis que int et long sont 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 Invocation les PointTracker des arguments et des valeurs de retour, puis les place dans un ThreadLocal juste avant l’appel de méthode
  • Au point d’entrée de la méthode appelée, Invocation.start(...) récupère les informations du ThreadLocal, 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 de value peut être repris à l’intérieur de write(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 String comme 'a' et "abc"
  • Pour ces constantes, un ClassOriginTracker est 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 constantPoint ne 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 de String est créée, puis le byte[] dans String.value est relié à ClassOriginTracker
  • Une instruction comme String s = "abc"; est réécrite sous la forme String 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 String doivent référencer la même instance
    • Après instrumentation, du code qui dépend de cette garantie peut ne plus fonctionner
  • FlowTracker prévoit plusieurs mécanismes pour limiter ce problème
    • Grâce à ConstantDynamic, même si le même littéral String sur une même ligne est exécuté plusieurs fois, la même instance est renvoyée à chaque fois
    • Certaines expressions stringA == stringB sont réécrites en Objects.equals(stringA, stringB) afin qu’à certains égards elles se comportent comme si elles visaient la même instance
    • Le suivi des littéraux String est désactivé dans certains packages comme java.lang.*
    • Ce comportement peut être configuré via breakStringInterning dans USAGE.md
  • 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 à ClassOriginTracker comme 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’Invocation reçoit un PointTracker pointant 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 MethodHandle renvoyé par StringConcatFactory via 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

 
GN⁺ 2024-09-15
Avis sur Hacker News
  • Super. J’ai créé FlowStorm, un outil pour Clojure dans la même veine : http://www.flow-storm.org/
    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
    • Vraiment chouette. Je me demande pourquoi tu as choisi JavaFX. Après avoir choisi JavaFX, as-tu aussi regardé cljfx ?
    • Sympa. Je me demande aussi si tu apprécies l’approche qui consiste à utiliser les métadonnées des structures de données pour tracer les valeurs.
  • Vraiment impressionnant.
    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
  • La démo qui remonte d’un élément HTML jusqu’à l’instruction SQL qui a ajouté cette valeur dans la base de données est impressionnante.
    J’imagine très bien ce genre d’outil devenir à l’avenir une première ligne de défense pour traquer les bugs.
    • Merci.
      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.
    • En y réfléchissant, s’il existait une façon standard de suivre l’origine et l’authenticité des données, cela aurait probablement évité beaucoup de problèmes et permis d’exprimer plus facilement de nombreuses règles métier.
      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.
  • Je ne comprends pas encore complètement la vue d’ensemble ni les usages possibles, mais cela me rappelle les environnements Smalltalk, où tout peut être inspecté.
    Dans Smalltalk, tout est objet et message, ce qui permet de remonter les traces et d’interagir avec les éléments.
  • Très chouette. La vidéo de démo est bonne aussi, et ça a clairement l’air utile quand on doit se plonger dans une base de code inconnue.
  • Il y a quelques années, j’ai expérimenté un concept similaire[1]. Je voulais appliquer quelque chose comme les source maps JavaScript au HTML.
    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...
  • Dans le bon sens du terme, cela me rappelle la démo d’Eve-lang où, pour déboguer un programme, on demandait simplement « pourquoi n’est-ce pas ici ? ». Excellent travail.
    https://www.youtube.com/watch?v=TWAMr72VaaU&t=164s et https://witheve.com/
  • Si ma mémoire est bonne, il y avait un article sur un outil similaire pour détecter dynamiquement les injections SQL dans les programmes Java. Est-ce le même outil ?
    • Non, c’était probablement un autre outil.
      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.
  • J’ai un temps imaginé suivre les données au-delà d’Internet. Par exemple, savoir d’où vient une image et sur quel CDN elle se trouvait.
    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.
  • J’essaie de faire tourner cet outil dans VSCode, avec le projet que je cherche à comprendre.
    Je dois faire une pause pour l’instant, mais j’ai hâte de le faire fonctionner et de pouvoir l’explorer un peu.