Challenge Javascript – HTMLProtector

meme-refactor

Nous allons voir ici la résolution d’un challenge JavaScript sensé protéger une page par un simple mot de passe.
Nous verrons une fois de plus qu’une protection située uniquement côté utilisateur (grâce au JavaScript) n’apporte aucune sécurité et encore moins quand il s’agit d’HTMLProtector. :D

Présentation du challenge

La page est disponible sur  le dépôt dédié de 0x0ff.info.
Elle présente un simple champ permettant d’entrer un mot de passe et un bouton pour valider.
En dessous, un lien promeut probablement le site de l’auteur mais le domaine est inaccessible pour le moment (l’IP 66.39.109.24 ne répond pas à son port 80 au moment de la rédaction de l’article) donc aucune information de ce côté.
Un affichage du code source de la page n’apporte pas grand chose, sauf si vous parlez le Brainfuck couramment ?
Pour finir, si vous êtes aussi bon que moi en recherche Google, vous n’obtiendrez probablement aucun résultat …

C’est parti, allez chercher la tronçonneuse et voyons comment débroussailler tout ça !

Phase n°1 : Dé-offuscation

Sous cette appellation barbare, nous allons tenter de rendre le code un peu plus parlant.
Nous allons tout d’abord retrouver un code lisible de manière manuelle afin de bien comprendre les différentes opérations d’offuscation qui ont été utilisés puis nous verrons comment le faire beaucoup plus rapidement à l’aide du formidable outil qu’est notre navigateur !

À la main

D’après le code source de la page, il n’y a aucun code HTML et uniquement 3 fonctions JavaScript appelées :

document.write(unescape("%3C%53[…]");
hp_d01(unescape("[…]");
hp_d01(unescape("[…]");

A chaque fois, l’argument de la fonction est caché par du contenu “échappé” c’est à dire, rendu illisible car remplacé par des caractères différents. (plus d’info sur le site w3scools)
Un interpréteur de JavaScript en ligne nous permet de dé-échapper ce contenu pour le rendre plus lisible.
En entrant par exemple la chaine suivante sur ce site, nous obtenons un code presque propre :

writeln("

<pre>"+unescape("%3C%53[…]%54%3E")+"</pre>

");

Un petit nettoyage du résultat avec jsbeautifier.org nous donne un code lavé plus blanc que blanc :

hp_ok = true;

function hp_d01(s) {
    if (!hp_ok) return;
    var o = "",
        ar = new Array(),
        os = "",
        ic = 0;
    for (i = 0; i < s.length; i++) {
        c = s.charCodeAt(i);
        if (c < 128) c = c ^ 2; os += String.fromCharCode(c); if (os.length > 80) {
            ar[ic++] = os;
            os = ""
        }
    }
    o = ar.join("") + os;
    document.write(o)
}

Ce qui nous permet de découvrir la fonction hp_d01 qui est utilisé dans les deux autres lignes de JavaScript.
Cette fonction semble déchiffrer l’argument qui lui ai donné avant d’écrire un résultat difficile à prédire directement dans le code de la page. Cela peut être du HTML, du JavaScript, ou n’importe quel autre langage qui pourra ensuite être interprété par le navigateur.

À partir de là, il vous sera plus simple d’enregistrer la page en local sur votre machine pour y faire des modifications (n’ayez crainte, elle ne mord pas). Il sera ensuite possible de remplacer la dernière ligne (document.write) par le code que nous venons d’éclaircir afin d’y voir plus clair :)

Pour découvrir les deux dernières lignes mystérieuses, nous allons arranger la fonction hp_d01 pour afficher le résultat directement, sans avoir à se faire des nœuds au cerveau pour découvrir son fonctionnement interne. Pour cela, nous remplaçons la dernière ligne de la fonction (le document.write) par un simple alert :)

alert(o);

On voit bien là l’intérêt de pouvoir travailler le code du côté client et le problème de sécurité que cela pose pour une page comme celle-ci !
Un appel à la page à partir de notre navigateur et nous voilà avec le contenu de la page telle qu’elle est rendu par le navigateur :

<!--HEAD-->
<script language="JavaScript">
    function Kod(s, pass) {
        var i = 0;
        var BlaBla = "";
        for (j = 0; j < s.length; j++) { BlaBla += String.fromCharCode((pass.charCodeAt(i++)) ^ (s.charCodeAt(j))); if (i >= pass.length) i = 0;
        }
        return (BlaBla);
    }

function f(form)
{
    var pass = document.form.pass.value;
    var hash = 0;
    for (j = 0; j < pass.length; j++) {
        var n = pass.charCodeAt(j);
        hash += ((n - j + 33) ^ 31025);
    }
    if (hash == 124456) {
        var Secret = "" + "\x74\x5c\x17\x06\x24\x0a\x34\x0e\x24\x58\x43\x0f\x27\x5a\x06\x4a\x68\x7d\x44\x06\x68\x57\x16\x19\x21\x5b\x16\x18\x68\x40\x0c\x4b\x23\x5a\x0c\x1c\x68\x5c\x0c\x1c\x68\x4d\x0c\x1e\x6f\x42\x06\x4b\x3b\x5b\x0f\x1d\x2d\x50\x43\x02\x3c\x1a\x43\x21\x3d\x47\x17\x4b\x24\x51\x17\x4b\x25\x51\x43\x0a\x68\x45\x16\x02\x2b\x5f\x43\x0e\x25\x55\x0a\x07\x66\x14\x22\x03\x64\x14\x02\x05\x2c\x14\x2a\x4b\x2f\x41\x06\x18\x3b\x14\x1a\x04\x3d\x13\x11\x0e\x68\x5c\x06\x19\x2d\x14\x05\x04\x3a\x14\x17\x03\x2d\x14\x08\x0e\x31\x1a\x4d\x45\x68\x7c\x06\x19\x2d\x14\x0a\x1f\x68\x5d\x10\x51\x68\x51\x56\x58\x71\x0d\x51\x09\x2a\x01\x05\x5c\x29\x52\x55\x0d\x7d\x05\x54\x5c\x7b\x55\x07\x53\x2d\x55\x50\x5b\x7b\x07\x07\x5d\x7e\x1a\x43\x22\x2e\x14\x1a\x04\x3d\x14\x14\x04\x26\x50\x06\x19\x68\x43\x0b\x0a\x3c\x14\x0a\x18\x68\x5d\x17\x45\x66\x1a\x43\x1c\x2d\x58\x0f\x47\x68\x5e\x16\x18\x3c\x14\x02\x05\x68\x79\x27\x5e\x68\x5c\x02\x18\x20\x14\x0c\x0d\x68\x16\x14\x0e\x24\x58\x43\x0f\x27\x5a\x06\x49\x66\x14\x2b\x04\x38\x51\x43\x12\x27\x41\x43\x03\x29\x50\x43\x0d\x3d\x5a\x4d\x4b\x1a\x51\x0d\x0a\x3d\x50\x4d\x57\x67\x5c\x17\x06\x24\x0a" + "";
        var s = Kod(Secret, pass);
        document.write(s);
    } else {
        alert('Wrong password!');
    }
}
</script>

<center>


<form name="form" method="post" action="">
<b>Enter password:</b >
<input type = "password" name = "pass" size = "30" maxlength = "30" value = "" >
<input type = "button" value = " Go! " onClick = "f(this.form)" >
</form>


</center >
    <!--/HEAD-->
    <!--BODY-->

<table width="100%" border="0">

<tr bgcolor="#445577" align="center">

<td><a href="http://www.antssoft.com/index.htm?ref=htmlprotector"><font face="Arial, Helvetica, sans-serif" color="#FFFFFF" size="-1">This webpage was protected by HTMLProtector</font></a></td>

</tr>

</table>


<!--/BODY-->

Le code est maintenant totalement lisible et nous pouvons passer à la phase 2 : Analyse !

Depuis le navigateur

L’extension “JavaScript Deobfuscator” pour Firefox permet d’afficher le code directement issue du moteur JavaScript. Les performances s’en trouve légèrement affectées mais cela permet d’avoir un code directement clair. On peut donc le récupérer en ouvrant la console dans “Options” -> “Développement Web” -> “JavaScript Deobfuscator”. La console affiche tous les codes exécutés en temps réel par défaut donc c’est peut-être le moment de faire du ménage dans vos 374 onglets ouverts bande de sagouins !

Une fois de plus, un coup de jsbeautifier.org ne peut pas faire de mal. La console de développement web de base permet de voir également le code HTML mais nous n’en aurons pas besoin ici.

Phase n°2 : Analyse

Après un bon coup de ménage, vous obtiendrez peut-être un résultat qui tentera de se rapprocher de cet excellent rendu ;)

Dans le formulaire, nous voyons que le point d’entrée est la fonction f qui prendra comme argument le présent formulaire :

<input type="button" value=" Go! " onClick="f(this.form)">

Penchons-nous donc sur cette fameuse fonction habillement nommée “f” :

function f(form) {
	var pass=document.form.pass.value;
	var hash=0;
	for(j=0; j&lt;pass.length; j++){
		var n= pass.charCodeAt(j);
		hash += ((n-j+33)^31025);
	}
	if (hash == 124456) {
		var Secret =""+"\x74\x5c[…]\x24\x0a"+"";
		var s=Kod(Secret, pass);
		document.write (s);
	} else {
		alert ('Wrong password!');
	}
}

Elle semble calculer un “hash” à partir des codes ASCII de chaque caractère entré par l’utilisateur. Si ce hash est égal à 124456, alors la fonction “Kod” est appelée avec le password de l’utilisateur ainsi qu’une chaine hexadécimale (Secret). Sinon, un bref “Wrong password!” viendra conclure cette tentative infructueuse.

Jetons maintenant un œil sur ce “Kod” :

function Kod(s, pass) {
	var i=0; var BlaBla="";
	for(j=0; j&lt;s.length; j++) { BlaBla+=String.fromCharCode((pass.charCodeAt(i++))^(s.charCodeAt(j))); if (i&gt;=pass.length) i=0;
	}
	return(BlaBla);
}

Cette dernière réalise un simple XOR sur les caractères ASCII du password et du secret pour afficher un résultat que l’on imagine clair (comprenez lisible par un humain).

Cette fonction Kod ne nous apprend donc rien à propos du mot de passe attendu. Heureusement, la fonction f vole à notre secours !

En effet, le calcul du hash peut nous donner une information quant à la longueur du mot de passe attendu. En supposant que les caractères utilisés appartiennent à la table ASCII (man ascii), nous pouvons en déduire que chaque tour dans la boucle augmentera le hash d’une valeur comprise entre 30976 et 31167. Ces valeurs sorties du chapeau ne vous disent rien ? Laissez moi vous éclairer.

Regardez bien le calcul du hash :

hash += ((n-j+33)^31025);

Chaque tour dans la boucle augmente le hash d’une valeur proche de 31025. Plus précisément, le hash sera augmenté d’une valeur que nous pouvons légèrement influer car nous maitrisons n (qui est le code décimal du caractère du mot de passe). Il s’ajoutera ensuite 33 moins le numéro du tour de la boucle (premier tour j = 0, deuxième tour j = 1, etc.). Concrètement nous pouvons faire varier n de 0 à 127, ce qui implique que nous pourrons faire varier la partie de droite (n-j+33) entre 0+33 et 127+33 = 160 pour le premier tour, entre 32 et 159 pour le deuxième tour, etc. Ensuite, ce chiffre que nous aurons habillement choisi sera utilisé pour faire un OU exclusif bit à bit (XOR) avec une valeur fixe : 31025. Il va donc falloir se pencher sur les représentations binaires de ces nombres.

31025 = 111100100110001
  160 =        10100000

Nous voyons que nous pouvons influer jusqu’au huitième bit (tiens, ça ferait un bon titre de livre ça !) ce qui permet de faire varier le résultat entre :

31025 = 111100100110001
   49 =        00110001
  XOR = 111100100000000

30976 pour le minimum et :

31025 = 111100100110001
  142 =        10001110
  XOR = 111100110111111

31167 pour le maximum :)

Ensuite, si le hash doit être égal à 124456, alors le mot de passe a forcément 4 caractères ! Avec 3 caractères on arrive à un hash <= 93501 (31167 * 3) tandis qu’avec 5 caractères, on ne peut pas faire moins que 154880 (30976 * 5). Pour vérifier notre théorie, nous pouvons calculer qu’un password de 4 caractères donnera un hash compris entre 123904 et 124668 : le hash (124456) est en plein milieu de cet ensemble !

J’ai essayé de faire aussi didactique que possible mais si vous n’avez pas compris un point n’hésitez pas à le préciser et les petits lutins magiques rajouteront une sous partie pour les noobs de votre espèce ^^

Nous avons déjà la longueur du mot de passe rien qu’en lisant la fonction qui calcule le hash. Malheureusement, je n’ai pas pu aller plus loin de manière “propre” :( J’ai bien tenté une attaque par analyse fréquentielle comme ce qui est utilisé contre un chiffre de Vigénère (très bien expliqué sur Wikipedia) mais ce n’est guère facile en raison du XOR qui se situe dans la fonction de déchiffrement. Tout ceci pour dire que je n’ai réussi qu’à trouver le mot de passe à l’aide d’un sale brute force :(

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import itertools

chiffre = "\x74\x5c\x17\x06\x24\x0a\x34\x0e\x24\x58\x43\x0f\x27\x5a\x06\x4a\x68\x7d\x44\x06\x68\x57\x16\x19\x21\x5b\x16\x18\x68\x40\x0c\x4b\x23\x5a\x0c\x1c\x68\x5c\x0c\x1c\x68\x4d\x0c\x1e\x6f\x42\x06\x4b\x3b\x5b\x0f\x1d\x2d\x50\x43\x02\x3c\x1a\x43\x21\x3d\x47\x17\x4b\x24\x51\x17\x4b\x25\x51\x43\x0a\x68\x45\x16\x02\x2b\x5f\x43\x0e\x25\x55\x0a\x07\x66\x14\x22\x03\x64\x14\x02\x05\x2c\x14\x2a\x4b\x2f\x41\x06\x18\x3b\x14\x1a\x04\x3d\x13\x11\x0e\x68\x5c\x06\x19\x2d\x14\x05\x04\x3a\x14\x17\x03\x2d\x14\x08\x0e\x31\x1a\x4d\x45\x68\x7c\x06\x19\x2d\x14\x0a\x1f\x68\x5d\x10\x51\x68\x51\x56\x58\x71\x0d\x51\x09\x2a\x01\x05\x5c\x29\x52\x55\x0d\x7d\x05\x54\x5c\x7b\x55\x07\x53\x2d\x55\x50\x5b\x7b\x07\x07\x5d\x7e\x1a\x43\x22\x2e\x14\x1a\x04\x3d\x14\x14\x04\x26\x50\x06\x19\x68\x43\x0b\x0a\x3c\x14\x0a\x18\x68\x5d\x17\x45\x66\x1a\x43\x1c\x2d\x58\x0f\x47\x68\x5e\x16\x18\x3c\x14\x02\x05\x68\x79\x27\x5e\x68\x5c\x02\x18\x20\x14\x0c\x0d\x68\x16\x14\x0e\x24\x58\x43\x0f\x27\x5a\x06\x49\x66\x14\x2b\x04\x38\x51\x43\x12\x27\x41\x43\x03\x29\x50\x43\x0d\x3d\x5a\x4d\x4b\x1a\x51\x0d\x0a\x3d\x50\x4d\x57\x67\x5c\x17\x06\x24\x0a"

def createPasswordGenerator():
	alphabet, length = '', 4
	for i in range(128):
		alphabet += chr(i)
	res = itertools.permutations(alphabet, length)
	for i in res:
		yield ''.join(i)

def verifyHash(x):
	# avant tout, si la clé ne fait pas 4 char on /quit
	if len(x) != 4:
		return False
	# ensuite on calcule le hash
	h = 0
	for i in range(len(x)):
		h += ((ord(x[i]) - i + 33)^31025)
	return h == 124456

def dechiffre(cle):
	"""Renvoi le déchiffré obtenu à partir de la clé envoyée en argument"""
	retour = ''
	iCle = 0
	for i in chiffre:
		retour += chr(ord(i) ^ ord(cle[iCle]))
		iCle+=1
		if iCle &gt;= len(cle):
			iCle = 0
	return retour

def clairEstClean(clair):
	"""Renvoi true si aucun caractère n'est interndit"""
	net = True
	allow = [0, 8, 9, 10, 11, 12, 13]
	deny = [i for i in range(256) if i &lt; 32 or i &gt; 128]
	for i in allow:
		deny.remove(i)
	for i in clair:
		if ord(i) in deny:
			net = False
	return net

def ratioAlpha(clair):
	"""Renvoi le coefficient de lettre min/maj et espace du message (entre 0 et 1)."""
	retour = 0
	for i in clair:
		if ord(i) == 32 or 65&lt;=ord(i)&lt;=90 or 97&lt;=ord(i)&lt;=122: retour += 1 return float(retour)/len(clair) if __name__ == '__main__': generator = createPasswordGenerator() for candidate in generator: if verifyHash(candidate): clair = dechiffre(candidate) if ratioAlpha(clair) &gt;= 0.8:
				print('Le dechiffré semble assez clair avec le mot de passe %s, le voici :' % candidate)
				print(clair)

Ce petit script python permet de retrouver le mot de passe au bout de quelques minutes mais ce n’est pas très élégant.

J’attends donc vos retours si vous pensez à une autre solution et/ou avez des remarques sur ce script codé avec les pieds !

Spread The luvz..Share on FacebookTweet about this on TwitterShare on Google+Share on TumblrShare on LinkedInPin on PinterestShare on Reddit
  • Cidrolin

    Tu aurais également pu attaquer le one time pad. “Cette fonction Kod ne nous apprend donc rien à propos du mot de passe attendu”. En es-tu sûr?

    By the way, “Nous verrons une fois de plus qu’une protection située uniquement côté
    utilisateur (grâce au JavaScript) n’apporte aucune sécurité”. Pour ma part je ne vois pas le problème, le déchiffrement d’un contenu chiffré coté client me paraît tout à fait correct.

  • yaap

    Salut Cidrolin, désolé j’avais pas vu ton commentaire avant :/

    Je ne vois pas ce que tu veux me montrer sur la fonction Kod. Qu’as-tu appris en la lisant ? Elle donne un indice de plus sur le password ?
    Et qu’entends-tu par “attaquer le one time pad” ? j’ai fait des dizaines de tests de statistiques mais impossible d’en sortir une information valable :(
    Hésite pas à expliquer ce que tu en penses, je suis intéressé par tout ce que j’ai raté !

    Effectivement tu as raison, ma phrase est un peu trop ouverte pour être exacte. J’aurais du dire quelque chose comme “la protection des données du serveur ne peut pas être réalisé en Javascript”. Je faisais plus référence aux protections sensé protéger le serveur implémentée en JS côté client…

  • Greg

    Hello,
    Le commentaire de cidrolin m’a interpelé, et effectivement, il y a un limitation à l’implémentation.
    L’OTP nécessite une clé secrète aussi longue que le message, ce qui n’est pas le cas ici.
    De plus, le code donne une autre indication dans la fonction form: le rendu de l’appel à Kod est renvoyé directement via un document.write(). Selon toute vraisemblance, c’est donc du HTML valide. On peut donc supposer que la variable Secret commence par une balise HTML, ce qui permet de connaître la version claire des 6 premiers caractères, et des 7 derniers (ce qui est beaucoup plus que nécessaire dans ce cas précis)
    Ensuite, grâce à la brillante démonstration que la clé a une longueur de 4 caractères, on a donc la clé :)

    Merci pour le challenge.