Write-up – Nuit du hack 2017 CTF Quals – Matriochka step 3

Introduction

Quelques consultants Intrinsec ont participé au CTF de qualification de la Nuit du Hack 2017, qui s’est déroulé dans la nuit du 31 mars. Nous avons terminé 36ème et voici un Write-Up sur une épreuve de reverse que nous n’avons pas résolue avant la fin du temps réglementaire.

Voici l’énoncé de l’épreuve :

Identification

Le binaire « step3 » de l’épreuve est accessible ici.

(SHA256 : f7c926721b03de5f2afefaeeb552441f848ea280031c95b1c6ffe1a7103762b2)

Le binaire récupéré une fois l’étape 2 validée est un exécutable Linux 64 bits strippé, comme le montre le résultat de la commande « file » :Lors de la première exécution, on constate deux éléments intéressants :

  • Le mot de passe est transmis en paramètre.
  • La sortie du programme, « Try again :(« , est affichée après la terminaison du programme, indiquant l’utilisation de sous-programmes créés via des fonctions telles que « fork() » ou « system() ».

La comparaison finale validant le challenge est facilement identifiable : une chaîne codée en dur est comparée avec une variable dont la valeur est dérivée du mot de passe saisi :

Le but ici est donc de trouver le mot de passe qui va générer cette chaîne.

Contournement de la protection anti-debug

Après une analyse rapide de la fonction « main() » du programme avec l’outil IDAPro, on confirme plusieurs appels à la fonction « fork() » :

Si l’on regarde en détail cet extrait de code assembleur, qui est réutilisé à plusieurs reprises, on peut voir un appel à la fonction « rand() » dans le premier basic block. Cette fonction retourne un nombre pseudo-aléatoire qui, suivant sa parité, va déterminer la branche qui sera exécutée.

La branche de gauche va donc exécuter un « fork() ». Les deux processus vont alors exécuter la comparaison suivante :

test eax, eax

Deux cas vont alors se produire :

  • Exécution par le processus père : le registre eax contient le PID du processus fils et est donc différent de 0. La comparaison sera alors validée, le saut effectué, et l’appel système « syscall(0x3c) » contourné.
  • Exécution par le processus fils : le registre eax vaut 0, l’appel système « syscall(0x3c) » est exécuté, et le processus fils se termine.

Note : Comme indiqué dans ce tableau de correspondance des appels système, la valeur « 0x3c » correspond à la fonction « exit() ».

Maintenant, si l’on s’intéresse à la branche de droite, la condition est inversée : le processus père se termine et le fils continue son exécution.

Pour contourner cette protection dans GDB, plusieurs possibilités s’offrent à nous :

  • Supprimer les appels à « fork() » avec des instructions de padding (« nop », « mov eax, eax », etc.)
  • Ecrire un fichier de configuration GDBInit

A noter toutefois que la modification du binaire peut poser des problèmes ultérieurement si une vérification d’intégrité du code a été implémentée par le développeur. Le second choix nous a donc paru comme étant le plus judicieux.

L’idée est simple : placer un breakpoint sur tous les appels à la fonction « fork() », qui définira le comportement du debugger (« suivre le père » ou « suivre le fils »).

Voici un aperçu du fichier de configuration « GDB » :

Les deux fonctions « fc() » et « fp() » définissent le mode de suivi de « gdb » : « fc() » suit l’exécution du fils tandis que « fp() » s’occupe de suivre l’exécution du père.

L’instruction « commands 1 » va exécuter la routine « fp() » lors du déclenchement du breakpoint 1, qui dans notre cas est à l’adresse 0x400940.

Les appels à la fonction « fork() » ne sont maintenant plus un problème pour continuer l’analyse du binaire, GDB alterne entre les processus automatiquement :

Analyse en profondeur

Une fois cette protection contournée, l’analyse du code peut commencer.

Deux routines de chiffrement par XOR sont appliquées sur le mot de passe, la première ayant pour clé « pony » :

La seconde chiffre avec la clé « platypus » le blob binaire généré par la première routine :

Afin d’illustrer ce comportement, voici une implémentation rapide de ces deux opérations en Python :

A titre d’exemple, le chiffré d’un mot de passe « abcdefghi » sera donc « aalily|bi ».

Une routine d’encodage à l’adresse 0x400cdc est ensuite appliquée sur le mot de passe chiffré « aalily|bi » :

Une analyse dynamique montre que la valeur du mot de passe avant et après encodage est contenue respectivement dans les registres RBX et RAX :

L’encodage ressemble fortement à du « base64 », mais une comparaison avec cet algorithme nous montre que la chaîne de retour est différente :

L’analyse statique de la routine d’encodage permet de déterminer la liste des caractères utilisés, référencée à l’adresse 0x3613c0.

En plaçant un nouveau breakpoint et en affichant la valeur de la chaîne précédente, nous constatons qu’elle subit une permutation avant l’appel à la fonction d’encodage.  

Par souci de clarté, nous utiliserons par la suite le terme « chaîne de référence » pour désigner cette variable.

Implémentation d’une solution

Nous avons ensuite implémenté cet encodage en Python : 

La fonction fake64_e() prend en paramètre la chaîne à encoder (« data ») et la chaîne de référence pour l’encodage (« key »). Dans les grandes lignes, cette fonction découpe la chaîne d’entrée en sous-chaînes de 3 octets. Pour chacune d’entre elles, des opérations bit à bit sont effectuées et génèrent 4 nombres. Ces nombres correspondent à des numéros d’index de la chaîne de référence (« key »). Mises bout à bout, ces lettres génèrent le résultat encodé.

Lors de l’exécution du binaire, cet algorithme d’encodage est appliqué 16 fois sur le mot de passe d’entrée, et à chaque itération, une permutation sur la chaîne de référence est effectuée.

Nous avons donc itéré sur l’algorithme d’encodage et récupéré les 16 chaînes de référence pour les intégrer dans notre script Python. La chaîne finale, mentionnée au début de cet article, a également été insérée dans le but d’y appliquer le processus de décodage qui nous permettra de récupérer le mot de passe de l’épreuve :

Une fonction de décodage a donc été développée en Python :

Il ne reste plus qu’a effectuer les 16 itérations de décodage sur la chaîne finale du binaire et d’appliquer les deux chiffrements XOR mentionnés dans la section précédente :

Validation

Après ces 16 itérations et ces déchiffrements, le mot de passe apparaît : 

Un grand bravo à la team NDH pour nous avoir fourni un CTF avec des épreuves de qualité !