🇫🇷 ph0wn Musical Ear

Table of Contents

ph0wn Musical Ear

Ce challenge etait proposé à l’édition 2021 du ph0wn, je n’ai pas participé à cette édition mais ait réalisé le challenge plusieurs mois après.

Le writeup officiel est disponnible ici

1ere reconnaissance

On est face à une application android (.apk). Pour émuler et avoir une idée de ce que le programme fait j’utilise genymotion.

On atteri sur une jolie interface

musical_ear1

Le bouton play sound nous joue une note, le jeu consistera à trouver parfaitement la note parmis un panel de 12 possibilitées.

panel

Etant donné qu’environ 0.01% de la population est dotée de l’oreille absolue, le 1er objectif est de trouver comment tricher.

Obtenir un 20/20

Le programme intègre lui même un système de log ou les solutions sont écrites du moment ou le fichier cheatfile existe

jadx-1

En chérchant une manière “automatique” on trouve qu’à chaque manche, un entier, entre 0 et 23 est généré. Une note est ensuite choisie à partir de cet indexe. Ensuite, si l’indexe de la note séléctionnée modulo 12 est celle générée, la note est juste. Il existe donc 24 notes possibles pour 12 choix possibles.

J’ai utilisé frida avec un script très simple.

  • client :

script.py

import frida, sys

ss = open("hook.js","r").read()
device = frida.get_device_manager().enumerate_devices()[-1]
print(device)
# exit(1)
session = device.attach("Ph0wn Musical Ear")
script = session.create_script(ss)
script.load()
sys.stdin.read()

hook.js

Java.perform(function () {
    var Random = Java.use('java.util.Random'); // Le module

    // On overload la fonction nextInt du module (similaire à utiliser LD_PRELOAD sur linux)
    Random.nextInt.overload('int').implementation = function (bound) {
        console.log('[Random.nextInt] Bound: ' + bound);

        var result = this.nextInt(bound);

        console.log('[Random.nextInt] Result: ' + result);

        return 1; // Retourner 1 (La#) au lieu d'un résultat aléatoire
    };
});

Genymotion ouvre une interface android debug bridge (adb) sur la machine virtuelle, c’est grâce à cette interface qu’on va pouvoir lancer le serveur frida.

$ adb push ./frida-server-16.1.7-android-x86_64 /tmp
$ adb shell
vbox86p:/ # cd /tmp/      
vbox86p:/tmp # ls
frida-server-16.1.7-android-x86_64
vbox86p:/tmp # ./frida-server-16.1.7-android-x86_64

Maintenant la note jouée sera toujours la même ! En allant à 20, pas de flag en vue, uniquement un message reverse the dex banana et un bad decryption key.

Obtenir le flag

En regardant le fonctionnement de la branche une fois les 20 notes correctement jouées, on remarque l’utilisation d’une fonction HurryUp qui est ensuite passé dans CanYouWin.

jadx-2

La classe ph0wn.payload.Payload est utilisée mais n’est pas référencée dans notre fichier.

jadx-3

La classe semble être chargée en mémoire mais aucune opération de déchiffrement est utilisé.

En fait la fonction public final byte[] CanYouWin(InputStream inputStream) se charge de lire un buffer à l’envers, l’argument passé correspond à jarFile.getInputStream(jarFile.getEntry("classes.dex")).

le fichier classes.dex, retrouvé par l’extraction de signed.apk inversé donne bien un autre .dex, cependant impossible de lire correctement le .dex, mon hypothèse est que la version image et disque diffèrent.

Pour retrouver éfficacement le .dex j’ai utilisé FRIDA-DEXDump (fonctionne aussi avec frida).

En suivant simplement la notice et en éxécutant frida-dexdump -FU l’outil retrouve automatiquement l’interface adb et en extrait tous les .dex de mon application.

C’est dans le fichier classes13.dex qu’on retrouve la classe qui nous intéresse.

jadx-4

Un cipher est chiffré avec l’algorithme blowfish. Par une clée de 8 caractères composé de caractères entre A et F. Le mode par défaut utilisé quand il n’est pas spécifié est ECB.

import base64
import itertools
from Crypto.Cipher import Blowfish

text = base64.b64decode("BahoP2kAnYuRf5LqEW5umIRwZZD7iKrcOzdKXglXMKg=")

print(text)


def bruteforce_key(length=8):
    characters = 'ABCDEF'
    for combo in itertools.product(characters, repeat=length):
        yield ''.join(combo)

for key in bruteforce_key():
    cipher = Blowfish.new(key.encode(), Blowfish.MODE_ECB)
    result = cipher.decrypt(text)
    if(b"ph0wn" in result):
        print("->", key)
        print(result)
        exit(1)
    print(key)

solution

Flag: ph0wn{ThisWasAReversibleDex}