On reçoit un étrange fichier qui est en fait le controleur d’une bombe ! Il faut réussir à passer toutes les étapes pour la désamorcer sans soucis.
Débuts
Lorsqu’on lance le fichier, on nous demande d’entrer le bon mot de passe. J’ouvre le fichier exécutable dans Ghidra afin de voir comment est fait le code. En lisant le main
, je remarque qu’il va falloir trouver 3 flags au total pour avoir le final.
undefined4 main(void)
{
void *pvVar1;
void *pvVar2;
void *pvVar3;
pvVar1 = calloc(0x10,1);
pvVar2 = calloc(0x10,1);
pvVar3 = calloc(0x10,1);
signal(2,handler);
puts(
"Welcome to my fiendish little bomb! You have 3 phases in which to blow yourself up!\nHave anice day :D"
);
phase_1(pvVar1);
phase_2(pvVar2);
phase_3(pvVar3);
printf("You can submit this flag now... MCTF{%s_%s_%s}\n",pvVar1,pvVar2,pvVar3);
return 0;
}
Phase 1
Heuresement que Ghidra génère le code C à partir de l’assembleur, le flag saute aux yeux directement. Une bête comparaison avec et c’est plié, phase suivante !
void phase_1(char *param_1)
{
int iVar1;
readline(param_1);
iVar1 = strcmp(param_1,"s3as0n4l");
if (iVar1 != 0) {
explode_bomb();
}
puts("Well done! You have defused phase 1!");
return;
}
Phase 2
La phase 2 est un peu plus compliquée. Cependant, j’ai déjà eu à faire avec ce genre de code, c’est « juste » une simple équation.
void phase_2(char *param_1)
{
size_t sVar1;
readline(param_1);
sVar1 = strlen(param_1);
if (sVar1 != 5) {
explode_bomb();
}
if ((int)param_1[2] != (*param_1 + 1) * 2 ||
((((int)param_1[2] != param_1[1] + 0x10 || (int)param_1[1] != *param_1 + 0x22) ||
(int)param_1[3] != param_1[2] + 7) || (int)param_1[4] != param_1[3] + 0xb)) {
explode_bomb();
}
puts("Forwards, trooper!");
return;
}
Je sors Z3 dans un script Python et j’écris mon algo. Grand prince en plus, la taille de la chaîne de caractères est directement indiquée ! Comme c’est une chaine de caractères, je fais un tableau de taille 5 (vu que c’est la taille de la chaine précisée dans le code) contenant des BitVec d’une taille de 8 bits (la taille du type char en C).
Je crée mon solveur et y ajoute les différentes équations sans oublier de rajouter l’extension de bits vu que le code cast des char en int (ça passe de 8 à 32 bits).
from z3 import *
flag = [BitVec(f"flag[{i}]", 8) for i in range(5)]
s = Solver()
s.add(ZeroExt(24, flag[2]) == ZeroExt(24, ((flag[0] + 1)*2)))
s.add(ZeroExt(24, flag[2]) == ZeroExt(24, (flag[1] + 0x10)))
s.add(ZeroExt(24, flag[1]) == ZeroExt(24, (flag[0] + 0x22)))
s.add(ZeroExt(24, flag[3]) == ZeroExt(24, (flag[2] + 7)))
s.add(ZeroExt(24, flag[4]) == ZeroExt(24, (flag[3] + 0xb)))
Une fois les équations posées, je lance le solveur et parse ensuite son résultat pour mettre l’indice dans le tableau flag en clé du résultat dans un dictionnaire. Cela me permet d’automatiser l’affichage de la clé.
if (str(s.check()) == "sat"):
# on parse la chaine produite pour s.model pour ne garder que index=resultat
flagDic = str(s.model()).replace(',', '').replace("flag[", '').replace(']', '').replace(' ', '').split("\n")
flagDic[0] = flagDic[0][1:] # pour se débarasser du [
final = {}
for i in flagDic:
key, value = i.split('=')
final[int(key)] = int(value)
for key in sorted(final):
print(chr(final[key]), end='')
print()
Phase 3
Paradoxalement, le code de la phase 3 pour le résoudre est plus simple que la phase 2. Cependant, il faut bien comprendre le code avant de le faire.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
Tout d’abord, je remarque à nouveaux qu’on nous donne la taille du flag : 10 caractères. Je vois aussi que local_20 est juste là pour nous brouiller les pistes, ça ne derait pas être un tableau mais juste un int. C’est un itérateur de boucle que je vais appeler i. Les lignes 42 à 46 nous le montre bien.
La ligne 42 va tester donc chaque lettre de la réponse donnée avec… un drole de truc. J’ai mis un bout de temps à comprendre ce que c’était.
La variable local_5f reçoit une adresse mémoire à la ligne 18. Elle n’est plus touchée par la suite. En regardant le code assembleur, Ghidra me montre gentillement ce que contient cette adresse mémoire. Ce n’est rien d’autre que la liste de caractères qui est mise dans puVar4 plus loin.
Le programme va donc comparer chaque lettre du flag entré avec certains caractères d’une liste. À partir de cet instant-là, j’ai mis beaucoup de temps avant de comprendre comment le programme marchait alors que c’est plutôt simple.
La solution
Les variables local_6X contiennent des valeurs hexadécimales. Le stokage en mémoire se fait en little endian, on aura donc une mémoire qui ressemble à ça :
00000100 0a 26 0c 03
00000104 33 37 04 31
00000108 0c 03 00 00
0000010c 00 00 00 00
Le programme récupère la valeur se trouvant à ((int)&local_6a + local_20[0])
du tableau de caractères. Cette adresse est ensuite caster sur un bit. Le programme va chercher l’adresse de local_6a, l’additionner avec i et ne prendre que l’octet se trouvant à cette adresse.
Le premier caractère sera l’indice 10 dans le tableau (= pointeur) local_5f, ensuite 0x26, etc. jusqu’à 03 qui se trouve à la dixième place, comme la taille de la chaine. Il est alors facile de faire un script python pour trouver le mot final.
alpha = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
indexHexa=[0xa, 0x26, 0xc, 0x3, 0x33, 0x37, 0x04, 0x31, 0xc, 0x3]
for i in indexHexa:
print(alpha[i], end='')
print()
Comment j’ai fait à 2h30 du matin
Si ça semble simple ici, à 2h30 du matin je n’avais pas la tête claire ! J’ai utilisé GDB pour mieux comprendre comment le programme marche (merci à Leeo sur le discord Gnous pour tous les conseils et explications).
Tout d’abord, il faut charger le point d’entrée. Je lance le programme avec gdb et je peux alors charger les informations du fichier avec info files
. Connaissant les fonctions et où je souhaite m’arreter, je fais break *phase_3
afin de pouvoir m’arreter au début la fonction et de voir comment elle marche exactement.
Je passe alors les premiers challs avec les réponses précédentes et lors du break point, je tape disassemble
pour voir le code assembleur et notamment la position de chaque instruction. En s’appuyant sur le code Ghidra, je trouve où m’arreter afin d’étudier les registres.
Après avoir remis ce nouveau break point (ou plutot ces), je tape n’importe quoi (par ex: aaaaaaaaaa) et je regarde les registres avec info registers
. Je ne vois pas le registre AL qui contient l’index du tableau local_5f. On peut le trouver avec la commande info registers al
. Et là on voit la valeur hexadécimale et décimal. Et j’ai alors remarqué que ça vaut 0xa, puis 0x26, etc.