Writeup Reverse Engineering - Edition 2026
Table des matières
Dans cet article, nous vous présenterons les différentes solutions aux challenges du Jeanne d’Hack CTF 2026 pour la catégorie Reverse.
Reverse Intro
Les sources du challenge sont disponibles ici.
Challenge d’introduction au Reverse! Arriverez-vous à battre ce niveau démoniaque ?
Ce challenge est le premier de la catégorie Reverse Engineering. Le but du challenge est
d’introduire les débutants à cette catégorie. Pour ce faire, un unique fichier HTML est
fourni (index.html). Ce fichier contient une fonction checkFlag avec le code suivant:
// The flag check is not straightforward. Reverse engineering required!
function checkFlag() {
const input = document.getElementById('flagInput').value.trim();
const errorMsg = document.getElementById('errorMsg');
// Aahah nobody will ever guess this!
if (btoa(input.split('').reverse().join('')) === 'b3J0TmlfZXNyZVZlcl82MjAyX0tjNEhfM25OYUVK') {
errorMsg.textContent = '';
document.getElementById('congratsModal').style.display = 'flex';
document.getElementById('flag').textContent = 'JDHACK{' + input + '}';
} else {
errorMsg.textContent = 'Incorrect flag. Try again!';
}
}
La fonction btoa convertit une chaîne de caractères en base64. Toutes les opérations sont inversibles,
on peut donc récupérer le flag en les appliquant sur la chaîne “b3J0TmlfZXNyZVZlcl82MjAyX0tjNEhfM25OYUVK”
dans l’ordre inverse. On obtient ainsi :
>>> atob("b3J0TmlfZXNyZVZlcl82MjAyX0tjNEhfM25OYUVK").split("").reverse().join("")
"JEaNn3_H4cK_2026_reVerse_iNtro"
Snake Game
Les sources du challenge sont disponibles ici.
How about a quick game of Snake?
Ce challenge est le second de la catégorie Reverse Engineering. Cette fois-ci, on nous fournit un fichier Python dont le code est le suivant :
def check_flag(code):
# SSSssuper SSssecure!!!
return base64.b64encode(''.join(chr(((ord(c) + 2) % 256) ^ 0x23) for c in code)[::-1].swapcase().encode()).decode() == "dhpEd2YWFGJxRGRuYlIaYlpyaWJaclNOYlUKcxFlYlZETkFTVmIAUUBXZg=="
flag = input(">>> ")
if check_flag(flag):
print("Ssss Congratulations! You found the Sssecret flag! You can validate with: ")
print("JDHACK{" + flag + "}")
else:
print("\nGame Over! Try again.")
L’idée du challenge est la même que précédemment, mais avec plus d’opérations et plus de complexité, de manière à
augmenter la difficulté tout en restant abordable pour une personne qui n’a jamais fait de reverse engineering.
On peut décomposer la fonction check_flag comme ceci:
- On commence par effectuer un décalage de 2 (chiffrement de César).
- On XoR le résultat avec 0x23.
- On inverse la chaîne avec
[::-1]. - On utilise
swapcasepour échanger la casse des caractères obtenus. - On encode en base64 le résultat et on compare à une chaîne statique.
En effectuant ces opérations dans l’ordre inverse, on obtient:
import base64
encoded = "dhpEd2YWFGJxRGRuYlIaYlpyaWJaclNOYlUKcxFlYlZETkFTVmIAUUBXZg=="
decoded = base64.b64decode(encoded).decode()
swaped = decoded.swapcase()
reversed = swaped[::-1]
xored = ''.join(chr(ord(c) ^ 0x23) for c in reversed)
flag = ''.join(chr((ord(c) - 2) % 256) for c in xored)
print("Flag: JDHACK{" + flag + "}")
Lorsque l’on exécute le script précédent, on obtient:
Flag: JDHACK{cRaP!_SN@KES_d0n'T_KNoW_hoW_7O_keEp_53crE7s}
LoL
Depuis votre dernière partie de LoL, votre compte a été banni. Trouverez-vous un moyen de le réactiver ?
Les sources du challenge sont disponibles ici.
Ce challenge est le troisième de la catégorie Reverse Engineering et le premier à contenir un code exécutable. On nous fournit un programme compilé pour Linux sans les symboles. On peut décompiler le programme grâce à un logiciel comme Ghidra.
Le code de la fonction main est le suivant:
int main(void) {
fputs(&DAT_00104080,stdout);
fwrite("[League Of Losers] Game is starting\n",1,0x24,stdout);
fwrite("[League Of Losers] Error: cannot start game, you are BANNED!\n",1,0x3d,stdout);
do {
char *__s1 = (char *)FUN_001011c9();
if (__s1 == (char *)0x0) {
fwrite("[League Of Losers] Error: Fail to read input\n",1,0x2d,stdout);
return -1;
}
int iVar1 = strcmp(__s1,"help");
if (iVar1 == 0) {
FUN_00101624();
}
else {
iVar1 = strcmp(__s1,"play");
if (iVar1 == 0) {
iVar1 = FUN_00101278();
if (iVar1 == 0) {
fwrite("[League Of Losers] You are not banned anymore! But there are still other challenge s to solve ;)\n"
,1,0x60,stdout);
}
else {
fwrite("[League Of Losers] Error: cannot start game, you are BANNED!\n",1,0x3d,stdout);
}
}
else {
iVar1 = strcmp(__s1,"rank");
if (iVar1 == 0) {
fwrite("[League Of Losers] You current rank is: Dirt-III\n",1,0x31,stdout);
}
else {
iVar1 = strcmp(__s1,"unban");
if (iVar1 == 0) {
FUN_001014bd();
}
else {
iVar1 = strcmp(__s1,"quit");
if (iVar1 == 0) {
fwrite("[League Of Losers] Game shutting down\n",1,0x26,stdout);
return 0;
}
fprintf(stdout,"[League Of Losers] Error: Unknown command \'%s\'\n",__s1);
fwrite("[League Of Losers] Try \'help\' to see available commands\n",1,0x38,stdout);
}
}
}
}
free(__s1);
} while( true );
}
On peut voir que le programme propose un interpréteur minimaliste contenant plusieurs commandes. Parmi elles, on retrouve la commande “unban” qui, d’après son nom, peut nous permettre de réactiver notre compte, comme indiqué dans la description. Une fois renommée, la fonction possède le code suivant:
void unban(void) {
int iVar1 = FUN_00101278();
if (iVar1 == 0) {
fwrite("[League Of Losers] Error: Account is not banned\n",1,0x30,stdout);
}
fwrite("[League Of Losers] Attempting to unban account\n",1,0x2f,stdout);
fwrite("[League Of Losers] Need authentication, enter Admin password:\n",1,0x3e,stdout);
void *__ptr = (void *)FUN_001011c9();
if (__ptr == (void *)0x0) {
fwrite("[League Of Losers] Error: Fail to read input\n",1,0x2d,stdout);
}
else {
iVar1 = FUN_00101284(__ptr);
if (iVar1 == 0) {
fwrite("[League Of Losers] Error: Authentication failed\n",1,0x30,stdout);
}
else {
fwrite("[League Of Losers] Authentication successful\n",1,0x2d,stdout);
fprintf(stdout,
"[League Of Losers] Welcome back Admin! You left this last time you connected \'JDHACK {%s}\'\n"
,__ptr);
fwrite("[League Of Losers] Account UNBANNED!\n",1,0x25,stdout);
DAT_00104320 = 0;
}
free(__ptr);
}
return;
}
À partir des nombreuses chaînes affichées à l’écran via fwrite, on comprend que la fonction FUN_001011c9
permet de lire l’entrée de l’utilisateur et que la fonction FUN_00101284 se charge de l’authentification.
Pour valider le challenge, il faut faire en sorte que cette dernière renvoie une valeur non nulle.
Le code est le suivant:
int FUN_00101284(char *param_1) {
int result;
char *ptr;
for (ptr = param_1; *ptr != '\0'; ptr = ptr + 1) { }
if ((long)ptr - (long)param_1 == 0x1c) {
if (((((((param_1[10] == '7') && (param_1[5] == 'r')) && (param_1[0x19] == 'n')) &&
((*param_1 == 'y' && (param_1[0xe] == 'N')))) && (param_1[0x15] == 'l')) &&
(((((param_1[0xc] == 'r' && (param_1[0x12] == 'H')) &&
((param_1[0xb] == 'e' &&
(((param_1[0x1b] == '!' && (param_1[4] == '4')) && (param_1[1] == '0')))))) &&
((param_1[0x14] == '_' && (param_1[0xd] == 'I')))) && (param_1[3] == '_')))) &&
((((param_1[0x11] == '7' && (param_1[9] == 'n')) &&
((param_1[7] == '_' &&
(((param_1[2] == 'u' && (param_1[0x16] == 'E')) && (param_1[0x13] == 'e')))))) &&
(((param_1[0x10] == '_' && (param_1[0x17] == 'G')) &&
((param_1[8] == 'e' &&
(((param_1[6] == '3' && (param_1[0xf] == 'g')) &&
((param_1[0x1a] == 'D' && (param_1[0x18] == 'E')))))))))))) {
result = 1;
}
else {
result = 0;
}
}
else {
result = 0;
}
return result;
}
La fonction commence par vérifier que la taille de l’entrée est de 28 (0x1c) caractères, puis compare
un à un les caractères. Si on remet dans l’ordre les caractères, on obtient alors le flag suivant : y0u_4r3_en7erINg_7He_lEGEnD!
CTF:Go
Les sources du challenge sont disponibles ici.
Vous êtes un agent d’élite en mission pour désamorcer une bombe placée par un groupe de terroristes. Votre objectif : infiltrer le système de sécurité de la bombe et récupérer le code PIN nécessaire pour la neutraliser avant qu’il ne soit trop tard.
Les terroristes n’ont eu que le temps d’amorcer la bombe ce matin et ont laissé derrière eux une mystérieuse clé USB contenant le fichier
libpasscode.so. Ce fichier pourrait renfermer des indices précieux pour vous aider à désamorcer la bombe. Votre mission, si vous l’acceptez : utilisez vos compétences en rétro-ingénierie pour déchiffrer le contenu de ce fichier et désamorcer la bombe avant qu’il ne soit trop tard.
Pour ce challenge, nous disposons d’une bibliothèque C (libpasscode.so) permettant de générer et de vérifier les codes PIN
d’une bombe à désamorcer. L’objectif est de retrouver le code PIN actuel pour désamorcer la bombe.
D’après l’énoncé, nous savons que la bombe a été armée une seule fois et que cette action a eu lieu dans la journée.
La bibliothèque libpasscode.so contient les trois fonctions exportées suivantes:
arm_bombget_error_messagecheck_flag
En analysant la fonction check_flag, nous trouvons le code suivant:
int check_flag(char *pin) {
int iVar1;
uint uVar2;
time_t now;
char local_24 [20];
printf("[LibPasscode] Checking PIN: %s\n",pin);
iVar1 = strcmp(pin,"1337");
if (iVar1 == 0) {
uVar2 = 0xfffffac7; // -1337 as integer
}
else {
now = time((time_t *)0x0);
iVar1 = FUN_001011a0(((ulong)((SUB168(SEXT816(-0x7777777777777777) * SEXT816(now),8) + now >> 5)
- (now >> 0x3f)) % 0x5a0) / 10,local_24);
if (iVar1 == 0) {
iVar1 = strcmp(pin,local_24);
uVar2 = (uint)(iVar1 == 0);
}
else {
uVar2 = 0xffffffff;
}
}
return uVar2;
}
La fonction commence par comparer le PIN avec “1337”. En cas d’égalité, la valeur 0xfffffac7 est renvoyée. Sinon, le temps courant
est récupéré et passé en paramètre à la fonction FUN_001011a0 après avoir subi une série d’opérations arithmétiques. Cette
fonction possède le code suivant:
int FUN_001011a0(int param_1,long param_2) {
int fd;
ssize_t r;
int value;
ulong uVar1;
char c [9];
fd = open("pincode.txt",0);
value = fd;
if (fd != -1) {
uVar1 = 0;
value = 0;
while( true ) {
r = read(fd,c,1);
if (r != 1) break;
if ((c[0] == '\n') || (c[0] == '\r')) {
if (value == param_1) goto end;
value = value + 1;
uVar1 = 0;
}
else if ((value == param_1) && (uVar1 < 0xb)) {
*(char *)(param_2 + uVar1) = c[0];
uVar1 = uVar1 + 1;
}
}
if (value == param_1) {
end:
*(undefined1 *)(param_2 + uVar1) = 0;
value = 0;
close(fd);
}
else {
value = -2;
close(fd);
}
}
return value;
}
La fonction ouvre le fichier “pincode.txt” puis lit ligne par ligne jusqu’à atteindre la ligne correspondant au premier paramètre
(celui basé sur le temps courant). Lorsque l’indice demandé est atteint, la fonction copie la ligne courante dans la variable
param_2. Nous pouvons donc déduire que cette fonction récupère la ligne demandée par le premier paramètre et stocke le résultat
dans le second.
En revenant à la fonction check_flag, nous constatons que le résultat de la lecture du fichier est comparé à notre PIN.
En cas d’égalité, la valeur 1 est renvoyée, sinon 0.
Pour trouver le PIN, il est nécessaire de comprendre comment le fichier “pincode.txt” est généré. Cette génération est effectuée
dans la fonction arm_bomb, dont le code décompilé est le suivant (après une passe de rétro-ingénierie):
int arm_bomb(void) {
int fd;
int random_value;
ulong now;
ssize_t sVar1;
int c;
uint hash;
char *ptr;
char *pcVar2;
char *local_50;
char line [10];
char local_39 [9];
fd = open("pincode.txt",0x241,0x1a4);
c = fd;
if (fd != -1) {
now = time((time_t *)0x0);
c = 0x4c;
ptr = "LibPasscode, A secure way to verify your PIN!";
hash = 0;
do {
ptr = ptr + 1;
hash = hash * 0x21 + c;
c = (int)*ptr;
} while (*ptr != '\0');
c = 0x90;
srand((int)((now & 0xffffffff) / 600) * 600 ^ hash);
ptr = line;
do {
do {
random_value = rand();
pcVar2 = ptr + 1;
*ptr = "0123456789"[random_value % 10];
ptr = pcVar2;
} while (local_39 != pcVar2);
local_39[0] = '\0';
sVar1 = write(fd,line,10);
if ((sVar1 == -1) || (sVar1 = write(fd,"\n",1), sVar1 == -1)) goto fail;
c = c + -1;
ptr = line;
local_50 = line;
} while (c != 0);
do {
c = rand();
ptr = local_50 + 1;
*local_50 = "0123456789"[c % 10];
local_50 = ptr;
} while (local_39 != ptr);
local_39[0] = '\0';
sVar1 = write(fd,line,10);
if ((sVar1 == -1) || (sVar1 = write(fd,"\n",1), sVar1 == -1)) {
fail:
c = -4;
close(fd);
}
else {
c = close(fd);
c = (uint)(c != -1) * 4 + -3;
}
}
return c;
}
Nous pouvons faire les observations suivantes :
- Le générateur de nombres pseudo-aléatoires (PRNG) utilisé est celui de la bibliothèque standard C (
srand/rand). - Il est initialisé avec
srand(seed ^ hash(passphrase)), oùseedest une valeur basée sur le temps courant. - Le code PIN est généré caractère par caractère avec
rand() % len(lettres).
La fonction génère ensuite 0x90, soit 144 valeurs, suivies d’une dernière valeur, toutes écrites dans le fichier “pincode.txt”. Pour
retrouver le PIN nécessaire pour désamorcer la bombe, il faut parvenir à retrouver la graine utilisée pour initialiser le générateur
aléatoire. La valeur hachée étant constante, nous pouvons nous concentrer sur le temps. Il est possible de brute-forcer l’ensemble
des valeurs de temps possibles de la journée, mais comment détecter que l’on teste la bonne valeur ?
On se rappelle alors du code spécial “1337” présent dans la fonction check_flag. Il pourrait s’agir d’une backdoor permettant à la
personne ayant mis en place la bombe de la désactiver en cas d’erreur. En testant ce code sur la page web ou sur la version physique,
nous pouvons voir s’afficher une sorte de version au format LibPasscode v1.2.3-abcdef.
Cette version est renvoyée par la troisième et dernière fonction : get_error_message:
char * get_error_message(int param_1) {
int iVar1;
undefined1 local_14 [12];
if (param_1 != -1337) {
switch(param_1) {
case -5:
return "Error generating pincode";
case -4:
return "Error writing pincode file";
case -3:
return "Error closing pincode file";
case -2:
return "Error reading pincode file";
case -1:
return "Error opening pincode file";
default:
return "Unknown error";
}
}
iVar1 = FUN_001011a0(0x90,local_14);
if (iVar1 == 0) {
snprintf((char *)string,0x40,"LibPasscode v1.2.3-%s",local_14);
} else {
snprintf((char *)string,0x40,"LibPasscode v1.2.3-ERROR");
}
return string;
}
Dans le cas où le code d’erreur reçu est -1337, la fonction FUN_001011a0 est appelée afin de récupérer la dernière ligne du fichier
“pincode.txt”. Ce dernier est ensuite utilisé pour construire la version affichée à l’écran. Ainsi, lorsque l’on saisit le code “1337”,
on récupère en réalité le dernier PIN généré pour la journée. À partir de ce PIN, il est possible de brute-forcer le temps auquel le
fichier “pincode.txt” a été généré, de le générer nous-mêmes et de calculer le PIN courant en fonction de l’heure actuelle
(arrondie par tranche de 10 minutes).
La solution est la suivante :
-
Récupérer la version: On envoie le code spécial
1337via l’interface ou l’API, ce qui nous permet de récupérer la version, par exemple :LibPasscode v1.2.3-2a1b3c. -
Brute-force du seed: On suppose que la bombe a été armée aujourd’hui (minuit UTC).
- Pour chaque timestamp possible depuis minuit jusqu’à maintenant:
- On initialise le PRNG comme dans la bibliothèque C.
- On génère 145 codes PIN (144 emplacements + 1 version).
- Si le hash correspond à celui divulgué, nous avons trouvé la graine.
- Pour chaque timestamp possible depuis minuit jusqu’à maintenant:
-
Récupérer le code PIN courant: On calcule l’index de la tranche de 10 minutes actuelle, on prend le code PIN à cet index dans la liste générée et on le soumet via l’interface ou l’API pour désamorcer la bombe.
Le script exploit.py implémente cette solution :
python exploit.py --url http://<IP>:5000
[*] Getting leaked version string from server...
[+] Leaked version string: LibPasscode v1.2.3-4763682466
[+] Extracted version hash: 4763682466
[*] Brute-forcing seed for today...
[Brute-force] |================================================= | 99% ETA: 0ss
[+] Found seed: 1752788400
[+] Current slot: 130, PIN: 6858607877
[*] Submitting PIN to get the flag...
[+] Server response: JDHACK{1ook_l!K3_tH3_anT!-TERR0r157_h@Ve_woN}
Jeanne d’Hack RPG - Level I
Les sources du challenge sont disponibles ici.
Vous vous réveillez dans une sombre forêt, l’air frais rempli des hurlements hantés des loups. La panique vous saisit alors que vous réalisez que vous devez fuir les ombres se tapissant juste au-delà des arbres.
Allez-vous survivre à cette course ?
Ce challenge constitue le premier niveau d’une série de quatre défis portant sur le thème des RPG (Role Playing Game).
Chaque défi consiste en une bibliothèque partagée qu’il faut analyser pour obtenir un flag et progresser au niveau suivant. Dans ce premier niveau, le personnage se réveille au milieu de la forêt, perturbé par de sinistres bruits.
+==============================================================================+
| Awakening in the Forest |
+==============================================================================+
| |
| ( |
| ) |
| ( ( |
| ) |
| ( ( |
| ) /\ -( |
| ( // | (` |
| _ -.;_/ \\--._ |
| (_;-// | \ \-'.\ |
| ( `.__ _ ___,') |
| `'(_ )_)(_)_)' |
| |
+==============================================================================+
| You awaken to the sound of rustling leaves and distant howls echoing through |
| the dense forest. The flickering embers of your small campfire cast eerie |
| shadows around you, illuminating the rough bark of the trees that surround |
| your makeshift shelter. The night air is cool, and a sense of unease settles |
| in your stomach. |
| |
+==============================================================================+
| [0] You decide to investigate the source of the noise, feeling a mix of |
| curiosity and caution. |
| [1] You choose to ignore it and try to go back to sleep. |
+------------------------------------------------------------------------------+
À chaque étape, un ou plusieurs choix sont disponibles. Par exemple, si l’on choisit de retourner dormir, on finit dévoré par les loups et l’on meurt, ce qui recommence le niveau.
Pour échapper aux loups, il est possible d’analyser le code décompilé à l’aide d’un SRE comme Ghidra, ou plus simplement en testant toutes les combinaisons possibles. Au fur et à mesure des choix effectués, le personnage parvient à la porte d’un village. Cette porte est fermée et nécessite un mot de passe. En cas d’erreur, les loups rattrapent le personnage et le dévorent.
Afin de trouver le mot de passe, il est nécessaire d’utiliser Ghidra. Le premier niveau contient les symboles,
rendant son analyse relativement simple. En cherchant les chaînes de caractères liées à la porte du village,
on finit par trouver la fonction keep_moving_forward dont le code est le suivant :
int keep_moving_forward(void) {
int iVar1;
undefined8 choices;
undefined8 img;
char *input;
char *dup;
int c;
choices = create_choices();
choices_add(choices,"Give the code to enter the village.");
c = 0;
while( true ) {
if ((c == L'0') || (c == L'1')) {
choices_dispose(choices);
return 0;
}
img = get_image("level1.wooden_door");
c = window_frame("The Village",img,
"You manage to get out of the forest and stumble accross a small village.\n
However the wolves are behind you so you run as quickly as you can to the\n
entry do or of the village which is closed. You knock at the door with all\n
of your rema ing strength until someone yield at you: \n\n
\"What is the secret code ?\"\n"
,choices);
if (c == L'0') {
window_clear();
input = (char *)window_prompt("What is the secret code: ");
dup = strdup(input);
dup = (char *)enc(dup);
iVar1 = strcmp("B1ofs@urX1t4tswhwDeM2w2m1od",dup);
if (iVar1 == 0) {
choices_dispose(choices);
free(dup);
iVar1 = enter_village(input);
return iVar1;
}
choices_dispose(choices);
free(dup);
iVar1 = attack_by_wolves(2);
return iVar1;
}
if ((c == 0x1b) || (c == 0x71)) break;
window_msg("I did not understand what you are saying");
}
choices_dispose(choices);
return -1;
}
Le programme obtient l’entrée via l’appel à window_prompt, en effectue une copie grâce à strdup, puis
transmet cette copie à la fonction enc. Enfin, l’appel à strcmp vérifie si le résultat de la fonction enc
est égal à “B1ofs@urX1t4tswhwDeM2w2m1od”. Dans le cas contraire, le personnage est attaqué par les loups et perd.
La fonction enc se présente comme suit :
byte *enc(byte *input) {
for (byte *c = input; *c != 0; c = c + 1) {
*c = *c ^ 1;
}
return input;
}
Cette fonction exécute un XOR entre chaque caractère et la valeur 1. Étant donné que l’opération XOR est inversible, il est possible de retrouver le flag en appliquant enc à la chaîne statique trouvée dans le programme. Le code Python suivant permet d’afficher le mot de passe :
input_string = "B1ofs@urX1t4tswhwDeM2w2m1od"
xor_result = ''.join(chr(ord(c) ^ 1) for c in input_string)
print(xor_result)
Le résultat obtenu est C0ngrAtsY0u5urvivEdL3v3l0ne, ce qui permet d’entrer dans le village et de valider le niveau.
Jeanne d’Hack RPG - Level II
Les sources du challenge sont disponibles ici.
Émergeant dans le village, vous trouvez refuge parmi ses lumières chaleureuses. Mais en explorant, des chuchotements sur des trésors cachés et des outils oubliés vous attirent.
Quels secrets découvrirez-vous qui pourraient aider votre périple ?
Pour ce deuxième challenge, le joueur se retrouve dans le village au milieu de la nuit après avoir réussi à échapper aux loups. Pour avancer dans l’histoire, il faut dormir dans l’étable pour passer la nuit. Une fois le jour levé, le joueur peut s’aventurer dans le village : visiter des maisons abandonnées ou discuter avec des villageois. Ces derniers indiquent qu’un objet susceptible d’aider dans le périple se trouve à la taverne.
Arrivé à la taverne, le joueur peut jouer à un jeu de cartes contre un poivrot. Quel que soient les choix effectués, la partie se termine toujours par une défaite.
+==============================================================================+
| Tavern Challenge |
+==============================================================================+
| _____ |
| |A . | _____ |
| | /.\ ||A ^ | _____ |
| |(_._)|| / \ ||A _ | _____ |
| | | || \ / || ( ) ||A_ _ | |
| |____V|| . ||(_'_)||( v )| |
| |____V|| | || \ / | |
| |____V|| . | |
| |____V| |
| |
+==============================================================================+
| Welcome to the High Card Wins game! Draw a card and try to have the |
| highest card. You will play against the grizzled man. Good luck! |
| |
| You drew a Hearts (Value: 4). |
| |
+==============================================================================+
| [0] Discard you card to draw a new one |
| [1] Wait a turn |
| [2] End the game here |
+------------------------------------------------------------------------------+
En examinant la boucle du jeu de cartes, le pseudo-code suivant a été relevé (extraits pertinents) :
int FUN_00101d95(void) {
undefined1 buffer [32];
char desc [128];
char game_choices [9];
char *cards [4];
cards[3] = "Diamonds";
cards[2] = "Hearts";
cards[1] = "Clubs";
cards[0] = "Spades";
memset(game_choices,L'0',9);
snprintf(desc,0x400,
"Welcome to the High Card Wins game! Draw a card and try to have the \n
highest card. You will play against the grizzled man. Good luck!\n\n
You drew a %s (Value: %d).\n"
,cards[(long)(d4() + -1) + 2], d6());
void *choices = create_choices();
choices_add(choices,"Discard you card to draw a new one");
choices_add(choices,"Wait a turn");
choices_add(choices,"End the game here");
int turns = 5;
int c = 0;
do {
while( true ) {
do {
if ((((c == L'0') || (c == L'1')) || (c == L'2')) || (turns < 1)) {
/* End the game here | 5 turns passed */
choices_dispose(choices);
if (DAT_00106190 == '\0') {
game_choices[5] = '0';
}
else {
game_choices[5] = '1';
}
if (DAT_00106192 == '\0') {
game_choices[6] = '0';
}
else {
game_choices[6] = '1';
}
if (DAT_00106191 == '\0') {
game_choices[7] = '0';
}
else {
game_choices[7] = '1';
}
game_choices[8] = '\0';
FUN_001016c0(game_choices,8,buffer);
iVar1 = memcmp(buffer,&DAT_00106170,20);
if (iVar1 == 0) {
uVar2 = FUN_00101c02(game_choices);
}
else {
uVar2 = FUN_00101c02(0);
}
return uVar2;
}
turns--;
c = window_frame("Tavern Challenge",get_image("level2.cards"),desc,choices);
game_choices[turns] = (char)c;
} while (c == 0x32);
if (c < 0x33) break;
LAB_00101faf:
if ((c == 0x1b) || (c == 0x71)) {
choices_dispose(choices);
return -1;
}
window_msg("I did not understand what you are saying");
}
if (c == 0x30) {
/* Discard you card to draw a new one */
...
}
else {
/* Wait a turn */
...
}
} while( true );
}
Les choix du joueur sont stockés dans le tableau game_choices ; des bits supplémentaires
(1 ou 0) y sont ajoutés selon la valeur de variables globales
(DAT_00106190, DAT_00106191 et DAT_00106192). Lorsque le joueur choisit
« End the game here » ou lorsque 5 tours sont écoulés, le programme remplit
le tableau puis appelle FUN_001016c0.
void FUN_001016c0(undefined1 *param_1,uint param_2,long param_3) {
for (local_9 = 0; local_9 < 5; local_9 = local_9 + 1) {
*(undefined4 *)(param_3 + (ulong)local_9 * 4) =
*(undefined4 *)(&DAT_001060a0 + (long)(int)(uint)local_9 * 4);
}
local_20 = param_1 + (param_2 & 0xffffffc0);
local_18 = param_3;
for (local_80 = param_1; local_80 < local_20; local_80 = local_80 + 0x40) {
FUN_00101501(local_18,local_80);
}
local_21 = (byte)param_2 & 0x3f;
for (local_a = 0; local_a < local_21; local_a = local_a + 1) {
local_78[(int)(uint)local_a] = *local_80;
local_80 = local_80 + 1;
}
local_78[(int)(uint)local_21] = 0x80;
local_b = local_21;
while (local_b = local_b + 1, local_b < 0x40) {
local_78[(int)(uint)local_b] = 0;
}
if (0x37 < local_21) {
FUN_00101501(local_18,local_78);
for (local_c = 0; local_c < 0x38; local_c = local_c + 1) {
local_78[(int)(uint)local_c] = 0;
}
}
local_30 = &local_40;
local_40 = param_2 * 8;
local_38 = &local_3c;
local_3c = param_2 >> 0x1d;
FUN_00101501(local_18,local_78);
return;
}
void FUN_00101501(int *param_1,undefined8 param_2) {
for (local_9 = 0; local_9 < 0x10; local_9 = local_9 + 1) {
local_28[(int)(uint)local_9] = local_9;
}
FUN_001011f9(param_1,&local_48,param_2,local_28,&DAT_001060e0,&DAT_00106130,&DAT_00106164);
local_28[0] = 5;
for (local_a = 1; local_a < 0x10; local_a = local_a + 1) {
local_28[(int)(uint)local_a] = local_28[(int)(local_a - 1)] + 9 & 0xf;
}
FUN_001011f9(param_1,&local_68,param_2,local_28,&DAT_001060e0,&DAT_00106150,&DAT_00106169);
*param_1 = local_60 + local_44 + *param_1;
param_1[1] = param_1[1] + local_40 + local_5c;
param_1[2] = param_1[2] + local_3c + local_58;
param_1[3] = param_1[3] + local_38 + local_68;
param_1[4] = param_1[4] + local_48 + local_64;
iVar1 = *param_1;
*param_1 = param_1[1];
param_1[1] = param_1[2];
param_1[2] = param_1[3];
param_1[3] = param_1[4];
param_1[4] = iVar1;
return;
}
Le code de cette dernière et des sous-routines effectue de nombreuses opérations complexes
ressemblant à une fonction cryptographique. Le tableau DAT_001060a0 contient les valeurs suivantes :
DAT_001060a0
001060a0 01 23 45 67 undefined4 67452301h
001060a4 89 ab cd ef undefined4 EFCDAB89h
001060a8 fe dc ba 98 undefined4 98BADCFEh
001060ac 76 54 32 10 undefined4 10325476h
001060b0 f0 e1 d2 c3 undefined4 C3D2E1F0h
Ces constantes sont typiques d’implémentations de SHA-1,
ce qui suggère que FUN_001016c0 calcule un haché (SHA-1 ou similaire) du premier paramètre
et le stocke dans le troisième. Le résultat est ensuite comparé à un tableau statique via memcmp.
Le but consiste donc à trouver la série de choix produisant le haché attendu :
e1 51 67 57 d5 87 9a 29 61 bf 5d fd 44 13 7e 75 ef 0f f5 fa
Les cinq premiers octets du haché correspondent aux cinq choix de tours (caractères ‘0’, ‘1’ ou ‘2’) et les trois octets suivants dépendent des variables globales. L’analyse du code montre que ces variables sont positionnées à 1 quand le joueur effectue certaines actions :
- $choices[5] = 1$ si le joueur a dormi dans l’étable, 0 sinon.
- $choices[6] = 1$ si le joueur a visité la maison abandonnée, 0 sinon.
- $choices[7] = 1$ si le joueur a parlé au villageois avant d’aller à la taverne, 0 sinon.
Il est possible de tester toutes les combinaisons à la main ou automatiquement avec un outil comme John the Ripper.
$ echo "e1516757d5879a2961bf5dfd44137e75ef0ff5fa" > hash.txt
$ john --mask='?1?1?1?1?1?2?2?2' -1='[012]' -2='[01]' --format=Raw-SHA1 hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-SHA1 [SHA1 256/256 AVX2 8x])
Warning: no OpenMP support for this hash type, consider --fork=8
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
0g 0:00:00:00 N/A 0g/s 194400p/s 194400c/s 194400C/s 01022111..22222111
Session completed
En utilisant initialement le format Raw-SHA1, aucune combinaison ne donne le haché recherché,
ce qui indique soit une variante de SHA-1, soit un autre algorithme. La présence de la
constante 0x5C4DD124 permet d’identifier RIPEMD-160
comme candidat alternatif. En lançant John avec le format ripemd-160, l’outil trouve une solution :
$ john --mask='?1?1?1?1?1?2?2?2' -1='[012]' -2='[01]' --format=ripemd-160 hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (ripemd-160, RIPEMD 160 [32/64])
Warning: no OpenMP support for this hash type, consider --fork=8
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
02100101 (?)
1g 0:00:00:00 DONE (2025-11-07 20:17) 50.00g/s 64000p/s 64000c/s 64000C/s 10000101..10120101
Use the "--show" option to display all of the cracked passwords reliably
Session completed
La solution correspond donc aux actions suivantes à réaliser en jeu :
- Dormir dans l’étable.
- Ne pas visiter la maison abandonnée.
- Parler au villageois avant d’aller à la taverne.
- Aux tours de la taverne : tirer une nouvelle carte lors des deux premiers tours, passer le troisième tour, puis terminer la partie au quatrième tour.
En procédant ainsi, le joueur reçoit une épée utilisable pour la suite de l’aventure, ainsi que le flag : JDHACK{02100101}.
Jeanne d’Hack RPG - Level III
Les sources du challenge sont disponibles ici.
Avec un prix nouvellement acquis en main, votre aventure vous entraine dans les profondeurs sombre de la forêt. L’obscurité s’avance, éveillant à la fois excitation et peur…
Quelles mystères vous attendent à l’intérieur ?
Dans ce troisième défi, le joueur retourne dans la forêt du premier niveau. Cette fois, une étrange attraction dirige le joueur vers une grotte sombre. Quel que soit le choix initial, le joueur finit par se retrouver devant la porte d’un donjon. En entrant, le donjon se dévoile : plusieurs options sont alors proposées, par exemple se déplacer vers une pièce voisine ou fouiller la salle.
+==============================================================================+
| Dungeon |
+==============================================================================+
| -.._ | |-.._ | | |` .' |!____|_ |;' | _.! |
| | `-!._ | | `-!._ | `;;. . .-| | | | _!.i' |
| | |`-! | |`-! | ` | || |_._ __!_ !!.;' | |
| ``...__ | |``...__ | |`.;.i| |!-|| |' | | _|..- |
| |``--` |``--`_ | !| | ||____|_ _ `..!-' | |
| | | | | |`-_| |;' | ` | | | |
| ______|____!______|____!.!,.!| |!.,|._______ |_!..__|_____ |
| | | | | | | | | | || !' | | | |
| | | | | | |..!-i| |..||____|___ |. | | |
| | _!.-j | _!.-j | _.| || |' `| j `-!.._ | |
| _!.-'| | _!.-'| |..| ;| |`-.| __.___i |. |``-.. |
| ' | _.'' | _.' !-!| |. ||' ! ' ``._ | |
| |.| | |.| |.| | ||`!| __|____ |. | `.. |
| _.-' | . _.-' | . |.'/| |! |' | | ..| |-._ |
| .'| !.'|.'| !.'|.'| || | `.| ____-|_ | | `._ | |
| | .' | | .' | |//| |` ||. | |.! |.| |
| |_.'| .' |_.'| .' .' / \ || | ' `. | `._ |
| .' | .' | .' | .' |/| / \! | ___i__- | |`.| ` |
| ' !'| |' !'| | | / \|. | | | `. |
| |
+==============================================================================+
| Coordinate: (5, 5) |
| Facing: West |
| |
| You enter a flickering corridor. |
| |
| A chill runs down your spine as you step further inside. |
| |
+==============================================================================+
| [0] Right |
| [1] Forward |
| [2] Search the room for loot |
+------------------------------------------------------------------------------+
En avançant dans le donjon, le joueur rencontre des monstres qu’il faut combattre pour survivre. En recherchant les occurrences de la chaîne “Dungeon”, il est possible d’identifier la fonction responsable de l’exploration (après un peu de reverse engineering pour nommer les variables) :
int main_dungeon_loop(long dungeon,undefined8 player) {
int fight_cooldown = d4();
do {
if (player_is_alive(player) == 0) {
return 0;
}
int open_room_count = 0;
void *choices = create_choices();
// Iterate over the 4 possible directions to see if there is a open room
for (i = 0; (int)i < 4; i = i + 1) {
local_36 = *(ushort *)
(&DAT_00125280 + ((long)(int)i + (long)(int)(uint)local_1a * 4) * 2);
currentRoom = get_room(dungeon->current_room);
has_room = FUN_0010e928(currentRoom,i & 0xffff);
if (has_room) {
choices_add(choices,currentRoom);
// ...
open_room_count = open_room_count + 1;
}
}
choices_add(choices,"Search the room for loot");
// Dynamically create description about where the player is and which direction he is facing
poVar6 = std::operator<<(aoStack_238,"Coordinate: (");
currentRoom = get_room(dungeon->current_room);
uVar3 = FUN_0010e620(currentRoom);
poVar6 = (ostream *)std::ostream::operator<<(poVar6,uVar3);
poVar6 = std::operator<<(poVar6,", ");
currentRoom = get_room(dungeon->current_room);
uVar3 = FUN_0010e632(currentRoom);
poVar6 = (ostream *)std::ostream::operator<<(poVar6,uVar3);
poVar6 = std::operator<<(poVar6,")");
std::ostream::operator<<(poVar6,std::endl<>);
poVar6 = std::operator<<(aoStack_238,"Facing: ");
// Check cooldown and trigger a fight if needed
if ((is_visited(currentRoom) == '\x01') || (fight_cooldown < d20())) {
should_fight = false;
}
else {
should_fight = true;
}
if (should_fight) {
fight(dungeon,player);
// ...
}
else {
fight_cooldown = fight_cooldown + d4();
description = std::__cxx11::string::c_str();
player_choice = window_frame("Dungeon",get_image(),description,choices);
currentRoom = get_room(dungeon->current_room);
if ((player_choice == 0x1b) || (player_choice == 0x71)) {
quit = false;
}
else {
if ((player_choice < 0x30) || (0x38 < player_choice)) {
window_msg("I did not understand what you are saying");
}
else {
if ((player_choice + -0x30 < list_length(choices)) && (player_choice + -0x2f <= open_room_count)) {
move_to_next_room(dungeon,local_1a);
}
else {
player_choice = (player_choice + -0x30) - (uint)open_room_count;
if (player_choice == 0) {
player_choice = loot_room(dungeon,player);
}
else if (player_choice == 3) {
player_choice = enter_weird_room(dungeon,player);
}
if ((player_choice == 0x1b) || (player_choice == 0x71)) {
quit = false;
}
}
}
LAB_0010f899:
choices_dispose(choices);
quit = true;
}
}
} while( !quit );
}
Les fonctions ont été renommées à partir des chaînes de caractères trouvées dans les sous-routines. La boucle principale du jeu réalise les étapes suivantes :
- Vérification de l’état du joueur (mort/vivant).
- Construction de la liste des choix disponibles selon la salle courante (itération sur les 4 directions, ajout de l’option « fouiller la salle »).
- Génération dynamique de la description affichée (coordonnées et orientation).
- Détection et gestion d’un cooldown de combat : si la salle n’a pas été visitée et que le cooldown le permet, un combat est déclenché ; sinon, le joueur choisit une action via l’interface.
- Traitement du choix : changer de salle (
move_to_next_room), fouiller (loot_room) ou entrer dans une salle particulière (enter_weird_room). L’option permettant d’entrer dans la pièce étrange n’apparaît pas parmi les choix proposés.
Pendant un combat, le joueur affronte un monstre (gobelin, squelette ou orc) jusqu’à la mort de l’un des deux.
La structure player contient le nom et les statistiques (Force, Énergie magique, Points de vie, etc.).
gef> hexdump byte --size 34 $rsi
0x0000000012d1df70 74 65 73 74 00 00 00 00 00 00 00 00 00 00 00 00 test............
0x0000000012d1df80 00 00 00 00 00 00 03 00 0e 00 02 00 0f 00 10 00 ................
0x0000000012d1df90 12 00 ..
Il est possible de modifier ces valeurs en mémoire pour augmenter les points de vie du joueur ou infliger davantage de dégâts. Toutefois, éliminer les monstres ne permet pas d’obtenir le flag. (La violence ne serait donc pas la solution ?).
Si l’on regarde du coté de la fonction loot_room, le code est le suivant:
int loot_room(Dungeon *dungeon,void *player) {
string loot [40];
void *room = get_room(&dungeon->current_room);
get_loot(loot,room);
bool isEmpty = std::__cxx11::string::empty((char *)loot);
if (isEmpty) {
window_msg("There is nothing in this room");
}
else {
void *msg = std::__cxx11::string::c_str();
/* try { // try from 00111c8b to 00111cd8 has its CatchHandler @ 00111cee */
window_msg(msg);
std::__cxx11::string::operator=(loot,"");
room = get_room(&dungeon->current_room);
set_loot(room,loot);
}
std::__cxx11::string::~string(loot);
return 0;
}
La fonction loot_room effectue les opérations suivantes :
- Récupère le contenu (loot) de la pièce.
- Si la chaîne est vide, affiche « There is nothing in this room ».
- Sinon, affiche le contenu et vide le contenu de la salle (pour marquer la fouille).
Malheuresement, même après avoir fouillé l’ensemble du donjon, aucune salle ne semble contenir de flag.
La dernière piste intéressante est enter_weird_room. Cette fonction appelle une routine qui vérifie une condition ;
si celle-ci est vraie, la pièce courante reçoit un texte indiquant la présence d’une « weird room », le loot de la salle est mis à jour,
et une boucle similaire à la boucle principale propose de fouiller la salle ou de quitter.
int enter_weird_room(Dungeon *dungeon,void *player) {
// ...
current_room = get_room(&dungeon->field_0x40);
uVar1 = FUN_0010e632(current_room);
current_room = get_room(&dungeon->field_0x40);
uVar2 = FUN_0010e620(current_room);
current_room = get_room(&dungeon->current_room);
uVar3 = FUN_0010e632(current_room);
current_room = get_room(&dungeon->current_room);
uVar4 = FUN_0010e620(current_room);
ret = FUN_0010ef98(uVar4,uVar3,uVar2,uVar1);
if (ret == 1) {
// ...
current_room = get_room(&dungeon->current_room);
set_loot(current_room,local_4e8);
current_room = get_room(&dungeon->current_room);
std::__cxx11::string::string<>
(local_98,"What a weird room! You wonder how you ended up here!",&local_69);
FUN_0010e644(current_room,local_98);
choices = create_choices();
choices_add(choices,"Search the room for loot");
while( true ) {
current_room = get_room(&dungeon->current_room);
FUN_0010e66e(local_68,current_room);
local_34 = window_frame("Weird Room",get_image("level3.wall_xxx"),current_room,choices);
std::__cxx11::string::~string(local_68);
if ((local_34 == 0x1b) || (local_34 == 0x71)) break;
if (local_34 == 0x30) {
loot_room(dungeon,player);
choices_dispose(choices);
goto end;
}
window_msg("I did not understand what you are saying");
}
choices_dispose(choices);
end:
ret = 0x71;
std::__cxx11::string::~string(local_4e8);
}
else {
ret = 0;
}
return ret;
}
La condition à remplir est determiné par le résultat de la fonction FUN_0010ef98 :
int FUN_0010ef98(ushort param_1,ushort param_2,ushort param_3,ushort param_4) {
double dVar1;
double dVar2;
dVar1 = (double)std::pow<int,int>((uint)param_1 - (uint)param_3,2);
dVar2 = (double)std::pow<int,int>((uint)param_2 - (uint)param_4,2);
dVar1 = sqrt(dVar2 + dVar1);
return (int)(dVar1 + 0.5);
}
Renommée en distance(x1,y1,x2,y2), la logique devient claire : enter_weird_room vérifie si la salle courante est
suffisamment proche d’une autre salle (donnée en field_0x40). Si distance(…) retourne 1, la salle étrange
s’active et propose son contenu.
current_room = get_room(&dungeon->field_0x40);
y2 = get_y(current_room);
current_room = get_room(&dungeon->field_0x40);
x2 = get_x(current_room);
current_room = get_room(&dungeon->current_room);
y1 = get_y(current_room);
current_room = get_room(&dungeon->current_room);
x1 = get_x(current_room);
ret = distance(x1,y1,x2,y2);
En plaçant un point d’arrêt, il est possible de lire les paramètres passés à distance, par exemple :
0x7f733110ff98 (
$rdi = 0x0000000000000005,
$rsi = 0x0000000000000004,
$rdx = 0x0000000000000005,
$rcx = 0x0000000000000002
)
Dans l’état normal, la salle mystérieuse n’est pas directement accessible via le menu. Toutefois, l’option cachée conduit magiquement à cette salle si le joueur est suffisamment proche de la salle.
+==============================================================================+
| Weird Room |
+==============================================================================+
| -.._ | |-.._ | ___!_.__ | __._ |!____|___' | _.!|;' | _.! |
| | `-!._ | | `-!._ ! | ! | | _!.i' | | _!.i' |
| | |`-! | |`-! _.___|__ | __|_ _._ __!_._.;' | !!.;' | |
| ``...__ | |``...__ | | '|' | ' | _|..- | | _|..- |
| |``--` |``-- ___|_____|_____ ____|_ _ _!-' | `..!-' | |
| | | | | '| ! | | | | | |
| ______|____!______|_____ __..|..______|______-_____!..__|_____|_!..__|_____ |
| | | | | | | i !' | | | | | |
| | | | | | _ ____._!_______|_____|_____ | | |. | | |
| | _!.-j | _!.-` ' | | `|` '|' `-!.._ | j `-!.._ | |
| _!.-'| | _!.-'| _ _.__i____|____|_.___i___._ |``-..|. |``-.. |
| ' ┌──────────────────────────────────────────────────────────────────┐ |
| │ You found JDHACK{yOU_fOunD_ThE_$eCR3t_R0om} │ |
| _.│ │-._ |
| .'| │ Press any key to continue │ |
| | └──────────────────────────────────────────────────────────────────┘ |
| |_.'| .' |_.'| . ' | | | `. | `._ ' `. | `._ |
| .' | .' | .' | .' _i___ | __i__-___i___ | _ |`.| `| |`.| ` |
| ' !'| |' !'| .. | .. | `. | | `. |
| |
+==============================================================================+
| What a weird room! You wonder how you ended up here! |
| |
+==============================================================================+
| [0] Search the room for loot |
+------------------------------------------------------------------------------+
D’autres méthodes permettent d’atteindre le même résultat : modifier la valeur de retour de distance dans GDB ou patcher le saut conditionnel qui en dépend.
Jeanne d’Hack RPG - Level IV
Les sources du challenge sont disponibles ici.
En naviguant dans les profondeurs de la grotte, vous êtes confronté à une force inexpliquée. C’est un combat non seulement de force, mais de volonté.
Pouvez-vous maîtriser l’inconnu pour en sortir vainqueur ?
Dans ce niveau final, le joueur se réveille dans les profondeurs d’une grotte après que le sol se soit écroulé sous ses pieds dans la salle secrète du labyrinthe. En explorant la grotte, il finit par se retrouver face à un dragon. Quelles que soient les décisions prises par le joueur, le dragon s’avère trop puissant et terrasse celui-ci.
+==============================================================================+
| The Dragon's Lair |
+==============================================================================+
| |===-~___ _,-' |
| -==\\ `//~\\ ~~~~`---.___.-~' |
| ______-==| | | \\ _-~` |
| __--~~~ ,-/-==\\ | | `\ ,' |
| _-~ /' | \\ / / \ / |
| .' / | \\ /' / \ /' |
| / ____ / | \`\.__/-~~ ~ \ _ _/' / \/' |
|/-'~ ~~~~~---__ | ~-/~ ( ) /' _--~` |
| \_| / _) ; ), __--~~ |
| '~~--_/ _-~/- / \ '-~ \ |
| {\__--_/} / \\_>- )<__\ \ |
| /' (_/ _-~ | |__>--<__| | |
| |0 0 _/) )-~ | |__>--<__| | |
| / /~ ,_/ / /__>---<__/ | |
| o o _// /-~_>---<__-~ / |
| (^(~ /~_>---<__- _-~ |
| /__>--<__/ _-~ |
| |__>--<__| / .---_ |
| |__>--<__| | /' _--_~\ |
| |__>--<__| | /' / ~\`\|
| \__>--<__\ \ /' // |||
| ~-__>--<_~-_ ~--____---~' _/'/ /' |
| ~-_~>--<_/-__ __-~ _/ |
| ~~-'_/_/ /~~~~~~~__--~ |
| ~~~~~~~~~~ |
| |
+==============================================================================+
| You venture deeper into the cave system. The air grows warmer |
| with each step, and a faint red glow appears ahead. |
| |
| As you round a corner, the tunnel opens into a massive cavern. |
| Your heart stops. In the center lies an enormous dragon, |
| its golden scales reflecting the dim light. |
| |
+==============================================================================+
| [0] Try to sneak past quietly. |
| [1] Attack while it's sleeping. |
| [2] Back away slowly. |
+------------------------------------------------------------------------------+
La fonction qui gère le combat contre le dragon est la suivante, obtenue après une phase de rétro-ingénierie :
int dragon_battle(void *player)
{
int ret;
undefined8 uVar1;
undefined8 uVar2;
string local_48 [35];
allocator local_25;
int choice;
undefined8 choices;
bool won;
std::allocator<char>::allocator();
/* try { // try from 0010c92a to 0010c92e has its CatchHandler @ 0010cb0e */
std::__cxx11::string::string<>
(local_48,
"The dragon rises to its full height, towering above you."
"Its claws are like swords, i ts teeth like spears."
"This may be your final battle."
,&local_25);
std::allocator<char>::~allocator((allocator<char> *)&local_25);
/* try { // try from 0010c93b to 0010cafa has its CatchHandler @ 0010cb28 */
choices = create_choices();
choices_add(choices,"Strike at its heart.");
choices_add(choices,"Aim for its eyes.");
choices_add(choices,"Look for a weakness.");
ret = can_use_thuum(player);
if (ret == 0) {
choices_add(choices,"Use the Thu'um - the Voice.");
}
uVar1 = std::__cxx11::string::c_str();
uVar2 = get_image("level4.dragon_fire");
choice = window_frame("Dragon Battle",uVar2,uVar1,choices);
choices_dispose(choices);
if (choice == L'3') {
ret = can_use_thuum(player);
if ((ret == 0) && (ret = fight_dragon(), ret == 0)) {
won = true;
}
else {
won = false;
}
if (won) {
ret = show_win(player);
}
else {
ret = game_over_with_msg(player,"confused");
}
goto LAB_0010cafe;
}
if (choice < L'4') {
if (choice == L'2') {
ret = game_over_with_msg(player,"weakness");
goto LAB_0010cafe;
}
if (choice < L'3') {
if (choice == L'0') {
ret = game_over_with_msg(player,"heart");
goto LAB_0010cafe;
}
if (choice == L'1') {
ret = game_over_with_msg(player,"eyes");
goto LAB_0010cafe;
}
}
}
if ((choice == 0x1b) || (choice == 0x71)) {
ret = -1;
}
else {
ret = game_over_with_msg(player,"hesitation");
}
LAB_0010cafe:
std::__cxx11::string::~string(local_48);
return ret;
}
Le programme propose différents choix qui aboutissent à la mort du joueur de diverses manières. Un seul choix est différent, mais il
n’apparaît que si une certaine condition est remplie. Cette condition est vérifiée par la fonction can_use_thuum :
bool can_use_thuum(void *player) {
char string [19];
int i;
builtin_strncpy(string,"JDHACK",7);
i = 0;
while( true ) {
if (5 < i) {
return false;
}
if ((char)*(undefined2 *)((long)player + (long)i * 2 + 0x16) != string[i]) break;
i = i + 1;
}
return true;
}
Cette fonction vérifie si les statistiques du joueur forment la chaîne de caractères “JDHACK”. Par conséquent, il est nécessaire de créer le fichier de sauvegarde suivant avant de démarrer le jeu :
$ echo $(echo -e "4a444841434b504c4a65616e6e6500000000000000000000000000004a0044004800410043004b0000000000" | sed 's/../\\x&/g') > player.save
$ xxd player.save
00000000: 4a44 4841 434b 504c 4a65 616e 6e65 0000 JDHACKPLJeanne..
00000010: 0000 0000 0000 0000 0000 0000 4a00 4400 ............J.D.
00000020: 4800 4100 4300 4b00 0000 0000 0a H.A.C.K......
Une fois cette condition vérifiée, le joueur peut sélectionner l’option “Use the Thu’um”, qui appelle ensuite la fonction fight_dragon:
int fight_dragon(void) {
byte cVar1;
int ret;
size_t len;
void *s;
FILE *fp;
len = std::__cxx11::string::length(&DRAGON_WORDS);
s = std::__cxx11::string::c_str(&DRAGON_WORDS);
fp = fmemopen(s,len,"r");
if (fp == (FILE *)0x0) {
ret = 1;
}
else {
/* try { // try from 0010c71f to 0010c77a has its CatchHandler @ 0010c77e */
create_struct(&GLOBAL_STRUCT);
GLOBAL_FILE = fp;
parse_something();
fclose(fp);
do_loop(&GLOBAL_STRUCT);
cVar1 = get_flag(&GLOBAL_STRUCT);
if (cVar1 == 0) {
ret = get_top_value(&GLOBAL_STRUCT);
}
else {
ret = 1;
}
}
return ret;
}
La fonction commence par un appel à fmemopen, qui permet d’allouer un descripteur de fichier (FILE *) sans créer de fichier sur le système.
Une fois créé, ce descripteur est stocké dans une variable globale. Le pseudo-fichier contient une chaîne de caractères présente dans une string,
initialisée lors du chargement de la bibliothèque via les tableaux d’initialisation.
// Called by INIT_1
void _INIT_1_1(int param_1,int param_2) {
if ((param_1 == 1) && (param_2 == 0xffff)) {
std::__cxx11::string::string<>
((string *)&DRAGON_WORDS,
"Dah Osos Ruvaak Oblaan Dah Osos Ruvaak Onik Oblaan Ahst Osos Qethsegol Tah Bormah Ob laan Dah..." /* TRUNCATED STRING LITERAL */
,local_19);
__cxa_atexit(std::__cxx11::string::~string,&DRAGON_WORDS,&PTR_LOOP_00129a30);
}
return;
}
Cette chaîne semble contenir une série de mots dans un langage inconnu. En effectuant des recherches sur Internet, on découvre qu’il s’agit du langage draconique, comme indiqué sur cette page.
Si l’on revient à la fonction fight_dragon, celle-ci initialise une autre variable globale via create_struct:
void create_struct(astruct *param_1) {
param_1->field27_0x30 = 0;
param_1->field28_0x38 = 1;
std::vector<>::clear((vector<> *)param_1);
std::vector<>::reserve((vector<> *)param_1,0x100);
return;
}
Cette structure contient un vecteur C++ avec un espace initial de 100 éléments. La fonction fight_dragon appelle ensuite parse_something
avant de fermer le descripteur de fichier. Cette fonction, ainsi que ses sous-routines, réalise de nombreuses opérations, parmi lesquelles
on trouve ce message d’erreur : “fatal flex scanner internal error–no action found”. Ce message indique que le code utilise l’analyseur
syntaxique flex. On peut donc émettre l’hypothèse que la fonction parse_something lit le texte
écrit en draconique.
La fonction suivante, initialement appelée do_loop, possède le code suivant :
void do_loop(astruct *param_1) {
ulong uVar1;
undefined8 uVar2;
ulong uVar3;
bool run;
while( true ) {
uVar1 = param_1->field27_0x30;
uVar3 = FUN_00112bb6(¶m_1->field_0x18);
if ((uVar1 < uVar3) && (param_1->field28_0x38 != '\0')) {
run = true;
}
else {
run = false;
}
if (!run) break;
uVar2 = FUN_00112bda(¶m_1->field_0x18,param_1->field27_0x30);
FUN_0010d5fa(param_1,uVar2);
}
return;
}
La fonction contient une boucle infinie qui s’arrête lorsque certaines conditions sont remplies. La fonction FUN_0010d5fa
est appelée à chaque itération et contient un switch-case, qui dépend du second paramètre :
void FUN_0010d5fa(astruct *param_1, void *param_2) {
// ...
uVar1 = FUN_0011582a(param_2);
switch(uVar1) {
case 0:
uVar1 = FUN_0011583a(param_2);
FUN_0010dd76(param_1,uVar1);
break;
// ...
Cette structure est typique d’une machine virtuelle. Il s’agit d’une technique courante pour obfusquer un programme. Cette méthode consiste à créer un processeur virtuel avec son propre jeu d’instructions et des opérations propres à celui-ci. Il est donc possible de renommer les fonctions et les variables pour mieux comprendre le programme. Ainsi, on obtient la fonction suivante :
void run_vm(vm *vm) {
undefined8 instr;
ulong size;
ulong pc;
bool run;
while( true ) {
pc = vm->pc;
size = get_program_size(&vm->program);
if ((pc < size) && (vm->run != 0)) {
run = true;
}
else {
run = false;
}
if (!run) break;
instr = get_instruction(&vm->program,vm->pc);
execute_one_instruction(vm,instr);
}
return;
}
Le champ vm->program est un vecteur C++, qui possède donc la structure suivante :
template <class T, class A = std::allocator<T>>
class vector {
public:
// public member functions
private:
T* data_; // points to first element
T* end_; // points to one past last element
T* end_capacity_; // points to one past internal storage
A allocator_;
};
Il est possible de récupérer le code (souvent appelé Bytecode) de la machine virtuelle en plaçant un point d’arrêt sur la fonction
run_vm, puis en inspectant le contenu de la structure vm:
(gdb) b *0x00007ff5519e5434 # Break at run_vm
(gdb) x/3gx ($rdi+0x18) # Inspect vector
0x7ff551a01b98: 0x00007ff55133cb50 0x00007ff55133de00
0x7ff551a01ba8: 0x00007ff55133eb50
(gdb) x/2wx 0x00007ff55133cb50 # Inspect program data
0x7ff55133cb50: 0x00000000 0x00000003
# Dump the whole program
(gdb) dump binary memory bytecode.bin 0x00007ff55133cb50 0x00007ff55133de00
Une fois le programme récupéré, il est nécessaire d’analyser le switch-case pour comprendre comment les instructions sont structurées
ainsi que les différents codes d’opérations (Opcode). Le code de la fonction execute_one_instruction contient le pseudo-code suivant :
void execute_one_instruction(vm *vm,void *instr) {
int opcode = get_opcode(instr);
switch(opcode) {
case 0:
int value = get_operand(instr);
do_push(vm,value);
break;
case 1:
do_pop(vm);
break;
case 2:
...
}
}
L’analyse de la fonction permet d’identifier les opcodes suivants :
| Valeur | Nom | Description |
|---|---|---|
| 0 | PUSH | Ajoute une valeur sur la pile. |
| 1 | POP | Supprime la valeur du haut de la pile. |
| 2 | LOAD | Charge une valeur d’une adresse mémoire spécifique dans la pile. |
| 3 | STORE | Stocke la valeur du haut de la pile à une adresse spécifiée. |
| 4 | EXCH | Échange les deux valeurs du haut de la pile. |
| 5 | DUP | Duplique la valeur du haut de la pile. |
| 6 | ADD | Ajoute les deux valeurs du haut de la pile. |
| 7 | SUB | Soustrait la valeur du haut de la pile de la suivante. |
| 8 | MUL | Multiplie les deux valeurs du haut de la pile. |
| 9 | DIV | Divise la valeur du haut de la pile par la suivante. |
| 10 | XOR | Effectue un OU exclusif bit à bit sur les deux valeurs du haut. |
| 11 | AND | Effectue un ET bit à bit sur les deux valeurs du haut. |
| 12 | OR | Effectue un OU bit à bit sur les deux valeurs du haut. |
| 13 | EQU | Compare les deux valeurs du haut pour vérification d’égalité. |
| 14 | NOT | Effectue une opération NOT logique sur la valeur du haut. |
| 15 | LSS | Vérifie si la deuxième valeur du haut est inférieure à la première. |
| 16 | GTR | Vérifie si la deuxième valeur du haut est supérieure à la première. |
| 17 | LEQ | Vérifie si la deuxième valeur du haut est inférieure ou égale à la première. |
| 18 | GEQ | Vérifie si la deuxième valeur du haut est supérieure ou égale à la première. |
| 19 | NEG | Négatif la valeur du haut. |
| 20 | JMP | Saute à une adresse d’instruction spécifiée sans condition. |
| 21 | JFALSE | Saute si la valeur du haut est fausse (zéro). |
| 22 | JTRUE | Saute si la valeur du haut est vraie (non-zéro). |
| 23 | RET | Retourne d’un appel de fonction. |
| 24 | SYSCALL | Déclenche un “appel” système. |
| 25 | HALT | Arrête l’exécution du programme. |
Les instructions sont stockées sous la forme de deux entiers : l’opcode et l’opérande. À partir de cette table, il est possible d’écrire un désassembleur qui convertit le code binaire en représentation textuelle.
Voici un extrait du code du désassembleur :
class InstructionType:
PUSH = 0
POP = 1
...
INSTRUCTION_SET = {
InstructionType.PUSH: "push",
InstructionType.POP: "pop",
...
}
def instr_to_string(addr: int, opcode: int, operand: int = None) -> str:
instruction = "[{:04x}] ".format(addr)
instruction += INSTRUCTION_SET.get(opcode, "unknown")
if operand is not None:
instruction += " 0x{:x}".format(operand)
if 20 <= operand and operand <= 0x7f:
instruction += " '{}'".format(chr(operand))
return instruction
def disassemble(bytecode: list[int], start_addr: int = 0) -> str:
instructions = []
addr = start_addr
for i in range(0, len(bytecode), 2):
opcode = bytecode[i]
operand = bytecode[i + 1] if (i + 1) < len(bytecode) else None
instructions.append(instr_to_string(addr, opcode, operand=operand))
addr += 1
return "\n".join(instructions)
Et un extrait de sa sortie :
$ python disassembler.py bytecode.bin
[0000] push 0x3
[0001] push 0x1e ''
[0002] jmp 0xfb
[0003] push 0x7
[0004] push 0x11
[0005] push 0xb9
[0006] jmp 0xca
[0007] push 0xa
[0008] push 0xb9
[0009] jmp 0x106
[000a] jfalse 0x16 ''
[000b] push 0xe
[000c] push 0x40 '@'
[000d] jmp 0xfb
[000e] push 0x11
[000f] push 0xb9
[0010] jmp 0xfb
[0011] push 0x7d '}'
[0012] syscall 0x2
[0013] pop 0x0
[0014] push 0x0
[0015] jmp 0x1a ''
[0016] push 0x1
[0017] push 0x1a ''
[0018] push 0x70 'p'
[0019] jmp 0xfb
[001a] push 0xa
[001b] syscall 0x2
[001c] pop 0x0
[001d] halt 0x0
[001e] unknown 0x43 'C'
[001f] unknown 0x68 'h'
[0020] unknown 0x6f 'o'
[0021] unknown 0x6f 'o'
[0022] unknown 0x73 's'
[0023] unknown 0x65 'e'
[0024] unknown 0x20 ' '
[0025] unknown 0x79 'y'
[0026] unknown 0x6f 'o'
[0027] unknown 0x75 'u'
[0028] unknown 0x72 'r'
[0029] unknown 0x20 ' '
Une fois le bytecode désassemblé, il est possible de commencer l’analyse de celui-ci. Cette analyse peut se révéler complexe si elle est réalisée uniquement statiquement. Comme c’est souvent le cas lors d’une analyse de rétro-ingénierie, il est intéressant d’utiliser une approche dynamique en parallèle de l’analyse statique pour gagner du temps.
Il est possible d’étendre notre désassembleur pour permettre l’émulation du code. La fonction emulate permet d’émuler le bytecode de la machine virtuelle :
def emulate(bytecode: list[int], start_addr: int = 0) -> None:
stack = []
pc = start_addr # Program counter
while pc < len(bytecode):
opcode = bytecode[pc]
operand = bytecode[pc + 1]
# Execute the instruction
if opcode == InstructionType.PUSH:
stack.append(operand)
elif opcode == InstructionType.POP:
stack.pop()
elif ...:
else:
raise Exception("Unknown instruction: 0x{:x}".format(opcode))
pc += 2
On obtient ainsi la sortie suivante, similaire au comportement du programme réel :
$ python disassembler.py bytecode.bin
Choose your words carefully Dovah
AAAAAAAAAAAAAAAAAAAAAA
Language is Knowledge, Knowledge is Power.
You are not powerfull enough!
Il est possible d’afficher les différentes instructions exécutées lors de l’émulation, ce qui permet de tracer l’exécution :
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[01aa] dup 0x0
...
[01f0] ret 0x0
[000e] push 0xa
[0010] push 0xb9
[0012] jmp 0x106
[020c] push 0x254
[020e] exch 0x0
[0210] store 0x0
[0212] push 0x255
[0214] push 0x0
[0216] store 0x0
[0218] push 0x254
[021a] load 0x0
[021c] load 0x0
[021e] jfalse 0x24d
[0220] push 0x54 'T'
[0222] xor 0x0
[0224] push 0x37 '7'
[0226] equ 0x0
...
En analysant la trace d’exécution, on remarque que le programme effectue une série de comparaisons, toutes précédées d’opérations arithmétiques (add, sub, mul, xor, etc.). Il est possible de modifier notre émulateur pour afficher les valeurs manipulées par ces opérations.
Ainsi, on obtient la trace suivante :
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
...
[0222] xor a: 65, b: 84
[0226] equ a: 21, b: 55
[022c] add a: 0, b: 0
[023c] add a: 185, b: 1
[024a] mul a: 65, b: 89
[024e] equ a: 5785, b: 4272
[0254] add a: 0, b: 0
[0264] add a: 186, b: 1
[0272] xor a: 65, b: 109
[0276] equ a: 44, b: 35
...
Si l’on s’intéresse aux quatre premières opérations, on remarque que la valeur a du xor correspond au code ASCII de ‘A’. Cette valeur, xorrée avec 85, produit le résultat (21), qui est ensuite comparé à 55. Les deux opérations “add” suivantes semblent se répéter pour toutes les comparaisons et peuvent donc être ignorées. En ne conservant que les opérations de comparaison et l’opération qui les précède, on obtient :
[0222] xor a: 65, b: 84
[0226] equ a: 21, b: 55
[024a] mul a: 65, b: 89
[024e] equ a: 5785, b: 4272
[0272] xor a: 65, b: 109
[0276] equ a: 44, b: 35
[029a] sub a: 65, b: 58
[029e] equ a: 7, b: 45
[02c2] div a: 65, b: 38
[02c6] equ a: 1, b: 3
[02ea] sub a: 65, b: 73
[02ee] equ a: -8, b: 24
[0312] xor a: 65, b: 56
[0316] equ a: 121, b: 108
[033a] add a: 65, b: 117
[033e] equ a: 182, b: 200
[0362] xor a: 65, b: 58
[0366] equ a: 123, b: 101
[038a] div a: 65, b: 68
[038e] equ a: 0, b: 1
[03b2] div a: 65, b: 48
[03b6] equ a: 1, b: 1
[03da] mul a: 65, b: 110
[03de] equ a: 7150, b: 12980
[0402] xor a: 65, b: 118
[0406] equ a: 55, b: 54
[042a] xor a: 65, b: 98
[042e] equ a: 35, b: 41
[0452] mul a: 65, b: 49
[0456] equ a: 3185, b: 5145
[047a] add a: 65, b: 106
[047e] equ a: 171, b: 184
Il est possible d’extraire et de résoudre des contraintes sur notre entrée permettant de valider chacun des tests. Par exemple :
a ^ 84 == 55 => a == 55 ^ 84 => a == 99 ('c')
En résolvant chacune de ces contraintes, on obtient alors l’entrée suivante : “c0NgraTS_D0v@KiN”. Nous pouvons vérifier notre flag dans l’émulateur :
$ python disassembler.py bytecode.bin
Choose your words carefully Dovah
c0NgraTS_D0v@KiN
Congratulations, you can validate with:
JDHACK{c0NgraTS_D0v@KiN}