ZenSecCTF Team

ZenSec CTF Team write up page

19 October 2021

Reverse - BreakThePass

by Karzemrok

Description

Just try to break the pass !

http://34.245.93.104:3000/

Fichier

Résolution

L’application Web est un formulaire qui demande un mot de passe.

La fonction check_password en JS est la suivante:

function check_password() {
    let input = document.getElementById("chall-input-password");
    if (!input) {
        console.log("L'input n'a pas été trouvé");
        return;
    }
    let _value = input.value
    let ptr = allocate(intArrayFromString(_value), ALLOC_NORMAL);
    _check_the_flag(ptr)
    _free(ptr)
}

La valeur du mot de passe est transformée en tableau d’int et mise en mémoire.

Le pointeur vers cet espace mémoire est passé en paramètre de ‘_check_the_flag’ qui est un alias vers une fonction interne d’un programme wasm.

Nous allons transformer le WASM en JS (avec wasm2js) et en WAT (avec wasm2wat) pour avoir deux vues différentes pour nous simplifier l’analyse du binaire.

wasm2wat assets/authentification.wasm -o assets/authentification.wat
wasm2js assets/authentification.wasm -o assets/authentification.wasm.js

Le format JS est plus lisible, mais le WAT à l’avantage de pouvoir être retransformé en WASM facilement.

Pour qu’une fonction du WASM soit utilisable en JS, il faut qu’elle soit exportée, ce qui va nous aider à l’identifier.

Dans le format WAT, la fonction est la 37

Alors que dans le JS, c’est “$33

On voit des appels à 3 fonctions différentes:

fimport$1’ dans notre cas exécute gettimeofday, elle ne sera pas intéressante ici.

La fonction fimport$0 prend en paramètre un offset dans les data et utilise la chaîne de caractères disponible à cette adresse en tant que paramètre de emscripten_run_script qui se charge d’exécuter la chaine de caractère en tant que JavaScript dans le navigateur

Les datas WASM sont déclarée ici dans le fichier JS :

Ici, 1024 est également un offset. Pour savoir ce qui sera exécuté par fimport$0(1138) il faut regarder la chaine de caractère à l’offset 164 (1138-164) dans les data.

import base64

datas = "c3RoYWNreyVzfQAtKyAgIDBYMHgALTBYKzBYIDBYLTB4KzB4IDB4AG5hbgBpbmYATkFOAElORgAuAChudWxsKQBkaXNwbGF5X3Jlc3VsdCgnV2VsbCBkb25lICEhISBUaGUgZmxhZyBpcyA6ICVzJykAZGlzcGxheV9yZXN1bHQoJ0xlIGJydXRlIGZvcmNlIGVzdCBpbnRlcmRpdCAhISEnKQBkaXNwbGF5X3Jlc3VsdCgnUGFzc3dvcmQgaW5jb3JyZWN0ICEhIScpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEACgAREREAAAAABQAAAAAAAAkAAAAACwAAAAAAAAAAEQAPChEREQMKBwABAAkLCwAACQYLAAALAAYRAAAAERERAAAAAAAAAAAAAAAAAAAAAAsAAAAAAAAAABEACgoREREACgAAAgAJCwAAAAkACwAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAMAAAAAAwAAAAACQwAAAAAAAwAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAADQAAAAQNAAAAAAkOAAAAAAAOAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA8AAAAADwAAAAAJEAAAAAAAEAAAEAAAEgAAABISEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEhISAAAAAAAACQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAACgAAAAAKAAAAAAkLAAAAAAALAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAwAAAAADAAAAAAJDAAAAAAADAAADAAAMDEyMzQ1Njc4OUFCQ0RFRg=="

print(base64.b64decode(datas)[1188-1024:].split(b"\x00")[0]) # Dans $1
print(base64.b64decode(datas)[1138-1024:].split(b"\x00")[0]) # Dans $33

Ce qui nous donne

b"display_result('Password incorrect !!!')"
b"display_result('Le brute force est interdit !!!')"

On sait donc maintenant que nous devons éviter la fonction $1 et que fimport$0(1188) nous indique un mot de passe incorrect.

L’appel à fimport$0(1138) est une fonctionnalité anti-bruteforce.

Il nous reste donc l’appel à la fonction $4 pour continuer.

L’autre appel à fimport$0 se fait dans la fonction $2 ce qui semble être un bon point de sorti. Pour le confirmer, on peut modifier le WAT pour sauter dans cette fonction et voir ce qui se passe.

Pour repérer la fonction correspondante dans le fichier WAT, on peut chercher l’instruction call 0, qui apparait 3 fois

La fonction 5 affiche le message de mot de passe incorrect.

Ici, c’est la protection anti-bruteforce.

Cet appel, qui nous intéresse est dans la fonction 6

On va donc modifier la fonction exportée pour utiliser cette fonction et recompiler en WASM

On voit alors que le mot de passe testé est affiché en tant que flag.

Notre but va donc être de trouver un chemin depuis la fonction $33 vers la fonction $2

Notre unique sortie favorable de la fonction $33 était la fonction $4

Dans cette fonction, les fonctions de sortie possible sont $1 et $5. La fonction $1 étant la fonction indiquant un mauvais mot de passe, il faut donc accéder à $5

On voit une comparaison avec le nombre 27, on peut supposer qu’ici, il y a 26 caractères dans le mot de passe (26+\x00).

La fonction $5 fait un appel à $3 avec en paramètre le numéro 87.

La fonction $3 se chargera de faire un XOR entre ses deux paramètres.

À ce stade, il est probable que chaque fonction calcul fasse un XOR d’un caractère du mot de passe et le compare à une valeur.

On remarque que $6 ressemble à $5 en changeant simplement le caractère passé au XOR et à la comparaison.

On va donc extraire les paramètres passés à $3 en tant que clé XOR et les valeurs de comparaison.

XOR_KEY = [87,10,19,150,64,126,23,60,32,105,37,115,126,63,35,127,15,170,174,234,59,119,26,34,53,149,63]
XOR_PASS = [15,58,97,196,115,58,72,107,19,43,101,32,13,12,110,29,67,211,241,219,72,40,41,67,102,204]

À la fonction $31, on voit que le XOR est fait mais sans comparaison, il faut donc faire le XOR sur la valeur ‘0

On peut alors calculer le mot de passe final :

XOR_KEY = [87,10,19,150,64,126,23,60,32,105,37,115,126,63,35,127,15,170,174,234,59,119,26,34,53,149,63]
XOR_PASS = [15,58,97,196,115,58,72,107,19,43,101,32,13,12,110,29,67,211,241,219,72,40,41,67,102,204,0]

for x in range(len(XOR_KEY)):
    print(chr(XOR_KEY[x] ^ XOR_PASS[x]), end='')

<< MISC - Baby CloudFlare
Forensic - Docker Image >>
tags: sthack2021 - reverse - ctf