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éeto_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 à avoirint main(int argc, char ** argv)
- renommer local_c en
i
, puisqu’il s’agit d’un index variant dans une bouclefor
- sVar2/local_20 en
len_password
, puisqu’il s’agit de la longueur deargv[1]
, cad le mdp donné en paramètre au binaire - local_18 en
buffer
, car cette variable reçoit le buffer alloué viamalloc
.
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!