Ces deux dernières semaines j’ai eu l’occasion de participer à la phase de qualification du CTF ECW (pour European Cyber Cup). Dans ce premier write-up j’aborde la résolution d’un challenge de pwn que j’ai trouvé très sympa et dans lequel on va devoir exploiter un jeu sur gameboy.
Vous pouvez télécharger une archive contenant les fichiers fournis ici (code source + programme compilé).
Analyse du code
Il s’agit d’un jeu dans lequel on incarne un personnage enfermé dans un labyrinthe, et dont la résolution semble manifestement impossible. On a la possibilité de lui programmer une suite d’instruction (aller en haut, en bas, à gauche, à droite) en appuyant sur A puis en appuyant sur une des 4 flèches. La liste des instructions apparaît en haut. On peut sélectionner une instruction à l’aide des flèches de droite de de gauche et modifier le nombre de fois qu’elle va être exécuté à l’aide des flèches du haut et du bas. On peut également sélectionner deux instructions. Cela a pour effet d’échanger leur place si elles sont de types différents (ex : aller à gauche et aller à droite). Si les deux instructions sont identiques, cela les fusionne.
En lisant le code, on s’aperçoit que même si l’on arrive à atteindre la case du centre, le flag ne s’affichera pas.
Le bug
On trouve la faille dans la fonction move_inst (inst_list.c). En effet, elle nous permet de faire un appel à remove_inst, qui nous permet de décrémenter la variable inst_count de 1, sans actualiser la variable list_empty. On peut, en créant puis en sélectionnant deux fois le même item fixer inst_count à 0 tout en ayant list_empty à False.
Avec la configuration que nous avons obtenue grace à move_inst, nous pouvons essayer de supprimer l’item que nous avons déjà supprimé (avec le double select). Comme list_empty est à False, remove_inst sera bien appelé une seconde fois sur le même item.
remove_inst décrémente de 1 encore notre variable inst_count, la faisant passé de 0 à -1 (en signed), ou 0xFF=255 (en unsigned).
Si on fait le test en live dans l’émulateur de gameboy on obtient quelque chose d’intéressant: un bug !
On a maintenant accès à 255 octets de la mémoire qu’on peut « lire » et surtout qu’on peut réécrire. De quoi commencer la phase d’exploitation.
Exploitation
L’objectif est de lire le flag. On cherche donc à exécuter un shellcode. On debug le programme en utilisant BGB (un émulateur de Gameboy avec un debugger). En jouant un peu avec le programme et en inspectant la mémoire, on comprends à quels endroits sont situés les différentes variables.
- En violet: le tableau de pointeurs vers les fonctions de déplacement inst_funcs[4]
- En jaune: inst_rpt
- En bleu: inst_ids
- En rouge: inst_count
- En cyan: inst_cursor_pos
On peut arb-write à partir de inst_rpt. Comme on veut exécuter un shellcode, il nous faut trouver un moyen de contrôler le registre pointeur de code PC. L’idée est de rajouter un pointeur au tableau inst_funcs et d’appeler ce nouvelle élément lors de la simulation. Pour cela on écrit l’adresse de la zone mémoire dans laquelle on va écrire notre shellcode (dans mon cas j’ai choisis 0xc0c0) à 0xc0bb. On aurait pu l’écrire à 0xc0ba, mais on doit laisser cette emplacement vide pour conserver une répétition d’instruction valide (celle qui va justement appeler notre shellcode).
Si on créée une instruction ayant pour code 5, alors c’est le code indiqué par le pointeur se trouvant à 0xc0cb = 0xc0b1+ 5*2 qui sera appelé lors de la simulation. On écrit donc un 1 à 0xc0b9 (premier élément du tableau inst_rpt) et un 5 à 0xc0c9 (1er élément du tableau inst_ids).
Conception du shellcode
L’objectif est d’afficher le flag, donc de faire un appel sur bnprintf avec comme argument l’adresse du flag. Comme le créateur du chall est un mec bien (hourra), il nous facilite la tache (un tout petit peu) en nous indiquant l’adresse du flag (0x6fa).
En ouvrant le programme dans Ghidra avec l’extension ghidraboy, on repère un endroit intéressant où jump pour afficher le flag sans faire crasher le programme.
On note aussi les opcodes associés aux instructions qui nous intéressent et on construit notre shellcode. J’ai rajouté une addition sur le registre SP afin qu’il pointe au bon endroit lors du return en 0x0ed8.
ADD SP,4
LD DE,0x6fa
LD HL,0x0eca 21ca0e
JP HL e9
0xe8 0x04
0x11 0xfa 0x06
0x21 0xca 0x0e
0xe9
Le flag
Pour résumer notre mémoire doit être organisé comme ceci:
- [ 1 (répétition de l’appel à instr_funcs[5]]
- [ 0 (padding)]
- [0xc0 0xc0 (adresse de notre shellcode)]
- [ 0 // 3 fois (padding) ]
- [ shellcode ]
- [ 5 (index de instr_funcs appelé 1 fois) ]
- [ 0 // 15 fois (padding) ]
- [1 (pour remettre inst_counter à 1 et exécuter que la première instruction)]
On se sert du script python fourni (ou pas) pour rentrer notre combinaison, on clique sur start et boum ! le flag apparaît (youpi).