Solution de Nuliel pour Strike

intro reverse linux x86/x64

25 août 2024

Analyse préliminaire

nuliel@nuliel-Latitude-E7270:~/ctf/hackropole/FCSC2024/reverse/strike$ file strike 
strike: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=26be85f80fa50c84292c92b5c33f31c0df228c31, for GNU/Linux 3.2.0, not stripped

On voit que le binaire n’est pas strippé. Il reste donc les symboles de debug.

Pour obtenir le flag, on va faire de l’analyse statique avec Ghidra pour comprendre le binaire et résoudre le challenge avec un script python.

Analyse statique

Deux fonctions sont intéressantes : main et a.

Fonction main

On commence par consulter les chaines de caractères présentes dans le binaire (Search -> For Strings). Deux chaines sont intéressantes :

  • # congratulations! this is a strike :-) you should now see the flag printed ... # : nommée to_check, probablement parce que notre mdp sera comparé indirectement à to_check
  • Nice! The flag is: FCSC{%s}\n, le message qu’on cherche à afficher. Il a le nom DAT_001021d0

Il n’y a qu’une seule référence à DAT_001021d0: dans le main, après la comparaison local_1c == 0. L’objectif est donc d’avoir local_1c == 0

On peut faire quelques renommages et changements de types pour rendre le code décompilé plus compréhensible :

  • renommer les deux paramètres de la fonction main et adapter les types, de sorte à avoir int main(int argc, char ** argv)
  • renommer local_c en i, puisqu’il s’agit d’un index variant dans une boucle for
  • sVar2/local_20 en len_password, puisqu’il s’agit de la longueur de argv[1], cad le mdp donné en paramètre au binaire
  • local_18 en buffer, car cette variable reçoit le buffer alloué via malloc.

Au passage, on remarque la condition if (argc == 2), ce qui montre que le binaire n’attend bien qu’un seul paramètre.

Après renommage, on obtient le code suivant :

int main(int argc,char **argv)
{
  int iVar1;
  size_t len_password;
  byte local_21;
  uint len_password1;
  int local_1c;
  void *buffer;
  uint i;
  
  buffer = (void *)0x0;
  local_1c = -1;
  if (argc == 2) {
    len_password = strlen(argv[1]);
    len_password1 = (uint)len_password;
    if (((len_password & 1) == 0) &&
       (buffer = malloc(len_password & 0xffffffff), buffer != (void *)0x0)) {
      for (i = 0; i < len_password1; i = i + 2) {
        iVar1 = a(argv[1][i],argv[1][i + 1],&local_21);
        if ((iVar1 != 0) || (0x22 < local_21)) goto LAB_0010145a;
        *(char *)((ulong)(i >> 1) + (long)buffer) = charset[(local_21 + i) % 0x23];
      }
      if ((len_password1 - 0xa2 < 2) &&
         (iVar1 = memcmp(buffer,to_check,(ulong)(len_password1 >> 1)), iVar1 == 0)) {
        local_1c = 0;
      }
    }
  }
LAB_0010145a:
  if (buffer != (void *)0x0) {
    free(buffer);
  }
  if (local_1c == 0) {
    printf(&DAT_001021d0,argv[1]);
  }
  else {
    puts("[-] Error ...");
  }
  return local_1c;
}

Fonction a

Cette fonction est appelée dans le main avec un caractère du mdp passé en paramètre, le caractère suivant, et local_21 passé en référence, probablement pour retourner un résultat.

iVar1 = a(argv[1][i],argv[1][i + 1],&local_21);

Le code de la fonction a est le suivant :

undefined8 a(undefined param_1,undefined param_2,byte *param_3)
{
  byte local_a;
  char local_9;
  
  switch(param_1) {
  case 0x30:
    local_9 = '\0';
    break;
  case 0x31:
    local_9 = '\x01';
    break;
  case 0x32:
    local_9 = '\x02';
    break;
  case 0x33:
    local_9 = '\x03';
    break;
  case 0x34:
    local_9 = '\x04';
    break;
  case 0x35:
    local_9 = '\x05';
    break;
  case 0x36:
    local_9 = '\x06';
    break;
  case 0x37:
    local_9 = '\a';
    break;
  case 0x38:
    local_9 = '\b';
    break;
  case 0x39:
    local_9 = '\t';
    break;
  default:
    return 0xffffffff;
  case 0x61:
    local_9 = '\n';
    break;
  case 0x62:
    local_9 = '\v';
    break;
  case 99:
    local_9 = '\f';
    break;
  case 100:
    local_9 = '\r';
    break;
  case 0x65:
    local_9 = '\x0e';
    break;
  case 0x66:
    local_9 = '\x0f';
  }
  switch(param_2) {
  case 0x30:
    local_a = 0;
    break;
  case 0x31:
    local_a = 1;
    break;
  case 0x32:
    local_a = 2;
    break;
  case 0x33:
    local_a = 3;
    break;
  case 0x34:
    local_a = 4;
    break;
  case 0x35:
    local_a = 5;
    break;
  case 0x36:
    local_a = 6;
    break;
  case 0x37:
    local_a = 7;
    break;
  case 0x38:
    local_a = 8;
    break;
  case 0x39:
    local_a = 9;
    break;
  default:
    return 0xffffffff;
  case 0x61:
    local_a = 10;
    break;
  case 0x62:
    local_a = 0xb;
    break;
  case 99:
    local_a = 0xc;
    break;
  case 100:
    local_a = 0xd;
    break;
  case 0x65:
    local_a = 0xe;
    break;
  case 0x66:
    local_a = 0xf;
  }
  *param_3 = local_a | local_9 << 4;
  return 0;
}

La fonction a est constituée de deux switch. En regardant une table ASCII, on peut voir que les switch prennent un caractère parmi 0, … 9, a, b, c, d, e, f, et convertissent en un entier (4 bits pour chaque switch). On en déduit que le mdp attendu est sous format hexadécimal.

On a aussi la confirmation que le 3ème paramètre de la fonction sert à retourner une valeur :

*param_3 = local_a | local_9 << 4;

En résumé, la fonction a prend en entrée deux caractères du mot de passe sous forme hexadécimale (en d’autres termes un octet), et convertit cela en un entier.

Retour à la fonction main

On peut maintenant comprendre le fonctionnement du programme:

  • Le mdp attendu est sous forme hexadécimal, et est converti par la fonction a en entiers
  • Ces entiers ne doivent pas être strictement supérieurs à 0x22
  • Le buffer alloué via malloc est initialisé avec la ligne
*(char *)((ulong)(i >> 1) + (long)buffer) = charset[(local_21 + i) % 0x23];
  • Le buffer est ainsi dérivé du mot de passe passé en paramètre
  • le mot de passe a au maximum 2 + 0xa2 caractères
  • si le buffer est identique à to_check, on a gagné

Script python

On peut passer à l’écriture d’un script qui va partir de to_check et inverser toutes les étapes décrites précédemment pour obtenir le mdp en entrée.

to_check = "# congratulations! this is a strike :-) you should now see the flag printed ... #"

charset = "abcdefghijklmnopqrstuvwxyz!# $:-()."

def convert(res: int) -> str:
    if res <= 9:
        return chr(0x30 + res)
    elif res <= 15:
        return chr(0x61 - 10 + res)
    else:
        raise ValueError

flag = "FCSC{"
for i in range(len(to_check)):
    letter = to_check[i]
    res_a = (charset.index(letter) - 2 * i) % 0x23

    part1 = res_a & 0x0f
    part2 = (res_a & 0xf0) >> 4

    flag += convert(part2) + convert(part1)

flag += "}"
print(flag)

On obtient le flag FCSC{1b1a2108051f051503021a0d1e111512151b1b10020109111e030b10071e1d190e0e061c1c1b1b140e02060c00161b1f140a21100f15190d201e11061b160913170a0e221313080b0f211e121614120a07}

Merci à rbe pour ce challenge!