Comment désobfusquer les scripts PowerShell

Désobfusquer un script PowerShell, c’est défaire, couche par couche, les transformations qu’un attaquant a appliquées pour cacher ce que le script fait réellement — et le faire sans jamais exécuter le script. Une ligne suspecte qui arrive comme un mur de Base64, ou comme un enchevêtrement de codes de caractères et de concaténations de chaînes, est le plus souvent en dessous une commande PowerShell parfaitement ordinaire : un téléchargement, une charge utile décodée, une commande qui s’exécute en mémoire et ne touche jamais le disque. L’obfuscation n’est pas là pour être ingénieuse. Elle est là pour passer les scanners de signatures, rendre la ligne de commande illisible au premier coup d’œil et faire abandonner l’analyste humain avant la partie intéressante. Cet article explique pourquoi chaque couche existe, comment la retirer comme du texte inerte et ce que signifient réellement les morceaux trouvés. Vous pouvez parcourir tout le flux dans le désobfuscateur PowerShell, qui décode couche par couche entièrement dans votre navigateur — rien n’est exécuté et rien ne quitte votre machine.

Pourquoi les attaquants obfusquent PowerShell — et pourquoi la forme encodée est la première chose à retirer

PowerShell est intégré à toutes les machines Windows, il peut télécharger et exécuter du code depuis internet sans écrire de fichier sur le disque, et il s’exécute avec les privilèges de celui qui l’a lancé. Cette combinaison explique pourquoi c’est l’un des outils les plus détournés du cadre MITRE ATT&CK, catalogué comme technique T1059.001, et pourquoi la première chose qu’un attaquant fait avec une commande PowerShell malveillante est de la cacher. Une commande en clair comme DownloadString suivie de Invoke-Expression est trivialement détectable : n’importe quel scanner, n’importe quel examen de journal, n’importe quel analyste jetant un œil à la ligne de commande du processus la repérera. Encodée, compressée et découpée en morceaux, la même commande devient une chaîne de caractères qui ne correspond à aucune signature connue et ne signifie rien pour un lecteur humain.

C’est aussi pourquoi la forme encodée est la première chose à retirer, avant tout le reste. Tant que vous ne l’avez pas décodée, vous ne savez pas si vous regardez un installateur, une blague ou un voleur d’identifiants — et tout autre jugement n’est qu’une supposition. La bonne nouvelle, c’est que l’obfuscation est presque toujours réversible par déroulage statique : la charge utile doit pouvoir être décodée par la machine qui l’exécute, donc chaque couche a un inverse déterministe. Base64 se décode, gzip se décompresse, les codes de caractères redeviennent des lettres, les chaînes concaténées se rejoignent. Le travail est mécanique, pas mystérieux, et c’est l’étape la plus rentable du tri d’un script suspect : un seul décodage transforme un « blob illisible » en une commande lisible sur laquelle on peut vraiment raisonner.

-EncodedCommand et UTF-16LE : pourquoi le Base64 qui ressemble à du charabia est le plus souvent du texte lisible

La forme la plus courante d’obfuscation PowerShell que vous rencontrerez est un seul commutateur : powershell.exe -EncodedCommand suivi d’une longue chaîne Base64. Elle ressemble à des caractères aléatoires, mais c’est presque toujours simplement du texte de script ordinaire, encodé d’une façon qui surprend la première fois qu’on la voit. Le -EncodedCommand de PowerShell attend que le Base64 se décode en UTF-16LE — deux octets par caractère, avec un octet zéro après chaque lettre ASCII — parce que c’est l’encodage des chaînes .NET sous Windows. Ainsi, le Base64 de la commande inoffensive Write-Host "hello" commence par SQB3AHIAaQB0AGUALQBIAG8AcwB0ACA… et le motif révélateur est la répétition des caractères « A » qui proviennent de tous ces octets zéro.

Savoir cela change la façon de lire un blob encodé. Une chaîne Base64 qui se décode en charabia UTF-8 n’est probablement pas du tout de l’UTF-8 : essayez d’abord UTF-16LE, et le « charabia » se résout en PowerShell lisible. Il en va de même pour un blob Base64 nu sans commutateur -EncodedCommand en vue — les longs jetons de Base64 valide qui se décodent en texte imprimable sont la même astuce sans le drapeau. La règle pratique est simple : quand vous voyez un long jeton Base64 dans une ligne de commande PowerShell, supposez que c’est un script jusqu’à ce que le décodage prouve le contraire, et décodez-le de façon inerte — comme du texte, dans un outil qui ne l’exécute jamais — plutôt que de le coller dans une fenêtre de chat ou un terminal pour voir ce qui se passe.

Le millefeuille : compression, codes de caractères, concaténation, -f, accents graves

Les charges utiles réelles s’arrêtent rarement à une couche. Une fois le -EncodedCommand décodé, le script en dessous est souvent lui-même encodé à nouveau : un blob compressé en gzip enveloppé dans une autre chaîne Base64, ou une séquence de codes de caractères comme [char]68+[char]105 qui épelle une commande chiffre par chiffre, ou une chaîne découpée en fragments et recollée avec des signes plus, ou une chaîne de format dont l’opérateur -f remplit les morceaux à l’exécution, ou des accents graves parsemés dans les mots-clés pour que Do`wnloadString ne corresponde plus à une recherche naïve de DownloadString. Chaque couche existe pour une raison différente. La compression réduit une longue charge utile en une courte ligne et défait entièrement la recherche de texte. Les codes de caractères et la concaténation défont la correspondance de signatures en garantissant que les mots suspects n’apparaissent jamais littéralement dans le fichier. L’opérateur -f et les accents graves défont les simples recherches de chaînes tout en gardant le script syntaxiquement valide.

La défense contre toutes est la même : dérouler une couche à la fois, dans l’ordre, et s’arrêter quand plus rien ne s’applique. Décodez le Base64, puis vérifiez si le résultat est compressé et décompressez-le, puis résolvez les codes de caractères, joignez les fragments, substituez les arguments de format et retirez les accents graves — chaque étape produit du texte brut sur lequel l’étape suivante peut travailler. Un outil qui fait cela sous forme de chronologie visible, une carte par couche avec l’avant et l’après, vaut plus qu’un outil qui émet silencieusement une réponse finale, parce que la chronologie est elle-même une preuve : elle montre quelles couches étaient présentes, et quand le déroulage s’arrête tôt — « impossible de dérouler davantage » — c’est une découverte, pas un échec. Cela vous dit que la charge contient quelque chose de dynamique, de chiffré ou de construit à l’exécution que l’analyse statique seule ne peut pas atteindre.

Journalisation des blocs de script 4104 : comment les longs scripts sont découpés puis réassemblés

Il existe une source de PowerShell désobfusqué qui ne demande aucun décodage : la machine elle-même. Quand la journalisation des blocs de script est activée, Windows enregistre le contenu de chaque bloc de script traité par PowerShell comme événement 4104 dans le journal Microsoft-Windows-PowerShell/Operational — et il enregistre le code tel que PowerShell l’a vu, après que le moteur a déjà développé le -EncodedCommand et les couches d’obfuscation. Une ligne suspecte arrivée en Base64 apparaît dans l’événement 4104 comme le script lisible qu’elle a réellement exécuté. C’est pourquoi cette fonction est le réglage de journalisation PowerShell le plus précieux qu’un défenseur puisse activer, et pourquoi elle est désactivée par défaut : elle est verbeuse, et les journaux peuvent contenir des identifiants et d’autres données sensibles, donc Microsoft recommande la journalisation d’événements protégée en complément pour tout usage au-delà du diagnostic.

Le piège, c’est que Windows découpe les longs blocs de script en plusieurs événements 4104. Chaque événement porte le même ScriptBlockId, un MessageNumber et un MessageTotal — la partie 2 sur 5, par exemple — et le script complet n’existe que lorsque les parties sont regroupées par cet ID, triées par numéro de message et concaténées. Un seul événement isolé peut s’arrêter au milieu d’un mot, et une recherche qui ne lit que le premier fragment manquera ce que la charge fait à la fin. Le réassemblage est donc une étape mécanique avec une seule chose à vérifier : s’il manque une partie. Si le journal dit 2 sur 3 et que la partie 2 n’est jamais arrivée — parce que le journal a tourné ou a été effacé — le texte réassemblé est incomplet, et toute conclusion tirée de lui est provisoire. La même logique de regroupement est celle qu’utilise la visionneuse EVTX quand elle signale les événements 4104 dans les fichiers .evtx exportés ; voir aussi l’article complémentaire Lire les fichiers .evtx sans machine Windows.

Lire les indicateurs : berceaux de téléchargement, contournement d’AMSI, contournement de la politique d’exécution

Une fois le script lisible, l’étape suivante n’est pas de le déclarer malveillant — c’est de lire les indicateurs qu’il contient comme des pistes à vérifier. Un berceau de téléchargement — DownloadString, DownloadFile, Net.WebClient, Invoke-WebRequest, bitsadmin, certutil récupérant un fichier depuis une URL — vous dit que le script va chercher du code quelque part, et la question suivante est où, et si cette destination est attendue sur votre réseau. Un contournement d’AMSI — amsiInitFailed, AmsiUtils, ou du code qui corrige l’interface d’analyse antimalware — vous dit que l’auteur voulait que le script s’exécute sans être analysé, ce qui est un signal fort mais pas un verdict : les pentesters et certains outils légitimes font la même chose. Un contournement de la politique d’exécution (-ExecutionPolicy Bypass, -ep bypass) vous dit que l’auteur ne voulait pas que les protections propres à PowerShell arrêtent le script, ce qui est courant aussi bien dans les malwares que dans l’automatisation ordinaire qui exécute des scripts non signés.

La discipline consiste à traiter chaque indicateur comme une question, pas comme une réponse. Un berceau de téléchargement plus une fenêtre masquée (-WindowStyle Hidden) plus une commande encodée est un motif qui mérite d’être escaladé ; chacun d’eux pris isolément est un motif qui mérite d’être vérifié. Ce qui transforme une piste en verdict, c’est le contexte : qui a lancé le processus, quel était son processus parent, si l’URL contactée est connue, si le même script apparaît sur d’autres machines. Un désobfuscateur qui énumère les indicateurs avec le fragment correspondant et une note « vérifier dans le contexte » fait le travail honnête ; un qui tamponne un script « malveillant » ou « propre » à partir d’une liste de motifs exagère ce que l’analyse statique peut savoir. Et l’inverse compte aussi : un script sans indicateur trouvé n’est pas prouvé sûr — il utilise peut-être simplement une technique que la liste ne couvre pas, ou construit sa charge dynamiquement à l’exécution.

Le flux de travail sûr : ne jamais le coller dans le chat, ne jamais l’exécuter, décoder de façon inerte

La règle la plus importante pour manipuler un script PowerShell suspect est de ne jamais l’exécuter pour découvrir ce qu’il fait, et de ne jamais le coller dans une fenêtre de chat pour demander à une IA. L’exécuter, c’est exécuter la charge utile — c’est le résultat que vous essayez d’éviter. Le coller dans un chat envoie un script potentiellement malveillant à un service tiers, le laisse dans un historique de conversation que vous ne contrôlez pas, et vous vaut souvent un refus de toute façon, parce que les systèmes d’IA refusent couramment de désobfusquer des scripts qui pourraient être malveillants. Le flux de travail sûr est ennuyeux à dessein : copiez le blob dans un désobfuscateur local du navigateur, ou sur une machine d’analyse en bac à sable, et traitez la sortie décodée comme du texte inerte à lire, jamais comme du code à exécuter.

À partir de là, le flux est une courte liste de contrôle. Premièrement, décodez couche par couche jusqu’à ce que le texte cesse de changer, et notez où le déroulage s’est arrêté. Deuxièmement, lisez les indicateurs et notez ce que chacun signifierait s’il était vrai, puis vérifiez chacun par rapport au contexte — le processus parent, l’URL de destination, la machine d’où il vient. Troisièmement, remettez le résultat aux personnes ou aux systèmes conçus pour l’étape suivante : votre console EDR, une détonation en bac à sable, votre équipe de réponse à incident. Le travail d’un désobfuscateur s’arrête au texte lisible et aux indicateurs honnêtes ; le verdict appartient au bac à sable et à l’analyste, pas à l’outil de décodage. Si le script venait d’une machine que vous gérez, vérifiez si la journalisation des blocs de script était activée — si oui, les événements 4104 sont la vérité de base de ce qui s’est exécuté, et ils valent la peine d’être collectés avant que le journal ne tourne.

Sources

Questions fréquentes

Est-il sûr de désobfusquer un script PowerShell suspect ?

Oui, si le décodage est purement statique — traiter le blob comme du texte et ne jamais l’exécuter. Un désobfuscateur local du navigateur qui décode Base64, décompresse gzip, résout les codes de caractères et joint les chaînes ne fait que réarranger du texte ; rien ne s’exécute. Ce qui n’est pas sûr, c’est de coller le script dans un terminal, de double-cliquer sur un fichier .ps1, ou de le coller dans un service de chat, ce qui l’exécute ou l’envoie à un tiers. Gardez le décodage inerte et la sortie décodée comme du texte à lire.

J’ai décodé le -EncodedCommand et le résultat a toujours l’air encodé. Que faire ?

C’est normal : les vraies charges empilent les couches. Le texte décodé peut lui-même être un blob gzip enveloppé en Base64, ou une chaîne construite à partir de codes [char], de concaténation ou de l’opérateur de format -f. Déroulez une couche à la fois — décodez, puis vérifiez la compression, puis résolvez les codes de caractères et les jointures — jusqu’à ce que le texte cesse de changer. S’il cesse de changer tout en restant illisible, la charge est probablement chiffrée ou construite dynamiquement à l’exécution, et le déroulage statique a atteint sa limite : c’est une découverte à remettre à un bac à sable, pas un échec de l’outil.

Un résultat propre signifie-t-il que le script est sûr ?

Non. Un résultat propre signifie qu’aucun motif d’indicateur connu n’a correspondu et que chaque couche qui pouvait être déroulée l’a été — cela ne prouve pas que le script est inoffensif. Une charge peut être chiffrée, peut construire ses chaînes à l’exécution à partir de valeurs que seule la machine en cours d’exécution connaît, ou peut simplement utiliser une technique que la liste d’indicateurs ne couvre pas. Traitez un résultat propre comme « l’analyse statique n’a rien trouvé » et vérifiez-le dans un bac à sable ou avec votre EDR avant de décider quoi que ce soit. L’inverse vaut aussi : un seul indicateur est une piste à vérifier, pas un verdict.