Nous commençons par regarder le contenu du fichier de licence de Junior :
curl https://hackropole.fr/challenges/fcsc2024-reverse-fifty-shades-of-white/public/license-walter-white-junior.txt
----BEGIN WHITE LICENSE----
TmFtZTogV2FsdGVyIFdoaXRlIEp
1bmlvcgpTZXJpYWw6IDFkMTE3Yz
VhLTI5N2QtNGNlNi05MTg2LWQ0Y
jg0ZmI3ZjIzMApUeXBlOiAxCg==
-----END WHITE LICENSE-----
Cela ressemble au format ASCII Armored utilisé par PGP ou OpenSSL pour stocker clés et certificats (un marqueur de début, un marqueur de fin et une chaîne en base64 au milieu).
Nous réalisons un one-liner pour tenter de décoder la licence :
curl https://hackropole.fr/challenges/fcsc2024-reverse-fifty-shades-of-white/public/license-walter-white-junior.txt 2>/dev/null \
| sed -e '1d' -e '$d' \
| base64 -d
Name: Walter White Junior
Serial: 1d117c5a-297d-4ce6-9186-d4b84fb7f230
Type: 1
Nous analysons le programme dans Ghidra pour trouver comment la licence est contrôlée.
undefined8 main(int param_1,undefined8 *param_2) {
int iVar1;
void *local_28;
void *local_20;
if (param_1 != 2) {
printf("Usage: %s <license.txt>\n",*param_2);
exit(1);
}
iVar1 = parse(param_2[1],&local_28);
if (iVar1 == 0) {
puts("Invalid license!");
}
else {
check(&local_28);
}
return 0;
}
On appelle une première fonction appelée parse
à laquelle on passe le nom du fichier de licence et un pointeur vers une zone dans laquelle la licence décodée va nous être retournée.
On note en analysant le code que la lecture du fichier de licence ne tolère aucun écart (pas de lignes blanches en début ou fin ou d’espaces supplémentaires).
// param_1: Nom du fichier de licence
// param_2[0]: Pointeur vers le nom de la licence
// param_2[1]: Pointeur vers le numéro de série de la licence
//
undefined8 parse(char *param_1,undefined8 *param_2)
// Une zone de travail est allouée de la taille en octets
// du fichier de licence, elle est pointée par local_18
//
stat local_c8;
iVar1 = lstat(param_1,&local_c8);
local_18 = (char *)calloc(local_c8.st_size,1);
// Le fichier de licence est lu est placé vers local_18
// local_20 contient le nombre d'octets lus
//
local_20 = read(local_c,local_18,local_c8.st_size);
// Le fichier de licence doit commencer par
// "----BEGIN WHITE LICENSE----\n" et terminer par
// "-----END WHITE LICENSE-----\n"
//
iVar1 = strncmp(local_18,"----BEGIN WHITE LICENSE----\n",0x1c);
iVar1 = strncmp(local_18 + (local_20 - 0x1c),"-----END WHITE LICENSE-----\n",0x1c);
// On décode les données en base64
//
local_28 = (char *)b64d(local_18 + 0x1c,local_20 - 0x38);
// On récupère le nom de la licence
//
iVar1 = strncmp(local_28,"Name: ",6);
local_38 = strchr(local_30,10);
pcVar3 = strndup(local_30 + 6,(size_t)(local_38 + (-6 - (long)local_30)));
*param_2 = pcVar3;
// On récupère ensuite le numéro de série de la licence
//
local_30 = local_38 + 1;
iVar1 = strncmp(local_30,"Serial: ",8);
local_38 = strchr(local_30,10);
pcVar3 = strndup(local_30 + 8,(size_t)(local_38 + (-8 - (long)local_30)));
param_2[1] = pcVar3;
// On récupère le type de la licence:
// - Le type de la licence est numérique, il est lu par atoi
// - Il n'est pas renvoyé vers l'appelant
//
//
iVar1 = strncmp(local_30,"Type: ",6);
local_38 = strchr(local_30,10);
iVar1 = atoi(local_30 + 6);
*(int *)(param_2 + 2) = iVar1;
Nous regardons comment est vérifiée la licence, si le type de licence vaut ‘1’ nous avons une licence, une licence administrateur a un type égal à 0x539 (1337 en décimal) :
void check(undefined8 *param_1 {
int iVar1;
iVar1 = validate(*param_1,param_1[1]);
if (iVar1 == 0) {
puts("Invalid license!");
}
else if (*(int *)(param_1 + 2) == 1) {
printf("Valid license for %s!\n",*param_1);
}
else if (*(int *)(param_1 + 2) == 0x539) {
printf("Valid admin license for %s!\n",*param_1);
show_flag();
}
else {
puts("Invalid license, but nice try! Here: https://www.youtube.com/watch?v=dQw4w9WgXcQ");
}
return;
}
Il nous suffit donc de générer un nouveau fichier licence avec le numéro de série et le nom que nous avons déjà en modifiant le type de licence pour mettre 1337 au lieu de 1.
#!/usr/bin/python3
import textwrap
import base64
from pwn import *
licence = "Name: Walter White Junior\n" + \
"Serial: 1d117c5a-297d-4ce6-9186-d4b84fb7f230\n" + \
"Type: 1337\n"
licence_base64 = '\n'.join(textwrap.wrap(base64.b64encode(licence.encode("ascii")).decode('ascii'), width=27))
EXPLOIT = f"----BEGIN WHITE LICENSE----\n{licence_base64}\n-----END WHITE LICENSE-----\n"
print(EXPLOIT)
conn = remote('localhost',4000)
print(conn.recvuntil(b'Walter White Junior'))
conn.send(EXPLOIT.encode("ascii"))
conn.send(b'\n')
print(conn.recvuntil(b'[*]').decode('ascii'))
Le script suivant permet de synchroniser les opérations :
#!/bin/bash
set -e
if [ ! -f docker-compose.yml ]; then
wget https://hackropole.fr/challenges/fcsc2024-reverse-fifty-shades-of-white/docker-compose.public.yml -O docker-compose.yml
fi
docker-compose up -d
while ! nc -z localhost 4000; do sleep 1; done
python3 021_Fifty_shades_of_white.py
docker-compose down
Le résultat est le suivant :
Creating network "021_fifty_shades_of_white_default" with the default driver
Creating 021_fifty_shades_of_white_fifty-shades-of-white_1 ... done
----BEGIN WHITE LICENSE----
TmFtZTogV2FsdGVyIFdoaXRlIEp
1bmlvcgpTZXJpYWw6IDFkMTE3Yz
VhLTI5N2QtNGNlNi05MTg2LWQ0Y
jg0ZmI3ZjIzMApUeXBlOiAxMzM3
Cg==
-----END WHITE LICENSE-----
[+] Opening connection to localhost on port 4000: Done
b'[*] Send empty lines to mark the end of your inputs.\n[*] Give me a valid admin license for username: Walter White Junior'
Valid admin license for Walter White Junior!
Well done! Here is the flag for the Junior challenge:
FCSC{xxxxxxxx}
[*]
[*] Closed connection to localhost port 4000
Stopping 021_fifty_shades_of_white_fifty-shades-of-white_1 ... done
Removing 021_fifty_shades_of_white_fifty-shades-of-white_1 ... done
Removing network 021_fifty_shades_of_white_default