TL;DR
- Les attaquants obfusquent pour échapper aux scanners de signatures et cacher leur intention, pas pour rendre le code plus rapide.
- Les couches habituelles :
base64,gzinflate/gzuncompress,str_rot13, échappements hex/\x, tableaux de codes ASCII, variables-variables$$x. - Au bout du tunnel, un sink d'exécution :
eval,assert,create_functionoupreg_replaceavec le modificateur/e. - Pour déobfusquer : on n'exécute jamais. On remplace le sink par un
echopour dumper la couche suivante, ou on décode à la main dans un bac à sable. - Le payload final, c'est presque toujours : un web shell, un mailer de spam, un uploader, un réinfecteur, ou un voleur d'identifiants.
Pourquoi obfusquer ? Pour passer sous le radar
Une backdoor en clair, c'est trois lignes : prendre un paramètre dans la requête, le passer à system(), renvoyer la sortie. Lisible, évident, et détecté en une seconde par n'importe quel scanner. L'obfuscation ne sert pas à protéger une propriété intellectuelle ; elle a deux objectifs très précis.
- Échapper aux signatures. Les scanners (et les antivirus serveur) cherchent des motifs connus : un
eval($_POST[...])brut saute aux yeux. Empilé sous trois couches d'encodage, le même code devient une chaîne unique qui ne ressemble à rien de répertorié. - Cacher l'intention. Même un humain qui tombe dessus ne voit pas immédiatement « ah, ça envoie du spam » ou « ça ouvre un shell ». Le temps d'analyse joue pour l'attaquant : plus c'est pénible à lire, plus la backdoor survit longtemps.
L'obfuscation n'est pas du chiffrement. C'est un déguisement. Et un déguisement, par définition, se retire, il suffit de savoir où sont les coutures.
Les couches, du plus simple au plus retors
Un malware obfusqué est un oignon : chaque couche, une fois retirée, en révèle une autre, jusqu'au code réel au centre. Voici les techniques qu'on croise, dans l'ordre de fréquence.
- Encodage base64. La couche reine. Une chaîne de A-Z, a-z, 0-9,
+/=qu'on décode avecbase64_decode(). Visuellement reconnaissable au premier coup d'œil. - Compression.
gzinflate()ougzuncompress(): le code est compressé puis encodé, ce qui réduit la taille et casse encore plus les signatures. La séquencegzinflate(base64_decode(...))est un classique absolu. - Rotation
str_rot13. Un décalage de 13 lettres. Trivial, mais ça suffit à brouiller un scan naïf et à masquer le motevallui-même. - Échappements hexadécimaux. Les fonctions écrites en
\x65\x76\x61\x6cau lieu deeval: PHP interprète les\xdans les chaînes à double quote, donc le nom de fonction n'apparaît jamais en clair. - Tableaux de codes ASCII. Le nom de fonction reconstruit caractère par caractère via
chr(101).chr(118).chr(97).chr(108), encore une façon d'épelerevalsans l'écrire. - Variables-variables
$$x. On stocke un nom de fonction dans une variable, puis on l'appelle indirectement.$f = 'system'; $f($cmd);, le grep sursystemne trouve rien d'appelé.
// ⚠️ illustratif et NON exécutable, placeholders, pas un vrai payload
// ce à quoi ressemble la couche externe typique :
eval(gzinflate(base64_decode("…REDACTED_BASE64_BLOB…")));
// la même idée, en cachant le mot "eval" lui-même :
$fn = "\x65\x76\x61\x6c"; // = "eval", jamais écrit en clair
$fn(gzinflate(base64_decode("…REDACTED…")));
// reconstruction par codes ASCII :
$fn = chr(101).chr(118).chr(97).chr(108); // = "eval"
$fn(str_rot13("…REDACTED_ROT13…"));
eval(gzinflate(base64_decode(X))) se lit de l'intérieur vers l'extérieur : d'abord on décode le base64, puis on décompresse, et le sink eval est la dernière opération, celle qu'on ne laissera jamais s'exécuter.Le sink : là où le code se transforme en action
Toutes ces couches ne servent à rien tant qu'il n'y a pas une fonction qui transforme une chaîne en code exécuté. C'est le point d'exécution, le sink, et c'est exactement là qu'il faut couper. PHP en offre plusieurs.
eval(), le grand classique : exécute une chaîne comme du PHP. Direct, brutal, omniprésent.assert(), dans les vieilles versions,assertévaluait une chaîne comme du code. Unevaldéguisé en fonction de test anodine.create_function(), créait une fonction à partir d'une chaîne de corps. Supprimée en PHP 8, mais encore présente dans d'innombrables malwares hérités.preg_replace()avec le modificateur/e, le plus sournois : ce flag exécutait le motif de remplacement comme du PHP. Retiré en PHP 7, longtemps le sink favori car invisible pour qui cherchaiteval.
La règle de diagnostic est simple : trouve le sink, et tu as trouvé le cœur. Tout le reste n'est qu'emballage. Une fois le sink localisé, on sait précisément quelle est la dernière opération avant l'exécution, et donc où l'intercepter.
Déobfusquer sans exécuter : la règle d'or
Voici l'erreur fatale, celle que je vois faire à des gens pressés : copier le fichier sur un serveur de test et le lancer « juste pour voir ce que ça fait ». Non. On n'exécute jamais du code inconnu pour le comprendre. Le payload peut se réinfecter, contacter un C2, supprimer des preuves, ou se déclencher différemment selon l'environnement. L'exécuter, c'est laisser l'attaquant écrire la fin de l'histoire.
La bonne méthode transforme le sink en observateur. On remplace l'exécution par un affichage : au lieu de lancer la couche suivante, on l'imprime.
// ⚠️ technique d'analyse statique, illustratif, pas un vrai payload
// AVANT (ce que l'attaquant a écrit) :
// eval(gzinflate(base64_decode("…REDACTED…")));
// APRÈS (ce qu'on écrit pour analyser, dans un bac à sable isolé) :
echo gzinflate(base64_decode("…REDACTED…"));
// ^^^^ on remplace le SINK par echo : on DUMPE la couche suivante,
// on ne l'exécute pas. Le code décodé s'affiche, inerte.
On lit la sortie. Si c'est encore de l'obfusqué, une nouvelle chaîne base64, un nouveau eval, on recommence : on remplace ce nouveau sink par un echo, et on dumpe la couche d'après. On répète jusqu'à ce que le vrai source apparaisse, lisible. C'est exactement le pelage de l'oignon, manuel et contrôlé.
Variante encore plus sûre : ne pas exécuter de PHP du tout. On décode les couches à la main, dans un environnement isolé, base64_decode → inflate → rot13, avec des outils qui ne font que transformer des octets, jamais les interpréter. Un peu de Python, un peu de CyberChef hors-ligne, et on déroule. Plus lent, mais zéro risque d'exécution accidentelle.
echo mal placé peut déclencher quelque chose si le sink est plus malin qu'attendu. On isole d'abord, on décode ensuite. L'isolation n'est pas une précaution optionnelle, c'est la première étape.Ce qu'on trouve au centre de l'oignon
Une fois toutes les couches retirées, le payload réel se révèle. Dans la quasi-totalité des cas que j'ai disséqués, c'est l'une de ces cinq familles, et savoir laquelle, c'est savoir ce que l'attaquant cherchait.
- Un web shell / gestionnaire de fichiers. Une interface complète pour parcourir, lire, écrire et supprimer des fichiers, lancer des commandes système. Le couteau suisse du contrôle à distance.
- Un mailer (spam). Le serveur compromis devient une rampe d'envoi de masse. Souvent le premier signe visible : la réputation IP qui s'effondre, les emails légitimes qui partent en spam.
- Un uploader. Un mini-formulaire qui permet de déposer de nouveaux fichiers sur le serveur, typiquement pour installer la phase 2 une fois la tête de pont établie.
- Un réinfecteur. Le plus vicieux : du code qui se réécrit ailleurs, recrée les fichiers qu'on supprime, ou réinjecte la backdoor dans les fichiers cœur à chaque chargement. C'est pour ça qu'un nettoyage partiel échoue toujours.
- Un voleur d'identifiants. Il intercepte les soumissions de formulaires de connexion, les identifiants de base de données dans
wp-config.php, ou les clés d'API, et les exfiltre.
Les tells : repérer la bête avant même de l'ouvrir
Avant la dissection, il y a la détection. Certains signaux trahissent une backdoor obfusquée à l'œil nu, sans même décoder quoi que ce soit :
- Un fichier PHP d'une seule ligne, très longue. Le code légitime est aéré, indenté. Une ligne de 40 000 caractères, c'est un drapeau rouge immédiat.
- Des noms de variables en charabia.
$O0O0O,$_,$GLOBALS['x7f'], le code humain nomme ses variables ; le code généré pour cacher ne le fait pas. - Un
.phpdans un dossier d'upload.wp-content/uploadsne devrait contenir que des médias. Un fichier exécutable là-dedans n'a aucune raison légitime d'exister. - Un fichier cœur récemment modifié. Un
index.phpou unwp-load.phpdont la date de modification ne colle pas avec celle de l'installation : le réinfecteur est passé par là.
base64_decode, gzinflate, str_rot13, eval, assert) remonte la majorité des backdoors en quelques secondes. C'est le premier balayage que je lance sur tout site suspecté d'être compromis.La mentalité Fixer : observer, isoler, décoder, comprendre
Ce travail de dissection, c'est exactement la même méthode que je décris dans mon article sur le debug de hardware vintage : face à un système non documenté et hostile, on ne devine pas, on ne bricole pas au hasard. On observe ce qu'il fait, on l'isole pour le manipuler sans risque, on décode son fonctionnement couche par couche, et on remonte à la cause racine.
Une carte électronique des années 80 sans schéma et une backdoor obfusquée posent le même problème cognitif : un comportement opaque qu'il faut rendre intelligible. La discipline est identique. On ne se contente pas de « ça remarche » ou « j'ai supprimé le fichier », on veut comprendre pourquoi, parce que c'est la seule façon de savoir si on a vraiment réglé le problème ou juste masqué un symptôme.
C'est aussi la suite logique de mon article sur les backdoors qui échappent aux scanners : là, je montrais pourquoi un scan automatique passe à côté ; ici, je montre comment on prend le relais à la main quand le scan a échoué.
Ce que je retiens
- L'obfuscation est réversible. Avec de la patience et la bonne méthode, chaque couche se retire. Ce n'est pas du chiffrement, il n'y a pas de clé manquante, juste des déguisements empilés.
- Comprendre le payload, c'est cartographier les dégâts. Un mailer signale du spam à couper ; un voleur d'identifiants signale des mots de passe à changer en urgence ; un réinfecteur signale qu'un nettoyage partiel ne suffira pas. Le payload dit ce qui a été compromis.
- On n'exécute jamais du code inconnu pour « voir ce qu'il fait ». On remplace le sink par un
echo, on décode en bac à sable isolé, on lit. L'exécution est la seule erreur irréversible de toute l'analyse. - Trouve le sink, trouve le cœur. Tout le reste est emballage. La fonction d'exécution finale est à la fois la cible de l'analyse et le point de coupe.
Décortiquer un malware, ce n'est pas de la paranoïa : c'est la seule façon de transformer un incident en connaissance. On sort de chaque dissection en sachant exactement ce qui s'est passé, et un système qu'on comprend est un système qu'on peut vraiment défendre.
Un site WordPress qui se comporte bizarrement ?
Spam qui part tout seul, fichiers qui réapparaissent, réputation IP en chute libre : ce sont les signes d'une backdoor active. Sur wp-pirate.fr, je dissèque, je nettoie en profondeur et je verrouille, sans me contenter de supprimer le symptôme.
Faire auditer mon site ↗