Outil utilisé pour ce challenge : Cutter
Avant d’aller plus loin, il faut connaître le fonctionnement de malloc et de memcmp.
Voici le programme principal de l’exécutable strike
:
int32_t main(int argc, char **argv)
{
int32_t iVar1;
char **s;
uint64_t var_2ch;
uint8_t var_21h; // Reçoit la valeur décodée par la fonction a() (0x00 à 0xFF, mais attendu <= 0x22)
uint32_t size; // Stocke la longueur de l’argument (argv[1])
int32_t var_1ch;
void *ptr; // Pointeur sur mémoire allouée (simule un buffer)
int64_t var_ch; // Variable de boucle (utilisée comme index, incrémentée de 2 en 2)
ptr = (void *)0x0; // Initialisation du pointeur à NULL
var_1ch = -1; // Par défaut, on suppose l'échec (code d’erreur)
// Vérification des conditions suivantes :
// 1) On a bien 2 arguments (donc argv[1] existe).
// 2) On calcule size = strlen(argv[1]) puis on vérifie que size est pair (size & 1 == 0).
// 3) On alloue "size" octets dans ptr (malloc), et on vérifie que l’allocation réussit.
// cf référence malloc : https://koor.fr/C/cstdlib/malloc.wp
if (((argc == 2) && (size = strlen(argv[1]), (size & 1) == 0))
&& (ptr = (void *)malloc(size), ptr != (void *)0x0))
{
// On parcourt la chaîne de l'argument d’entrée, 2 caractères à la fois :
// var_ch prend les valeurs 0, 2, 4, etc., jusqu’à "size" (exclu).
for (var_ch._0_4_ = 0; (uint32_t)var_ch < size; var_ch._0_4_ = (uint32_t)var_ch + 2) {
// Appel de la fonction a() avec deux caractères successifs de argv[1].
// La fonction a() :
// - décode ces deux caractères (ex : '3' et 'a' vont donner 0x3A),
// - écrit la valeur résultante (0x00..0xFF) dans var_21h,
// - retourne 0 si la paire est valide, sinon 0xffffffff (ou -1).
iVar1 = a(
(uint64_t)(uint8_t)argv[1][(uint32_t)var_ch], // Premier caractère (poids fort)
(uint64_t)(uint8_t)argv[1][(uint32_t)var_ch + 1], // Deuxième caractère (poids faible)
(int64_t)&var_21h // Pointeur où stocker l’octet résultat
);
// Si la fonction a() renvoie une erreur (iVar1 != 0) ou que var_21h > 0x22 (dernier index de la chaîne),
// on saute directement à la fin (goto code_r0x0000145a).
// (0x22 = 34 décimal, équivaut à la taille - 1 de la chaîne "abcdefghijklmnopqrstuvwxyz!# $:-().")
if ((iVar1 != 0) || (0x22 < var_21h))
goto code_r0x0000145a;
// Ici, on écrit dans le buffer "ptr" le caractère issu de la chaîne
// "abcdefghijklmnopqrstuvwxyz!# $:-()." à l’indice (var_21h + var_ch) % 0x23.
// - (uint32_t)var_ch >> 1 est équivalent à var_ch / 2
// - var_21h + var_ch est le calcul de l’index
// - % 0x23 = modulo 35, la longueur de la chaîne de référence
// Donc ptr[var_ch/2] = "abcdefghijklmnopqrstuvwxyz!# $:-()."[(var_21h + var_ch) % 35].
*(char *)((uint64_t)((uint32_t)var_ch >> 1) + (int64_t)ptr) =
"abcdefghijklmnopqrstuvwxyz!# $:-()."[(var_21h + (uint32_t)var_ch) % 0x23];
}
// Une fois la boucle terminée, on vérifie deux conditions pour valider le "flag":
// 1) (size - 0xa2 < 2) équivalent à size == 0xa2 (162 en hexadécimal), la taille requise.
// 2) Si memcmp(ptr,
// "# congratulations! this is a strike :-) you should now see the flag printed ... #",
// size >> 1) == 0,
// alors var_1ch = 0 (succès).
// Note : size >> 1 = size / 2 = 81 octets comparés (puisque size = 162).
// cf référence memcmp : https://koor.fr/C/cstring/memcmp.wp
if ((size - 0xa2 < 2) &&
(iVar1 = memcmp(ptr, "# congratulations! this is a strike :-) you should now see the flag printed ... #",
size >> 1), iVar1 == 0)) {
var_1ch = 0; // Code de succès
}
}
code_r0x0000145a:
// Libération de la mémoire si ptr n’est pas NULL
if (ptr != (void *)0x0) {
free(ptr);
}
// Si var_1ch == 0, on affiche le flag ; sinon, message d'erreur.
if (var_1ch == 0) {
printf("[+]\xa0Nice! The flag is: FCSC{%s}\n", argv[1]);
} else {
puts("[-] Error ...");
}
return var_1ch; // Retourne 0 si succès, -1 sinon
}
Voici la fonction a()
appelée dans le programme principal, qui n’est là que pour convertir les 2 nibbles (half-bytes) sous forme de caractères ASCII hexadécimaux (0-9, a-f) en octets :
// Fonction qui prend 2 nibbles et les converti en un octet.
// ---------------------------------------------------------
// arg1 : caractère hexadécimal (poids fort)
// arg2 : caractère hexadécimal (poids faible)
// arg3 : pointeur où stocker l’octet résultant
// return : 0 si les caractères sont valides, sinon -1
undefined8 a(int64_t arg1, int64_t arg2, int64_t arg3)
{
int64_t var_28h;
int64_t var_1ch;
uint8_t var_ah; // Représente le "poids faible" de l'octet final
int64_t var_9h; // Représente le "poids fort" de l'octet final (avant décalage)
// Premier switch : Décodage du poids fort (arg1)
// ------------------------------------------------
// 'arg1' et 'arg2' sont chacun un caractère en ASCII (ex: '3', 'b', etc.).
// Ici, on convertit arg1 (entre '0'..'9' ou 'a'..'f') en sa valeur hexadécimale (0..15).
switch((char)arg1) {
case '0':
var_9h = 0;
break;
case '1':
var_9h = 1;
break;
case '2':
var_9h = 2;
break;
case '3':
var_9h = 3;
break;
case '4':
var_9h = 4;
break;
case '5':
var_9h = 5;
break;
case '6':
var_9h = 6;
break;
case '7':
var_9h = 7;
break;
case '8':
var_9h = 8;
break;
case '9':
var_9h = 9;
break;
case 'a':
var_9h = 10;
break;
case 'b':
var_9h = 11;
break;
case 'c':
var_9h = 12;
break;
case 'd':
var_9h = 13;
break;
case 'e':
var_9h = 14;
break;
case 'f':
var_9h = 15;
break;
default:
// Si le caractère ne correspond pas à un hexadécimal valide,
// la fonction retourne 0xffffffff (soit -1), signalant l’erreur.
return 0xffffffff;
}
// Deuxième switch : Décodage du poids faible (arg2)
// ------------------------------------------------
// On convertit ici le second caractère (arg2) de la même façon.
switch((char)arg2) {
case '0':
var_ah = 0;
break;
case '1':
var_ah = 1;
break;
case '2':
var_ah = 2;
break;
case '3':
var_ah = 3;
break;
case '4':
var_ah = 4;
break;
case '5':
var_ah = 5;
break;
case '6':
var_ah = 6;
break;
case '7':
var_ah = 7;
break;
case '8':
var_ah = 8;
break;
case '9':
var_ah = 9;
break;
case 'a':
var_ah = 10;
break;
case 'b':
var_ah = 11;
break;
case 'c':
var_ah = 12;
break;
case 'd':
var_ah = 13;
break;
case 'e':
var_ah = 14;
break;
case 'f':
var_ah = 15;
break;
default:
// Même logique d’erreur si arg2 n’est pas un caractère hexadécimal valide.
return 0xffffffff;
}
// Construction de l’octet final : (poids fort << 4) + poids faible
// ----------------------------------------------------------------
// Exemple :
// var_9h = 0x3 (caractère '3'), var_ah = 0xa (caractère 'a') => 0x3A
// Puis on stocke cet octet dans la mémoire pointée par arg3, donc var_21h ici.
*(uint8_t *)arg3 = var_ah | (char)var_9h << 4;
// Retour 0 => succès
return 0;
}
Le programme strike
prend un argument en ligne de commande, vérifie que sa longueur est paire pour le découper en paires de caractères, les convertit en octets stockés dans un buffer, puis les assemble. Si le buffer décodé correspond au message "# congratulations! this is a strike :-) you should now see the flag printed ... #"
, le programme affiche le flag.
Pour décoder le message, il faut comprendre comment la ligne suivante fonctionne :
ptr = "abcdefghijklmnopqrstuvwxyz!# $:-()."[(var_21h + var_ch) % 35]
var_21h est un octet décodé par la fonction a().
var_ch est un compteur incrémenté de 2 en 2.
"abcdefghijklmnopqrstuvwxyz!# $:-()."
est la chaîne de référence de 35 caractères, qu’on nommera str_ref.
% 35 est utilisé pour s’assurer que l’index reste dans les limites de la chaîne de référence dont la longueur est de 35 caractères.
ptr est utilisé pour stocker le caractère situé à l’index [(var_21h + i) % 35] de la chaîne "abcdefghijklmnopqrstuvwxyz!# $:-()."
à la position var_ch/2.
Donc pour retrouver la chaîne qui va faire afficher le flag (qu’on va nommer str_flag), il faut procéder caractère par caractère. Pour chaque caractère, il faut trouver son index, entier compris entre 0 et 34.
On a :
idx = str_ref.find(str_flag[i]) # index du caractère de str_flag dans str_ref (0-34)
et d’après le code source :
idx = (var_21h + i * 2) % 35
donc :
var_21h = (idx - i * 2) % 35
autrement dit :
var_21h = (str_ref.find(str_flag[i]) - i * 2) % 35
Maintenant que le principe est compris, on peut écrire un script Python pour automatiser la conversion des caractères en chaîne hexadécimale, et ainsi retrouver le flag.
#!/usr/bin/env python3
import sys
str_ref = "abcdefghijklmnopqrstuvwxyz!# $:-()."
def main():
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <chaine>")
sys.exit(1)
str_flag = sys.argv[1]
results = []
length = len(str_flag)
for i in range(length):
# Calcul de l'index
idx = (str_ref.find(str_flag[i]) - i * 2) % 35
# Conversion en hexadécimal sur 2 caractères
results.append(f"{idx:02x}")
# Affichage final : on sépare les hex par des espaces
print("".join(results))
if __name__ == "__main__":
main()
Voici ce que cela donne :
$ python3 strike_decode.py "# congratulations! this is a strike :-) you should now see the flag printed ... #"
1b1a2108051f051503021a0d1e111512151b1b10020109111e030b10071e1d190e0e061c1c1b1b140e02060c00161b1f140a21100f15190d201e11061b160913170a0e221313080b0f211e121614120a07
Si on exécute le programme strike
avec cet argument, on obtient le flag :
./strike 1b1a2108051f051503021a0d1e111512151b1b10020109111e030b10071e1d190e0e061c1c1b1b140e02060c00161b1f140a21100f15190d201e11061b160913170a0e221313080b0f211e121614120a07
[+] Nice! The flag is: FCSC{1b1a2108051f051503021a0d1e111512151b1b10020109111e030b10071e1d190e0e061c1c1b1b140e02060c00161b1f140a21100f15190d201e11061b160913170a0e221313080b0f211e121614120a07}