Étonnant, le binaire semble segfault lorsque l'entrée n'est pas de longueur multiple de 4. Sinon, il n'affiche juste rien. Le but est donc de lui faire cracher le flag. Sans plus attendre, laissons place à Ghidra. Quelques variables ont été renommées pour la lisibilité :
La fonction main calcule la longueur de notre argument et appelle la fonction FUN_00400860. Je passe sur les détails parce que ce n'est pas la partie intéressante ; cette fonction décode notre entrée comme de la base64, renvoie son adresse et stocke sa taille en octets une fois décodée dans local_18.
La fonction FUN_00601050 est appelée avec l'adresse de notre mot de passe décodé et sa longueur.
Ce premier bloc vient charger auVar9 avec la valeur 0x80000000000000000000000000000000. C'est une initialisation que l'on ne retrouve plus dans les blocs suivants ; à la place, on y retrouvera à chaque fois un décalage de 1 bit vers la droite, autrement dit à l'itération i cette variable vaudra l'entier binaire 128 bits qui possède un unique 1 en i-ème position (de poids fort à poids faible).
Il charge ensuite auVar8 avec une constante 128 bits, ici 85 D1 31 C4 C8 26 CD 0D D5 5F FF B8 9E 4B BF 9C (telle qu'affichée dans le débugger d'IDA, en little-endian).
auParm1 contient les 16 premiers octets de notre entrée, vus comme un entier 128 bits. Un ET logique est effectué entre notre entrée et la constante formée. Enfin, la fonction popcnt permet de compter le nombre de bits à 1 dans un registre. Cela est fait en deux étapes à travers les registres 64 bits, mais la finalité est la même : si le XOR (ou l'addition des bits modulo 2...) des bits constituant le résultat du ET logique vaut 1, alors on rentre dans le if, qui va initialiser auVar8 à auVar9.
Le contenu de ce if fait probablement plus de sens dans les blocs suivants :
auVar8 est XORé avec auVar10, ce qui est équivalent à mettre le i-ème bit à 1 dans auVar8.
Ainsi, le résultat de ces 128 blocs est la formation d'un entier 128 bits c tel que c[i] est la somme modulo 2 des bits de (key & a[i]), où a est la i-ème constante magique.
Le dernier bloc (le "129ème") est un peu illisible avec les CONCAT et les SUB de Ghidra et est plus clair en assembleur :
A l'aide de PCMPEQD, on compare deux registres XMM, à savoir celui contenant c et une constante chargée, ici F4 A4 ... E9 62.
Bien ! Il ne reste plus qu'à reformuler ça mathématiquement (le chall a le tag "maths" après tout, même si ça reste assez léger à mon sens 🧐). Chaque bloc peut être reformulé de la façon suivante :
⟨ai∣x⟩≡cimod2
où x est l'inconnue (notre password), (ai)0≤i<128 les constantes magiques et (ci)0≤i<128 les bits de la valeur magique de comparaison finale. x et (ai) sont des vecteurs 128 bits qui codent l'entier qu'ils représentent.
On a 128 équations de ce genre, que l'on peut donc reformuler globalement ainsi :
Ax≡Cmod2
où A est la matrice de bits dont les lignes sont les ai, et C le vecteur colonne des ci.
Cette équation se résout en trois lignes de Sage :
R =IntegerModRing(2)M =Matrix(R, [...])b =vector(R, [...])print(M.solve_right(b))
Il ne reste plus qu'à coder un petit script pour parser le code afin de récupérer toutes les valeurs magiques et c'est gagné !
Je passe les détails : on trouve une solution, on la décodé en binaire puis ré-encode en base64 et on obtient eqFUxbL2zNoSFXuPo3P64A==. On la passe au binaire pour obtenir le flag :
On lance IDA et on débug pour voir pourquoi ça n'a pas marché. On se rend compte qu'en fait si, ça a marché ; la condition est bien passée et on n'a pas emprunté le syscall exit. On rentre dans la routine de déchiffrement AES et là on commence à voir venir la couille...
La routine AES déchiffre un nouveau bloc de code du programme et va jump dessus. Pas très grave me direz vous, il suffit d'analyser ce que fait ce nouveau bloc de code, éventuellement trouver la façon dont est générée le flag sur le passage et c'est plié.
Sauf que le bloc de code généré ressemble exactement à la fonction principale initiale, aux constantes magiques près. Ce nouveau code utilise les 16 prochains octets de notre mot de passe.
On effectue un rapide calcul, et on se rend compte qu'il y a en fait environ 127 blocs de code obfusqué de même longueur que la première fonction dans le binaire, soit au total 128 problèmes de ce type à résoudre. Il va falloir scripter intelligemment... !
Je passe sur les détails, il s'agit de parser le binaire directement pour récupérer les valeurs magiques, résoudre l'équation matricielle, utiliser le morceau de clé trouvé pour déchiffrer le prochain bloc de code et répéter. Les difficultés principales résidaient dans le parsing correct du binaire (j'ai passé beaucoup de temps à débugger une regex erronnée) et dans la compréhension de la routine de déchiffrement, qui est certes du AES classique, mais il faut bien identifier ce qu'on chiffre avec quelle clé à chaque itération. Il s'agissait en fait d'AES en mode CTR (avec le compteur "canonique" si je puis dire).
import re, structfrom Crypto.Cipher import AESfrom binascii import hexlify as tohex, unhexlify as unhexfrom base64 import b64encodefrom os import systemdefbin_array(b): B = []for j inrange(len(b)):for i inrange(8): B.append((b[j] >> (7- i)) &1)return Bdeffrom_bin(s): q =''.join(c for c in s if c in'01')returnbytes([int(q[i:i +8], 2) for i inrange(0, len(q), 8)])classCounter(object):def__init__(self): self.c =0defcounter(self): v = struct.pack('<Q', self.c %2**64)+ struct.pack('<Q', (self.c >>64) % (2**64)) self.c +=1return vf =open('ssecret.bin', 'rb').read()[0x1050:]f = [f[i:i +0x2c00]for i inrange(0, len(f), 0x2c00)][:128]secret =b''for k inrange(128): blob = f[k]print(blob[:100]) blob = re.split(rb'[\x22\x73][\xd3\xd2]\x01\x48\xb8', blob)[1:] res =bin_array(blob[-1][:8] + blob[-1][10:10+8]) A = [] C = [] j =0for b in blob[:-1]:if b.startswith(b'\x00\x00\x00\x00\x00\x00\x00\x80'):continue A.append(bin_array(b[:8] + b[10:10+8])) C.append(res[127- (7- (j %8)) -8* (j //8)]) j +=1# Ax = C mod 2# Sage script generationwithopen('ssecret.sage', 'w')as sagefile: sagefile.write("""R = IntegerModRing(2)M = Matrix(R, %s)b = vector(R, %s)print(M.solve_right(b))"""% (repr(A), repr(C)))system("sage ssecret.sage > ssecret.sage.sol")withopen('ssecret.sage.sol', 'r')as sol: sage_output = sol.read() key =from_bin(sage_output) secret += keyprint('[%s] Secret: %s'% (k, b64encode(secret).decode())) counter =Counter() cipher = AES.new(key, AES.MODE_CTR, counter=counter.counter) f[k +1]= cipher.encrypt(f[k +1])
Le script met un peu de temps à s'exécuter (2 minutes sur ma machine), probablement à cause des appels à Sage... (je ne connais pas bien Sage donc je ne sais pas s'il y a un moyen plus simple de "wrapper" tout ça, j'ai juste vu que je pouvais exécuter le script en ligne de commande donc j'ai foncé là-dessus).
On trouve alors enfin le (long) mot de passe à la 128ème itération, et on prie pour qu'il n'y ait pas une étape supplémentaire à la fin :
$ ./ssecret.bin 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
Well done! Here is the flag: FCSC{b0f6cfda0049a03d65d6b9e3e3ecf5b990c24ffe27784b7d553fcdc2f45a8ad4}