LE BLOG DE RICK Chasseurs de succès Linux Random WU

Par Rick dans WU le 17/04/2021.


La bombe

Cet article est le numéro 3 de la série WU Midnight 2021.

  1. WU Midnight 2021
  2. NSA Flag Checker & La grande évasion
  3. La bombe

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
void phase_3(char *param_1)

{
  int iVar1;
  uint uVar2;
  size_t sVar3;
  undefined4 *puVar4;
  undefined4 *puVar5;
  undefined4 local_6a;
  undefined4 local_66;
  undefined2 local_62;
  undefined local_60;
  undefined local_5f [3];
  undefined4 auStack92 [14];
  undefined4 uStack36;
  int local_20 [4];

  _local_5f = 0x33323130;
  uStack36 = 0x7a7978;
  iVar1 = -(int)(local_5f + 3);
  uVar2 = (uint)((int)local_20 + iVar1) >> 2;
  puVar4 = (undefined4 *)
           ("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +
           -(int)(local_5f + iVar1));
  puVar5 = (undefined4 *)(local_5f + 3);
  while (uVar2 != 0) {
    uVar2 = uVar2 - 1;
    *puVar5 = *puVar4;
    puVar4 = puVar4 + 1;
    puVar5 = puVar5 + 1;
  }
  local_6a = 0x30c260a;
  local_66 = 0x31043733;
  local_62 = 0x30c;
  local_60 = 0;
  readline(param_1);
  sVar3 = strlen(param_1);
  if (sVar3 != 10) {
    explode_bomb();
  }
  local_20[0] = 0;
  while (local_20[0] < 10) {
    if (param_1[local_20[0]] != local_5f[*(char *)((int)&local_6a + local_20[0])]) {
      explode_bomb();
    }
    local_20[0] = local_20[0] + 1;
  }
  puts("Sh*t, you actually managed to defuse the bomb! Well done :/");
  return;
}

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.

break pour fonction

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.

code asm désassemblé

nouveau break

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.

registres