User Rating: 5 / 5

Star ActiveStar ActiveStar ActiveStar ActiveStar Active
 

 

Il y a deux versions du challenge, car la première version du programme acceptait des faux positifs. Lorsque ce genre de chose se produit en CTF, c’est généralement bénéfique de faire un diff entre les versions : sur un fichier contenant beaucoup d’infos inutiles, on peut économiser des heures de recherche en voyant quelle partie a été retravaillée. Ici le diff n’est pas vraiment utile, il apporte juste une correction à un dépassement d’entier dans un modulo.

On charge le fichier dans IDA Pro (ou tout autre outil gratuit, merci à mon ami D.L. pour la licence IDA) :

Le challenge est composé d’une vingtaine de fonctions courtes, commençons par le main :

 

 

Fonction principale

Le main se contente de lire 20 caractères dans l’entrée standard, et envoie cette string à une fonction checker sub_138B. Cette dernière peut sembler très complexe à première vue avec ses if imbriqués, mais la logique est en réalité simple :

(Il y a 14 if imbriqués en tout)

On peut séparer cette fonction en sous-parties.

 

Initialisation

Tout d’abord, une étape de pre-processing qui va mettre en place les variables pour l’exécution de la suite du programme. On va se concentrer sur la fonction sub_1355 qui transforme des blocs de 4 caractères de l’entrée utilisateur sous forme d’entiers de 32 bits, en utilisant des opérations logiques bizarres.

Pour comprendre le comportement de cette fonction, on peut copier-coller le code dans Python (les opérations utilisent ici les mêmes symboles entre C et Python), et jouer un peu avec.

On constate que ces opérations servent à inverser la position des octets dans le nombre fourni. Mais à quoi ça sert ? Il s’agit en fait d’une correction liée à la manière dont le processeur représente la donnée :

Dans les architectures x64, qui utilisent le little endian, l’ordre des octets en mémoire n’est pas le même si l’on considère des int ou des char. Il faut donc inverser la position des octets pour retomber sur nos pattes lors d’une conversion de l’un à l’autre.

Ensuite, la dernière ligne de l’initialisation va copier une longue section de mémoire dans une variable locale v2.

 

 

 

Boucle d’exécution

Ensuite, le programme entre dans une boucle infinie d’exécution. Il récupère d’abord les 3 valeurs successives v2[ptr], v2[ptr + 1] et v2[ptr + 2].

En fonction de la valeur de v2[ptr], le programme va choisir (avec tous les if imbriqués vus plus haut) une fonction parmi 14 qui sera appelée avec les arguments v2[ptr + 1] et v2[ptr + 2]. Les experts en reverse auront reconnu un challenge typique de VM, où le programme du challenge a un format custom et s’exécute dans un interpréteur fait maison. C’est en quelque sorte du méta-assembleur !

Regardons de plus près quelques instructions à notre disposition :

Cette fonction compare les deux valeurs pointées par ses arguments, et met à jour un bit en mémoire qui note si ces valeurs sont égales ou non. Ce type d’instruction est généralement suivi d’une instruction conditionnelle pour reproduire un if, for ou while. Voici l’un de ces sauts conditionnels implémentés dans cette VM :

Si les deux valeurs sont égales, la variable ptr sera modifiée. Cela correspond à un jump conditionnel vers une autre instruction.

Plusieurs autres instructions classiques sont implémentées telles que xor, add ou exit. L’une des fonctions est cependant plus complexe que les autres :

Un oeil avisé saura reconnaître ici l’algorithme d’, équivalent à la fonction de Python. Le nombre et le modulo sont donnés en argument, mais la puissance est fixe (dword_4074) et vaut 65537. C’est au tour des amateurs de crypto de se réveiller, puisque 65537 (aussi écrit 0x10001) est l’exposant le plus répandu dans le cryptosystème RSA.

 

Reconstruction du programme

 

Une fois que toutes les instructions ont été identifiées, on peut passer au désassemblage du programme de la VM. On commence par récupérer le contenu de la section mémoire qui nous intéresse, c’est très simple avec IDAPython par exemple :

Ensuite, on reconstitue les instructions et les arguments dans un format lisible. Ici on se base uniquement sur le premier caractère de l’opcode car celui-ci est unique à chaque instruction :

On récupère le pseudocode suivant :

C’est un script très simple que l’on peut résumer en 5 conditions que les blocs d’input doivent vérifier :

 

 

 

Résolution des conditions

 

Afin de vérifier ces conditions, deux options s’offrent à nous. On pourrait tester les 2^32 valeurs possibles pour chaque bloc, ce qui prendrait environ 1h de calcul sur un bon CPU avec un bruteforce intelligent qui élimine les caractères non-imprimables.

L’autre option est d’utiliser les propriétés de RSA pour retrouver instantanément les valeurs attendues. On factorise chaque module (la factorisation naïve en testant tous les facteurs de 1 à sqrt(n) est très rapide), ce qui nous sert à retrouver l’exposant de déchiffrement. Ensuite, on applique la fonction de déchiffrement RSA qui nous donne directement le morceau du flag attendu :

Et le résultat s’affiche instantanément :

On peut maintenant valider avec le flag pour gagner 1337 points !

 

  

Cet article reflète exclusivement l'opinion de ses auteurs et n’engage en aucune façon Consultingit. J'espère que ça vous a plu. Vos commentaires/remarques sont les bienvenus: