3 points par GN⁺ 2024-08-30 | 1 commentaires | Partager sur WhatsApp
  • Un développeur a collé dans ChatGPT un JavaScript minifié pour comprendre l’implémentation d’un composant d’art ASCII vu sur le web, et l’outil a déroulé le flux obfusqué en une structure lisible
  • À l’origine, il s’agissait d’un composant React qui calculait les colonnes et les lignes selon la taille de la fenêtre, puis mettait à jour une grille de chaînes à 60 fps avec setInterval
  • ChatGPT a expliqué la sélection du jeu de caractères, le calcul des caractères selon les coordonnées et le temps, ainsi que le flux de mise à jour de div.textContent, avant de le réécrire en code TypeScript/React équivalent
  • Au départ, l’absence de caractères de bloc comme ░▒▓█ a fait penser à une implémentation différente, mais une nouvelle vérification du script d’origine a montré qu’il s’agissait en fait d’un problème d’encodage lors du copier-coller
  • Une fois le bon jeu de caractères rétabli, le rendu est devenu identique à celui du composant d’origine, montrant qu’on peut utiliser ChatGPT de façon pratique pour comprendre et réimplémenter du code minifié

Retrouver une implémentation d’art ASCII dans du JavaScript minifié

  • L’auteur a examiné le code source pour voir comment était implémenté le bloc d’art ASCII de la page reactive.network hackathon
  • Le code concerné était minifié, donc difficile à lire directement pour un humain ; en temps normal, il aurait fallu le décoder à la main ou retrouver un fichier .map pour restaurer la source originale
  • Cette fois, il a collé l’intégralité du code minifié dans ChatGPT et lui a demandé d’expliquer son fonctionnement

La structure révélée par le code minifié

  • Le code utilisait JavaScript et React pour générer un contenu textuel dynamique
  • Les principaux éléments étaient les suivants
    • des alias très courts pour des fonctions mathématiques comme Math.floor, Math.abs et Math.min
    • un jeu de caractères composé de "reactive.network REACTIVE.NETWORK" ainsi que de caractères spéciaux et de caractères de bloc
    • la sélection du jeu de caractères à utiliser selon le résultat de Date.now() % 3
    • le calcul de l’index du caractère à afficher à chaque coordonnée en fonction de la longueur du jeu de caractères choisi
  • La fonction centrale, eT, prenait des coordonnées de position et une valeur temporelle pour renvoyer un seul caractère à afficher
    • la valeur temporelle était convertie via t.time * 8e-5
    • les coordonnées étaient corrigées à l’aide de la plus petite valeur entre colonnes et lignes, ainsi que du ratio de l’écran
    • l’index du caractère était déterminé en combinant le résultat de Math.cos(position.x * position.x - position.y * position.y) avec la valeur temporelle

Le mode de rendu du composant React

  • Le composant utilisait useRef pour référencer le div destiné à recevoir le texte, et useState pour stocker la height et la width de la fenêtre
  • Le premier useEffect définissait la taille initiale avec window.innerHeight et window.innerWidth, puis mettait à jour ces dimensions à chaque événement resize
  • Le second useEffect calculait la taille de la grille ASCII à partir des dimensions de l’écran
    • cols valait Math.floor(width / 12) * 1.6
    • rows valait Math.floor(height / 12)
    • aspect valait cols / rows
  • setInterval s’exécutait toutes les 1_000 / 60 pour régénérer la chaîne à 60 fps
  • À chaque frame, le code parcourait toutes les lignes et colonnes, concaténait les caractères calculés, puis plaçait la chaîne finale dans element.textContent

Réécriture en TypeScript plus facile à lire

  • Le développeur a ensuite demandé une implémentation équivalente en TypeScript, mais rendue lisible pour un humain
  • Le code produit par ChatGPT traduisait le rôle du code minifié à l’aide des noms suivants
    • selectedCharacterSet
    • characterSetLength
    • calculateCharacter
    • le type Size
    • le composant React AsciiArtGenerator
  • Les noms de variables obscurs ont été remplacés par des noms qui exposent l’intention de l’implémentation
    • calculateCharacter calcule le caractère à afficher à partir des coordonnées, des colonnes et lignes, du ratio d’écran et du temps
    • textRef pointe vers le div qui contient réellement le texte
    • size et setSize représentent l’état de la taille de la fenêtre
  • Le code réécrit était bien plus lisible que la version minifiée d’origine, au point d’être suffisant pour apprendre comment le composant était implémenté

Malentendu sur des caractères manquants et problème d’encodage

  • Lors de la comparaison initiale, on a d’abord eu l’impression que le LLM avait manqué certains détails d’implémentation
  • En particulier, on pensait que des caractères comme ░▒▓█, visibles dans le composant d’origine, manquaient dans le résultat de ChatGPT
  • Par la suite, un utilisateur du forum HN a suggéré qu’il pouvait s’agir d’un problème de copier-coller, et une vérification supplémentaire a montré que le code source original contenait des caractères différents de ceux collés dans ChatGPT
  • En téléchargeant directement le script, il a été possible de récupérer les bons caractères, ce qui a confirmé un écart d’encodage comme cause du problème
  • Après correction du jeu de caractères, le rendu est devenu identique à celui du composant d’origine

1 commentaires

 
GN⁺ 2024-08-30
Avis de Hacker News
  • Je suis la personne qui a créé HumanifyJS, et j’ai développé un outil basé sur les LLM spécifiquement pour cet usage
    Il utilise les LLM au niveau de l’AST afin de garantir que le code continue de fonctionner après la désobfuscation : https://github.com/jehna/humanify

    • Je me demande s’il serait difficile d’ajouter une fonction pour renommer depuis zéro
      Je veux dire une fonction qui supprimerait tous les noms porteurs de sens dans du code ordinaire, puis choisirait de nouveaux noms à partir de l’algorithme et des noms restants, par exemple les noms intégrés
      Quand je refactorise, je le fais parfois manuellement avec un LLM, et c’est utile pour trouver des termes plus standard comme antiparallel_line au lieu de parallel_line_opposite_direction, ou des noms plus généralisables comme find_instance_in_list au lieu de find_animal_instance_in_animals
    • Davantage d’outils devraient être construits autour de l’AST, excellent travail
      Honnêtement, j’attends toujours un système de gestion de versions au niveau de l’AST
    • Je me demande quelles questions vous posez au LLM
      Est-ce que vous lui donnez toute la fonction et lui demandez « qu’est-ce qu’il faut renommer ? » en boucle jusqu’à ce que tout soit modifié, ou bien vous lui demandez tout d’un coup, puis vous parsez la sortie et vérifiez que l’AST correspond toujours ?
    • Je me demande si ça fonctionne aussi sur des fichiers énormes
      Je parle d’environ 50 000 lignes
      Modification : je suis en train de le lancer en mode openai sur un fichier JS de seulement 1,2 k ligne, et après 20 minutes il n’en est qu’à 70 %. Même si, en théorie, ça fonctionnait sur un fichier de 50 000 lignes, j’ai l’impression qu’il vaudrait mieux ne pas essayer
  • La minification JS est assez mécanique et simple, donc l’opération inverse devrait elle aussi être relativement facile
    Bien sûr, en général, c’est suffisamment pénible à faire à la main, mais la transformation elle-même est assez limitée, donc il est possible de lire le code avec seulement quelques notes pour suivre les identifiants obfusqués
    La déminification ou la désobfuscation plus générale semble encore être un problème ouvert. J’ai écrit par le passé quelques programmes volontairement obfusqués, et d’après mon expérience ChatGPT n’arrivait pas à les comprendre, même en surface
    Par exemple, il y avait un commentaire qui essayait d’expliquer avec GPT-4 le code d’un gist contenant un interpréteur Brainfuck en C de 160 octets [1], et la « version clarifiée » ne ressemblait pas du tout au code d’origine
    [1] https://gist.github.com/lifthrasiir/596667#gistcomment-47512...

    • Le fait qu’une opération soit simple ne veut pas dire que son opération inverse doit l’être aussi
      On peut citer par exemple multiplication/factorisation en nombres premiers, dérivation/intégration, souvenirs du passé/prédiction de l’avenir
      La désobfuscation de code est clairement l’un de ces problèmes inverses difficiles, et elle peut facilement devenir encore plus compliquée à cause de bugs, de routines inutilisées ou sans rapport, ou d’implémentations incorrectes qui donnent par hasard le bon résultat
      En ce sens, si ChatGPT obtient des résultats corrects en désobfuscation, c’est une bonne chose, mais il n’y a aucune raison de s’attendre a priori à ce qu’il en soit capable. C’est bien d’avoir vérifié les limites de ChatGPT sur des problèmes plus difficiles, mais je pense que ce que vous avez trouvé n’est qu’une borne supérieure. Je ne considère pas l’exemple de l’article original comme trivial
    • Remplacer let userSignedInTimestamp = new Date() par let x = new Date() est trivial, mais pour revenir en arrière, il faudra probablement lire et comprendre le code autour afin de voir dans quel contexte x est utilisé
      En plus, le reste du code est aussi minifié, ce qui complique encore les choses, et même si tout est correctement fait, cela reste au final une transformation avec perte : le nom de variable pouvait contenir des propriétés qui n’étaient pas explicitement présentes dans le code
    • Transformer une photo couleur en noir et blanc est assez simple, mais retrouver les couleurs d’origine ne l’est pas
      C’est parce que des données ont disparu pendant le processus
      La minification fonctionne de la même manière. Beaucoup d’informations nécessaires à la compréhension du code disparaissent, et les retrouver peut être très délicat
    • Remplacer un nom de variable descriptif comme timeFactor par un nom beaucoup plus court comme i peut être mécanique et simple, mais le processus est destructeur et l’inverser n’est pas vraiment facile
      C’est impossible sans une compréhension assez fine de ce que fait réellement le code. C’est précisément ce qu’a fait le LLM ici, et même si ce n’est pas surprenant, c’est impressionnant. Le réflexe de minimiser ça aussi vite n’est pas très agréable
    • Premier essai aléatoire avec Claude 3.5 Sonnet : https://claude.site/artifacts/246c1b1a-3088-447a-a526-b1e716...
      Je n’ai pas pu tester, je ne suis pas sur PC
  • Le titre ne devrait-il pas utiliser ChatGPT ou le nom du modèle, GPT-4, plutôt que le nom de l’entreprise, OpenAI ?

    • Quand on pense qu’OpenAI a transformé GPT, qui était un terme technique générique, en marque, il y a une certaine ironie équitable à employer OpenAI comme s’il s’agissait d’un nom de produit
  • Les LLM excellent dans la transformation de texte
    C’est leur force principale, mais j’ai l’impression qu’elle n’est pas assez exploitée

    • Ce n’est pas seulement une force principale : c’est précisément ce pour quoi les transformers ont été conçus, et l’on pourrait même soutenir, de façon polémique, que c’est tout ce qu’ils savent faire
      Les autres capacités, comme raisonner ou conserver des connaissances, ne sont que des effets secondaires de cette aptitude surhumaine à reproduire du texte sans “comprendre” le sens voulu
    • Ils sont particulièrement forts pour les transformations presque linéaires, où l’ordre ne change pas beaucoup et où il n’est pas nécessaire de prendre profondément en compte les relations entre les éléments
      Par exemple, ils ne savent pas trier une liste, mais ils savent traduire une langue. Parce qu’une liste triée presque correctement est fausse, alors qu’une traduction presque correcte est souvent acceptable pour nous
    • Avec les bons outils, on devrait pouvoir migrer une base de code vers d’autres langages et frameworks presque d’un simple clic
      Si une équipe se demande s’il vaut la peine de transformer un gros morceau de code Python en Elixir, elle n’a plus besoin de se contenter de suppositions
      Il y a quelque temps, j’ai essayé de traduire un script Python en JavaScript : c’était irréprochable, et avec quelques garde-fous, j’ai l’impression qu’on pourrait passer à l’échelle
    • N’est-ce pas déjà le principal usage en entreprise ?
      Nous nous en servons surtout pour extraire des données structurées à partir d’autres formats
    • Le problème, c’est qu’il faut des cas où l’on n’a pas à se soucier du risque d’hallucination, ou bien où l’on peut valider la sortie même sans disposer déjà de données dans un format utile
      Et en plus, il faut aussi ne pas avoir les connaissances ou les compétences qui permettraient de le faire plus vite avec awk, Python, Perl, etc.
  • Certains développeurs qui utilisaient la compression comme moyen d’obfuscation vont sûrement être en colère
    Ça me rappelle qu’une ancienne version de ColdFusion avait un outil de “chiffrement” du code. L’algorithme était très faible, et il n’a pas fallu longtemps avant que quelqu’un crée un outil de déchiffrement
    Mais certaines personnes n’aimaient pas ça, parce qu’elles croyaient l’outil sûr et l’utilisaient pour vendre du code sans en publier les sources. C’était à la fin des années 90/début des années 2000, avant que l’open source ne devienne la norme écrasante

    • Je pense qu’on va bientôt voir apparaître ce genre d’usages et une course aux armements
      Des sites proposeront des concours reposant en partie sur du code côté client obfusqué, et les participants malins pourront désormais le donner à ChatGPT pour augmenter leurs chances de gagner
      Ce n’est que le début
  • C’est un exemple de performance intellectuelle supérieure à celle des humains
    On ne peut pas le nier. Cette tâche est intellectuelle, et n’a rien à voir avec de la simple mémorisation. Le Web ne contient pas non plus d’énormes quantités de paires code minifié/code déminifié sur lesquelles les LLM pourraient s’entraîner
    Le LLM comprend ce qu’il déminifie et, sur ce sujet précis, il est globalement supérieur aux humains

    • Ce n’est que de la transformation de texte
      Peut-on vraiment être sûr qu’il n’y a pas beaucoup de paires code minifié/code non minifié sur le Web ? On peut facilement en générer à partir de JS existant pour en faire un jeu d’entraînement, et il existe déjà énormément de JS non minifié utilisé à l’entraînement
    • Je suis optimiste sur l’IA, mais je ne suis pas convaincu que ce soit un exemple du phénomène décrit
      Si les humains ont du mal à comprendre du code minifié, c’est à cause des noms de variables opaques, des boucles peu naturelles, des espaces réduits au minimum, etc. Les ordinateurs ne trouvent pas ces choses difficiles, et c’est d’ailleurs pour cela qu’on minifie. Une architecture d’attention devrait être bonne à ce genre de tâche
      Je pense aussi qu’il existe énormément de code minifié/non minifié. C’est précisément l’objectif des fichiers source map. Vu les investissements d’OpenAI dans la navigation Web et le développement logiciel, il ne serait pas surprenant qu’ils aient inclus des données minifiées/non minifiées dans l’entraînement
    • Je n’ai pas d’expertise en IA ni en compréhension de l’intelligence, mais ne pourrait-on pas aussi dire qu’une calculatrice de poche comprend vraiment l’arithmétique et possède une performance intellectuelle supérieure à celle des humains ?
    • Pourquoi ne devrait-on pas inclure dans le score humain le fait que des humains ont créé des outils pour aider à déminifier ?
      Le fait qu’un ordinateur multiplie instantanément des nombres à 1000 chiffres est un exemple où des humains ont d’abord créé un outil de ce type, et ont ainsi réussi la multiplication. Qu’y a-t-il d’autre, là-dedans, qui soit une réussite intellectuelle ? L’ordinateur ne s’est pas créé tout seul
      Si l’on trace la frontière de l’intelligence humaine au niveau du crâne et que l’on ne compte pas les outils que cette intelligence crée et utilise comme des étapes du processus de résolution de problèmes, alors il faut aussi accepter que les humains ne sont pas assez intelligents pour aller dans l’espace, pratiquer la chirurgie ou préparer la plupart de leurs repas
    • GPT-4 a consommé plus de code que tous vos ancêtres n’en verront au cours de leur vie, et il comprend les motifs intrinsèques entre le code et sa version minifiée
      Reconnaître la forme abstraite d’un code dont les noms de variables ont disparu, puis faire correspondre ces motifs à des noms de variables lisibles par des humains à partir de choses similaires vues sur l’immense Internet, cela ne paraît pas absurde
  • C’est intéressant, et cela semble s’être beaucoup amélioré
    Il y a un peu plus d’un an, j’avais reçu un e-mail me demandant de l’aide pour un plugin personnalisé sur un site WordPress piraté, et j’avais essayé d’utiliser GPT pour désobfusquer du code malveillant. À l’époque, j’avançais beaucoup mieux en suivant moi-même le code étape par étape
    Après avoir lu cet article, j’ai réessayé [0]. Il m’a donné quelque chose de compréhensible, mais le code était obfusqué au point d’évaluer essentiellement via eval une chaîne illisible à travers l’objet Window, donc cela ne suffisait pas à lui seul
    Une partie du rapport que j’avais envoyé à l’époque disait ceci : j’avais pu fouiller dans du code JavaScript fortement obfusqué, détecter le chargement de la page, appeler une façade de calcul théoriquement constant, puis décoder la logique qui redirigeait la page vers un site malveillant
    [0] https://chatgpt.com/share/f51fbd50-8df0-49e9-86ef-fc972bca6b...

  • Quelqu’un travaille-t-il sur un LLM de décompilation ?
    On dirait qu’on pourrait rendre tout code comme de l’open source.
    Dans ce cas, les données d’entraînement seraient faciles à produire. Il suffirait de compiler en masse du code GitHub gratuit avec plusieurs compilateurs, puis d’entraîner le modèle à inverser la compilation. C’est un cas où des données d’entraînement synthétiques sont adaptées et assez faciles à générer.
    Le décompilateur n’aurait qu’à être entraîné à inverser la compilation ; pour des tâches comme l’ajout de commentaires, on pourrait utiliser les grands LLM de code existants.

    • Les répercussions potentielles sont énormes.
      Au-delà de l’open sourcing, on peut imaginer décompiler facilement des applications propriétaires et les modifier pour corriger des bugs ou ajouter des fonctionnalités. Cela pourrait être particulièrement libérateur pour de très vieux programmes abandonnés.
      Pour des raisons juridiques, il est très probable que ce type de comportement devienne une fonctionnalité bloquée dans les grands modèles.
    • Avoir les sources n’est que la première étape ; il faut aussi le droit de lire ce code, de le modifier, de l’exécuter et de redistribuer les versions modifiées.
      Seul l’auteur du code peut accorder ces droits.
    • D’accord. Je pense que « l’IA génère/comprend du code source » est un gros leurre destiné à détourner l’attention.
      Si l’IA comprend vraiment très bien le code, elle produira ou corrigera directement des binaires.
      Je suis convaincu que, lorsqu’un vrai programmeur IA apparaîtra, il ne se souciera pas du code lisible par les humains et codera tout en langage machine. Si on lui confie la maintenance d’un système existant, il comprendra l’ensemble de la pile logicielle et matérielle.
      C’est un peu comme les modèles de diffusion pour la génération d’images, qui ne se préoccupent pas d’apprendre de véritables techniques de dessin.
    • Il existe un LLM qui décompile du x86 vers du C : https://github.com/albertan017/LLM4Decompile
    • Désobfusquer/déminifier n’est pas décompiler.
      C’est plutôt renommer des variables et des fonctions, et ajouter des retours à la ligne.
  • C’est très proche de la façon dont j’utilise souvent les LLM [0].
    Quand je dois lire du code à la main, je m’en sers comme première étape de décodage.
    Pouvoir donner un code spaghetti complètement illisible et demander à le rendre plus lisible pour partir de là, c’est incroyablement libérateur.
    Comme l’auteur l’a aussi constaté, les LLM ratent parfois des détails, mais je peux les repérer moi-même.
    Un autre usage, c’est lorsque je comprends ce que fait le code, mais que je n’arrive pas bien à saisir pourquoi il est écrit de cette manière. C’est particulièrement le cas quand la personne qui l’a écrit n’est plus dans l’entreprise. Dans ces cas-là, je colle la méthode dans un chat LLM, j’explique ce qu’elle fait, puis je demande pourquoi certaines choses pourraient être faites d’une façon particulière.
    Ce n’est pas toujours parfait, mais assez souvent, il produit une explication réellement cohérente, qui tient à la relecture et apporte un nouvel éclairage. Une ou deux fois, cela m’a même empêché de refactorer d’une manière qui m’aurait posé problème plus tard.
    [0] J’utilise ChatGPT et, plus récemment, openwebUI comme frontend pour plusieurs autres modèles, surtout des variantes de Claude, afin de comparer les différences. On peut aussi mettre en place des configurations amusantes où différents modèles relisent les réponses les uns des autres.

  • Si le code déminifié ne correspond pas au code minifié, comme le dit la partie finale selon laquelle « la réponse du LLM semble avoir raté quelques détails d’implémentation », son utilité diminue fortement.
    D’autant plus que, dans beaucoup de cas, on ne peut pas simplement exécuter le code pour vérifier les différences comme dans l’article.
    Modification : en lisant le fil ci-dessous, il semble que c’était un problème d’encodage. Cela dit, l’exactitude m’inquiète encore un peu.

    • Pour le renommage réel des noms, il faut utiliser un autre outil comme HumanifyJS : https://github.com/jehna/humanify
    • Le code déminifié semble assez proche de l’original.
      Dans certains cas, ChatGPT a même effectué son propre refactoring en plus de la déminification.
      Le code original n’appelle pas immédiatement handleResize : il contient son corps en ligne. Le minifieur a probablement réellement procédé à une inlining. La seule vraie différence ici est l’absence de la condition if (typeof window < "u").
    • Cela signifie que la version générée par ChatGPT a omis certains caractères utilisés dans l’exemple original.
      Plus précisément, il s’agit de caractères de bloc, mais HN ne semble pas permettre de coller des caractères Unicode. Ils sont visibles dans la version originale, mais pas dans la version générée par ChatGPT.
      Cela dit, il est tout à fait possible aussi que je n’aie pas fourni tout le contexte nécessaire.
      Dévaloriser toute la sortie parce qu’il manque quelques caractères relève presque du pinaillage excessif. À part cela, la sortie me semble identique.
    • En plus, il lui a aussi demandé de réimplémenter le tout de JavaScript en TypeScript.
      S’il lui avait seulement demandé de renommer les variables et les méthodes, le résultat aurait probablement été plus proche de l’original.