Lors du lancement de l'épreuve, on se confronte à une page nous demandant d'entrer un mot de passe de 16 caractères.
Après étude de la source, l'épreuve se décompose en 3 parties : En effet, 3 Web Workers travaillent en parallèle à la vérification du mot de passe.
Pour débugger ces Web Workers, il est possible d'utiliser le debugger de Google Chrome en posant un breakpoint au démarrage de chaque Worker.
;(function () { var messageHandler = function (e) { var c, p = e.data; if (((p.charCodeAt(0) * 42) - 38 === (Math.pow(68, 2) / 4) + 0xC0 ) && (p.charCodeAt(1) === p.charCodeAt(0) + 0x3A) && ((p.charCodeAt(2) + 0x10) === Math.sqrt(0x961)) && (Math.pow(p.charCodeAt(3), 3) / 25 === (Math.pow(p.charCodeAt(2), 3) * 2) + 0x186B) && (Math.sqrt(p.charCodeAt(4) - 2) === (p.charCodeAt(2) * 0x61) - ((0xB00B5 / 3) - (Math.pow(p.charCodeAt(3), 2) * 6) - (0xBABE * 3)))) { self.postMessage('1'); } else { self.postMessage('0'); } }; self.addEventListener('message', messageHandler, false); postMessage('Loaded'); })();
Le premier Worker va vérifier les quatre premiers caractères du mot de passe.
Il y une équation sur chaque ligne à résoudre pour chacun des caractères.
Après résolution de chacune d'elles, on obtient les caractères suivants :
![!}f
;(function () { self.console = { log: function () {} }; self.prompt = function () {}; importScripts('p.js'); Python.initialize(null, function(chr) { if (chr !== null) { postMessage(String.fromCharCode(chr)); } }); var messageHandler = function (e) { code = "def c(p = '" + e.data + "'):\n" + " if (ord(p[5]) == ((1337 % ord('=')) + 7)):\n" + " if (ord(p[6]) == (((42 * ord(p[5])) % 13) + 45)):\n" + " if (ord(p[7]) == ((0xc0ffee % ord(p[6])) + 57)):\n" + " if (ord(p[8]) == ((0xbadf00d & ord(p[7])) + ord(p[6]) + 50)):\n" + " return('1')\n" + " return('0')\n" + "import sys; sys.stdout.write(c())\n"; Python.eval(code); }; addEventListener('message', messageHandler, true); postMessage('Loaded'); })();
Le script ci dessus inclut le script 'p.js', qui est en réalité un interpréteur Python en Javascript !
La résolution de cette partie reste similaire à celle précédente, simplement dans un langage différent, qui va ensuite être interprété par Javascript.
On trouve les 4 prochains caractères qui sont :
?4;o
On a trouvé les 8 premiers caractères de la chaîne, il manque les 8 derniers dans le dernier Worker.
;(function () { importScripts('check3_vm.js'); var initialized = false; var messageHandler = function(e) { if (!initialized) { p = e.data; pp = "var passPtr = _malloc(17);"; pp += "writeStringToMemory(p, passPtr);"; pp += "var c = _check3(passPtr);"; eval(pp); } postMessage("" + c); } addEventListener('message', messageHandler, false); postMessage('Loaded'); })();
Après l'interpréteur Python en Javascript, voici Emscripten, qui permet d'interprêter du bytecode LLVM (généré à partir d'un code C) et le transformer en Javascript.
Voici la fonction chargée de la vérification de nos derniers caractères :
function _check3($x) { ; var __label__; __label__ = 2; while(1) switch(__label__) { case 2: var $1; var $2; var $p; $2=$x; $p=0; var $3=_malloc(6); $p=$3; var $4=$2; var $5=(($4+13)|0); var $6=HEAP8[($5)]; var $7=$p; var $8=(($7)|0); HEAP8[($8)]=$6; var $9=$2; var $10=(($9+15)|0); var $11=HEAP8[($10)]; var $12=$p; var $13=(($12+1)|0); HEAP8[($13)]=$11; var $14=$2; var $15=(($14+11)|0); var $16=HEAP8[($15)]; var $17=$p; var $18=(($17+2)|0); HEAP8[($18)]=$16; var $19=$2; var $20=(($19+14)|0); var $21=HEAP8[($20)]; var $22=$p; var $23=(($22+3)|0); HEAP8[($23)]=$21; var $24=$2; var $25=(($24+12)|0); var $26=HEAP8[($25)]; var $27=$p; var $28=(($27+4)|0); HEAP8[($28)]=$26; var $29=$2; var $30=(($29+10)|0); var $31=HEAP8[($30)]; var $32=$p; var $33=(($32+5)|0); HEAP8[($33)]=$31; var $34=$p; var $35=(($34+5)|0); var $36=HEAP8[($35)]; var $37=(($36 << 24) >> 24); var $38=_fmod($37, 42); var $39=(($38)&-1); var $40=(($39)|0)==7; if ($40) { __label__ = 3; break; } else { __label__ = 9; break; } case 3: var $42=$p; var $43=(($42+3)|0); var $44=HEAP8[($43)]; var $45=(($44 << 24) >> 24); var $46=(($45)|0)==42; if ($46) { __label__ = 4; break; } else { __label__ = 9; break; } case 4: var $48=$p; var $49=(($48)|0); var $50=HEAP8[($49)]; var $51=(($50 << 24) >> 24); var $52=((($51)+(3))|0); var $53=(($52)|0); var $54=_sqrt($53); var $55=(($54)&-1); var $56=$p; var $57=(($56+4)|0); var $58=HEAP8[($57)]; var $59=(($58 << 24) >> 24); var $60=((($59)-(30))|0); var $61=(($55)|0)==(($60)|0); if ($61) { __label__ = 5; break; } else { __label__ = 9; break; } case 5: var $63=$p; var $64=(($63+2)|0); var $65=HEAP8[($64)]; var $66=(($65 << 24) >> 24); var $67=_ldexp($66, 3); var $68=(($67)&-1); var $69=((($68)|0))%(3); var $70=((($69)+(47))|0); var $71=$p; var $72=(($71+5)|0); var $73=HEAP8[($72)]; var $74=(($73 << 24) >> 24); var $75=(($70)|0)==(($74)|0); if ($75) { __label__ = 6; break; } else { __label__ = 9; break; } case 6: var $77=$p; var $78=(($77+4)|0); var $79=HEAP8[($78)]; var $80=(($79 << 24) >> 24); var $81=((($80)+(800))|0); var $82=$p; var $83=(($82+2)|0); var $84=HEAP8[($83)]; var $85=(($84 << 24) >> 24); var $86=((((($85)|0))/(2))&-1); var $87=(($86)|0); var $88=_llvm_pow_f64($87, 2); var $89=(($88)&-1); var $90=(($81)|0)==(($89)|0); if ($90) { __label__ = 7; break; } else { __label__ = 9; break; } case 7: var $92=$p; var $93=(($92+1)|0); var $94=HEAP8[($93)]; var $95=(($94 << 24) >> 24); var $96=((($95)*(666))|0); var $97=$96 >> 9; var $98=(($97)|0)==57; if ($98) { __label__ = 8; break; } else { __label__ = 9; break; } case 8: $1=1; __label__ = 10; break; case 9: $1=0; __label__ = 10; break; case 10: var $102=$1; ; return $102; default: assert(0, "bad label: " + __label__); } }
Cette fonction effectue une boucle, qui pour chaque itération, vérifie un caractère du mot de passe.
En étudiant la source, on remarque que le point de sortie de cette fonction est dans le case 10 :
var $102=$1; return $102;
Cette variable $1 est initialisée à 1 dans le case 8, et à 0 dans le case 9.
Chacun des autres cases jumpent sur le case 9 en cas d'erreur (la fonction retourne alors 0).
Notre but sera donc d'éviter de jumper sur le case 9 en validant une à une les conditions, à la manière des Workers précédents.
On remarque après quelques tests que le 1er caractère du check3 (donc le 9ème du mot de passe) n'est pas vérifié. On peut donc le mettre à n'importe quelle valeur.
Pour nos tests, essayons le mot de passe suivant :
![!}f?4;oXABCDEF
Voici le code commenté de ce que l'on peut en tirer sur les parties qui nous intéressent :
var $36=HEAP8[($35)]; // Après un breakpoint sur cette ligne, on récupère 'A' : 2eme caractère du check3 (appelons le x) var $37=(($36 << 24) >> 24); // Le caractère ne doit pas être codé sur plus de 24 bits var $38=_fmod($37, 42); // x % 42 var $40=(($39)|0)==7; // ((x % 42) == 7)
Il faut donc trouver un caractère dont le modulo par 42 est égal à 7. Prenons donc ascii(49), à savoir le caractère “1” On a dorénavant un mot de passe de la forme :
![!}f?4;oX1BCDEF
var $44=HEAP8[($43)]; // 6ème caractère du check3 var $45=(($44 << 24) >> 24); // filtre var $46=(($45)|0)==42; // Le code ascii du 6ème = 42
Simplement, le 6ème caractère est ascii(42), à savoir le caractère *.
![!}f?4;oX1BCD*F
var $50=HEAP8[($49)]; // 5ème caractère (appelons le x) var $51=(($50 << 24) >> 24); // filtre var $52=((($51)+(3))|0); // $52 = x + 3 var $53=(($52)|0); // filtre var $54=_sqrt($53); // sqrt(x + 3) var $55=(($54)&-1); // filtre [...] var $58=HEAP8[($57)]; // 4ème caractère (y) var $59=(($58 << 24) >> 24); // filtre var $60=((($59)-(30))|0); // y - 30 var $61=(($55)|0)==(($60)|0); // condition à respecter : sqrt(x + 3) == y - 30;
Il faut que x et y respectent : 33 ⇐ {x, y} ⇐ 126 pour qu'ils soient affichables (caractères ascii) On garde les solutions de la fonction carrée en tête, puis on teste de trouver y avec différentes valeurs de x pour résoudre le système :
{x = 61; y = 38} => {x = '=', y = '&'} {x = 78; y = 39} => {x = 'N', y = "'"} {x = 97; y = 40} => {x = 'a', y = '('} {x = 118; y = 41} => {x = 'v', y = ')'}
Après, x dépasse 126, donc pas la peine de continuer.
Normalement toutes ces solutions sont bonnes jusque là…
Vu qu'on ne sait pas laquelle prendre, on continue en prenant x='a' et y='(' (au pif)
Le mot de passe est dorénavant de la forme :
![!}f?4;oX1B(a*F
var $65=HEAP8[($64)]; // 3ème caractère (x) var $66=(($65 << 24) >> 24); // filtre var $67=_ldexp($66, 3); // x * 2 ^ 3 => x * 8 var $68=(($67)&-1); // filtre var $69=((($68)|0))%(3); // (x * 8) % 3 var $70=((($69)+(47))|0); // ((x * 8) % 3 + 47) [...] var $73=HEAP8[($72)]; // 2ème caractère (y) qu'on a déjà trouvé : 49 ('1') var $74=(($73 << 24) >> 24); // filtre var $75=(($70)|0)==(($74)|0); // ((x * 8) % 3 + 47) == 49
Il faut que le résultat du modulo de la dernière ligne soit égal à 2.
Cela marche tel que :
x = {34('"'), 37('%'), 40('('), 43('+'), 46('.'), 49('1'), 52('4'), 55('7'), 58(':'), 61('='), 64('@'), 67('C'), 70('F'), 73('I'), 76('L'), 79('O'), 82('R'), 85('U'), 88('X'), 91('['), 94('^'), 97('a'), 100('d'), 103('g'), 106('j'), 109('m'), 112('p'), 115('s'), 118('v'), 121('y'), 124('|')};
On va prendre… allez, ascii(52). soit '4'.
Notre mot de passe :
![!}f?4;oX14(a*F
Plus que le dernier caractère à trouver !
var $79=HEAP8[($78)]; // 4ème caractère (x) var $80=(($79 << 24) >> 24); // filtre var $81=((($80)+(800))|0); // x + 800 [...] var $84=HEAP8[($83)]; // 3ème caractère (y) var $85=(($84 << 24) >> 24); // filtre var $86=((((($85)|0))/(2))&-1); // y / 2 var $87=(($86)|0); // filtre var $88=_llvm_pow_f64($87, 2); // (y / 2) ^ 2 var $89=(($88)&-1); // filtre var $90=(($81)|0)==(($89)|0); // (x + 800) == (y / 2) ^ 2
Nous avons auparavant choisi des solutions “au hasard” parmi celles possibles pour les caractères 3, 4 et 5.
Cette routine va nous permettre de savoir quel couple est le bon.
Pour x (3ème caractère), il n'y avait que 4 possibilités :
x = {38, 39, 40, 41}
Pour y :
x = {34('"'), 37('%'), 40('('), 43('+'), 46('.'), 49('1'), 52('4'), 55('7'), 58(':'), 61('='), 64('@'), 67('C'), 70('F'), 73('I'), 76('L'), 79('O'), 82('R'), 85('U'), 88('X'), 91('['), 94('^'), 97('a'), 100('d'), 103('g'), 106('j'), 109('m'), 112('p'), 115('s'), 118('v'), 121('y'), 124('|')};
En remplaçant x par les valeurs données ci-dessus et en calculant la partie gauche de l'équation (x + 800), on obtient ces résultats :
838, 839, 840 ou 841.
Pour la partie droite :
289, 342.25, 400, 462.25, 529, 600.25, 676, 756.25, 841, 930.25, 1024, 1122.25, 1225, 1332.25, 1444, 1560.25, 1681, 1806.25, 1936, 2070.25, 2209, 2352.25, 2500, 2652.25, 2809, 2970.25, 3136, 3306.25, 3481, 3660.25, 3844
On retrouve 841 des deux côtés, qui est la solution de notre équation.
Donc x = 41, et y = 58 ! {x = ')', y = ':'}
On n'oublie pas que, du coup, puisque le 4ème caractère est égal à 41, le 5ème caractère devient 'v' (cf case 4)
![!}f?4;oX1:)v*F
var $94=HEAP8[($93)]; // 7ème caractère (x) var $95=(($94 << 24) >> 24); // filtre var $96=((($95)*(666))|0); // (x * 666) var $97=$96 >> 9; // (x * 666) >> 9 var $98=(($97)|0)==57; // ((x * 666) >> 9)) == 57
On résoud ça en faisant
x = round(57 << 9 / 666)
On obtient : x = 44, soit x = ','
On valide l'épreuve avec le mot de passe :
![!}f?4;oX1:)v*,
Et un flag de plus, avec en prime un joli poney-megusta ! :)
Avoir récupéré en local les js + les images et le html (Avec par exemple un wget -r)