[ECW 2023] - Shellboy
Introduction
Ce challenge provient de la phase de qualification de l’ECW 2023.
Shellboy est un challenge de pwn se jouant sur un émulateur de GameBoy (WASMBoy). Les ressources données sont :
- Le code source
- La cartouche compilée
- Une API Web pour interagir avec l’instance distante
Premiers pas
Dans ce jeu, nous pouvons définir une liste d’instruction permettant à notre petit personnage de se déplacer de haut en bas et de droite à gauche. En effet, il est possible :
- De naviguer dans la liste d’instruction avec
left
etright
- D’ajouter une instruction avec la touche
A
et son type avec les flèches directionnelles (up
,down
,left
,right
) - De supprimer une instruction (et ses répétitions) avec le bouton
B
- De préciser le nombre d’instruction du même type avec les touches directionnelles
up
etdown
jusqu’à 255 - De fusionner ou de bouger deux instructions entre elles en appuyant sur
select
pour la première etselect
pour la seconde. Si les deux instructions sont de même type, alors elles seront fusionnées (addition de leur répétition). Si elles sont différentes, elles changeront de place dans la liste. - De lancer la simulation avec le bouton
start
Vulnérabilité
En jouant un petit peu avec le jeu (en appuyant sur toute les touches comme un bourrin 😂) , je me suis rendu compte d’un comportement anormal avec la fonctionnalité de fusion des instructions.
En effet, j’ai remarqué qu’en fusionnant une instruction avec elle-même, un comportement étrange se produit :
Le curseur de sélection n’est pas changé, mais la liste d’instruction diminue tout de même. En répétant le processus un certain nombre de fois, on passe tout a coup de 0 instructions à 255.
En effet, il est maintenant possible de parcourir plus de 16 cases avec le curseur. Il est temps de se pencher sur le code source.
Dans le fichier inst_list.c
, on remarque la fonction move_inst()
:
|
|
On remarque dans ce code, que lorsque deux instructions sont de même type, et que la somme de leurs répétitions est inférieure à 255, alors on supprime la deuxième stack d’instructions. Or, dans notre cas, les instructions sont les mêmes !
|
|
Lorsque la stack d’instructions est supprimée, la variable globale inst_count
est décrémentée. Mais ce n’est pas tout !
|
|
Lorsqu’on supprime normalement une instruction avec la touche B
, la variable inst_cusror_pos
est remise à 0
. Si inst_count = 0
, alors list_empty
devient True
.
Cependant, on remarque que la fonction move_inst()
est appelée sans que inst_count
ne soit vérifiée et inst_cursor_pos
réinitialisée. Cela permet de décrémenter inst_count
et de l’underflow pour la faire passer à 255
, tandis que inst_cursor_pos
reste à la même position.
|
|
De plus, on observe que la variable inst_cursor_pos
est utilisée pour accéder aux valeurs contenues dans le tableau inst_rpt[]
.
Ce tableau contient des valeurs comprises entre 0
et 255
, correspondant au nombre de répétitions de chaque instruction. En parallèle, le tableau inst_ids[]
, contient le type d’instruction.
Dans un cas normal, ces tableaux ont une taille maximale de 16. Cependant, la variable inst_cursor_pos
est comprise entre 0
et inst_count
Si inst_count
est supérieure à 16
, alors nous avons un dépassement de tableau (Out-of-Bound) sur les tableaux inst_rpt[]
et inst_ids[]
.
Ecriture arbitraire
Grâce à l’observation précédente, il est ainsi possible d’écrire des valeurs comprises entre 0
et 255
dans la mémoire , entre inst_rpt
et inst_rpt + 255
.
Il va être nécessaire de se faire une idée de l’emplacement des variables en mémoire pour exploiter cela.
Représentation de la mémoire
Cette capture montre la représentation en mémoire des variables :
Et le tableau avec les adresses :
Variable | Adresse |
---|---|
inst_funcs[] | 0xC0B1 à 0xC0B8 |
inst_rpt[] | 0xC0B9 à 0xC0C8 |
inst_ids[] | 0xC0C9 à 0xC0D8 |
inst_count | 0xC0D9 |
inst_cursor_pos | 0xC0DA |
bot_x | 0xC0E1 |
bot_y | 0xC0E2 |
Exploitation
Exécution de code arbitraire
Comme le décrit ce petit morceau de code, le but du challenge est d’aller lire le flag à l’adresse 0x06FA
.
|
|
Une première idée était d’exploiter le tableau de pointeurs inst_funcs[]
définis ci-dessous :
|
|
Cependant, inst_func[]
est situé avant inst_rpt[]
. On ne peut donc pas réécrire un pointeur de fonction.
Mais il est possible de regarder comment les fonctions sont appelées !
|
|
La fonction à appeler est récupérée dans inst_func[]
grâce à l’id de l’instruction. Cette id est présent dans inst_ids[]
. L’id d’une instruction est normalement compris entre 0
et 3
.
Si l’id a une valeur supérieure à 3
, alors, le pointeur de fonction récupéré est situé en dehors de inst_func
, entre autre dans une zone que l’on contrôle, précisément au tout début de int_rpt[]
!
inst_func[0]
–> 0xC0B1inst_func[1]
–> 0xC0B3inst_func[2]
–> 0xC0B5inst_func[3]
–> 0xC0B7inst_func[4]
–> 0xC0B9
On peut le vérifier avec le désassemblé :
|
|
Sachant que nous pouvons écrire dans inst_ids[]
la valeur de l’id que l’on veut, il ne reste plus qu’à exploiter !
Payload
L’idée générale est :
- Ecrire à partir de l’adresse
0xC0B9
(inst_rpt[0]
) un pointeur de fonction qui pointe vers0xC0BB
(inst_rpt[2]
), carinst_rpt
est un tableau deuint_8
. - Ecrire notre shellcode pour afficher le flag à partir de l’adresse
0xC0BB
. - Ecrire la valeur
4
dansinst_ids[0]
permettant de produire le comportement décris ci-dessus - Notre payload doit avoir une taille inférieur à 32 octets.
Le shellcode permettant d’afficher le flag est le suivant :
|
|
Enfin, le payload en hexadécimale est le suivant :
|
|
Code final
Une fois notre payload construit, il ne reste plus qu’à interagir avec l’instance distante :
|
|
Flag
Et voici le flag !