Writeup StarHack CTF: Challenge - Chaos
J’ai participé au StarHack CTF 2025, et voici le write-up du challenge “Chaos” que j’ai résolu.
Ce challenge était dans la catégorie “Binary” ; il nous était demandé d’exploiter un binaire pour obtenir un drapeau au format flag{...}.
Nous avions accès au binaire Linux compilé et à une session netcat pour exécuter le binaire sur les serveurs du CTF.
Analyse du binaire
Première étape, j’ai regardé les informations de base sur le binaire avec la commande file chaos pour obtenir quelques infos :
chaos: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e2937d93ac55bc47823fba02aa98e4b407ea01a3, for GNU/Linux 3.2.0, not strippedLe binaire est un ELF 64-bit pour Linux (sympa car je suis sous NixOS ❄️), non stripped, ce qui est une bonne nouvelle car nous pouvons avoir les noms des fonctions.
Je vérifie rapidement avec la commande strings s’il y a des chaînes intéressantes :
Enter password:Error reading input.Password too short.Executing VM...Invalid password.···Welcome to the multi-stage VM challenge!Can you decrypt and execute the hidden bytecode?Patching bytecode with input...:*3$"UGCC: (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0Nous voyons que c’est un challenge de Machine Virtuelle (VM), où nous devons déchiffrer et exécuter du bytecode caché. Le programme demande un mot de passe, et il y a des messages d’erreur pour la lecture de l’entrée et si le mot de passe est trop court.
Si je cherche des références pour “flag”, nous trouvons notamment la fonction print_flag et le fichier flag.txt.
$ strings chaos | grep flagflag.txtError: Could not read flag. Contact admin.print_flagAnalyse avec Ghidra
J’utilise ensuite ghidra pour analyser le binaire plus en profondeur.
NOTEJe ne fais plus beaucoup de CTF et je ne maîtrise pas nécessairement tous les outils.
ghidra ainsi que d’autres outils font partie des essentiels pour les challenges CTF, c’est pourquoi j’ai créé un petit nix-shell avec les outils de base pour toutes les catégories. Vous pouvez le trouver ici :
Comme nous l’avons vu plus tôt avec la commande file, le binaire n’est pas stripped, donc nous avons les noms des fonctions, ce qui est super pratique.
Je trouve directement la fonction main dans le menu Functions, qui est le point d’entrée du programme avec la logique principale.
undefined8 main(void) { char *pcVar1; size_t sVar2; void *__ptr; long lVar3; undefined8 uVar4; char local_488 [64]; undefined1 local_448 [1024]; undefined4 local_48; void *local_40; undefined4 local_38; undefined1 *local_30; int local_28;
setbuf(stdout,(char *)0x0); setbuf(stdin,(char *)0x0); setbuf(stderr,(char *)0x0); print_banner(); __printf_chk(1,"Enter password: "); pcVar1 = fgets(local_488,0x40,stdin); if (pcVar1 == (char *)0x0) { puts("Error reading input."); uVar4 = 1; } else { sVar2 = strcspn(local_488,"\n"); local_488[sVar2] = '\0'; sVar2 = strlen(local_488); if (sVar2 < 3) { puts("Password too short."); uVar4 = 1; } else { __ptr = malloc(0x17); lVar3 = 0; do { *(byte *)((long)__ptr + lVar3) = (&encrypted_stage2)[lVar3] ^ 0xaa; lVar3 = lVar3 + 1; } while (lVar3 != 0x17);
puts("Patching bytecode with input...");
*(char *)((long)__ptr + 3) = local_488[0]; *(char *)((long)__ptr + 10) = local_488[1]; *(char *)((long)__ptr + 0x11) = local_488[2];
puts("Executing VM...\n");
local_48 = 0; local_38 = 0; local_28 = 0; local_40 = __ptr; local_30 = local_488;
vm_execute(local_448);
if (local_28 == 0) { puts("Invalid password."); } free(__ptr); uVar4 = 0; } } return uVar4;}Donc maintenant nous allons essayer de comprendre la logique du programme et suivre le flux d’exécution.
print_banner()
Première chose intéressante, nous voyons qu’il y a une fonction print_banner() qui s’affiche au démarrage du programme.

Nous remarquons quelques informations :
- c’est une VM multi-stage
- nous devons déchiffrer et exécuter le bytecode caché
Le code est relativement simple :
void print_banner(void)
{ puts(&DAT_004020f8); puts(&DAT_004021a8); puts(&DAT_004021e0); putchar(10); puts("Welcome to the multi-stage VM challenge!"); puts("Can you decrypt and execute the hidden bytecode?"); putchar(10); return;}Mot de passe
Donc début classique, initialisation du tampon, affichage de la bannière, puis il demande un mot de passe.
pcVar1 = fgets(local_488,0x40,stdin); if (pcVar1 == (char *)0x0) { puts("Error reading input."); uVar4 = 1; }L’entrée est lue avec fgets et une erreur est affichée si cela échoue.
Ensuite, cela devient plus intéressant, nous vérifions la longueur du mot de passe :
sVar2 = strcspn(local_488,"\n"); local_488[sVar2] = '\0'; sVar2 = strlen(local_488); if (sVar2 < 3) { puts("Password too short."); uVar4 = 1; }Donc le mot de passe doit faire au moins 3 caractères (nous garderons cela à l’esprit).
Nous allouons ensuite 23 octets (0x17 en hexadécimal) — pas encore sûr de pourquoi.
else { __ptr = malloc(0x17);Et ici nous atteignons la partie intéressante, nous bouclons sur 23 (23 octets) et pour chaque octet nous faisons un XOR avec 0xaa à partir d’un tableau encrypted_stage2.
donc :
- il y a des données chiffrées quelque part dans le binaire (encrypted_stage2)
- nous les déchiffrons avec XOR 0xaa
- le résultat va dans la mémoire allouée
Encore plus intéressant !
puts("Patching bytecode with input...");
*(char *)((long)__ptr + 3) = local_488[0]; *(char *)((long)__ptr + 10) = local_488[1]; *(char *)((long)__ptr + 0x11) = local_488[2];- le
premier caractèreva à la position 3 du bytecode déchiffrié - le
deuxième caractèreà la position 10 - le
troisième caractèreà la position 17 (0x11 en hexadécimal)
Donc mon mot de passe modifie directement le code qui sera exécuté !
puts("Executing VM...\n");
local_48 = 0; local_38 = 0; local_28 = 0; local_40 = __ptr; local_30 = local_488;
vm_execute(local_448);Nous initialisons quelques variables, puis appelons vm_execute qui exécutera le bytecode patché.
if (local_28 == 0) { puts("Invalid password."); }Une autre chose intéressante, si local_28 est égal à 0 après l’exécution du bytecode, nous affichons “Invalid password”.
encrypted_stage2
Maintenant que je comprends le mécanisme, je dois trouver ce encrypted_stage2.
encrypted_stage2 est défini comme une variable globale.
Nous pouvons utiliser gdb pour extraire ces octets directement de la mémoire du programme.
dans gdb, nous pouvons faire :
x/23xb &encrypted_stage2
petites explications :
xpour examiner la mémoire/23pour lire 23 octetsxbpour afficher en hexadécimal (octet par octet)&encrypted_stage2est l’adresse de la variable.

Voici la sortie, donc dans la colonne de gauche nous avons les adresses mémoire puis à droite les 23 octets chiffrés.
Maintenant que j’ai extrait les données chiffrées, je dois les déchiffrer avec XOR 0xAA comme dans le code.
Voici un petit script bash pour faire ça :
#!/bin/bashencrypted="ab e8 ab aa af ac a5 ab 99 ab aa af ac a2 ab da ab aa af ac ab ad 55"
echo "$encrypted" | tr ' ' '\n' | while read byte; do printf "%02x " $((0x$byte ^ 0xaa))doneecho
Analyse du bytecode déchiffré
En regardant ce bytecode déchiffré, nous remarquons un motif clair :
01 42 01 00 05 06 ...... 01 33 01 00 05 06 ...... 01 70 01 00 ...Les positions que le programme patche (3, 10, et 17) sont initialement 0x00.
Motif répétitif : nous voyons la séquence 01 XX 01 00 05 06 qui se répète 3 fois.
La logique de la VM semble être une comparaison. Juste avant chaque 0x00, nous trouvons les octets 0x42, 0x33 et 0x70.
- À la position 1, nous avons 0x42. La VM attend probablement cette valeur à la position 3.
- À la position 8, nous avons 0x33. La VM attend probablement cette valeur à la position 10.
- À la position 15, nous avons 0x70. La VM attend probablement cette valeur à la position 17.
Le bon mot de passe doit donc être composé des caractères ASCII correspondant à ces valeurs hexadécimales :
- 0x42 = ‘B’
- 0x33 = ‘3’
- 0x70 = ‘p’
Le mot de passe est B3p.
Test de la solution
echo "B3p" | nc 15.237.107.187 1029
Nous obtenons le drapeau !
Conclusion
Voici le write-up pour ce challenge, j’ai trouvé le drapeau. Pour les autres challenges StarHack CTF 2025 que j’ai résolus, je pourrais les publier si j’ai le temps.
J’ai fini 24ème au classement général, pas mal pour un retour au CTF !
Si vous voulez vous entraîner sur ce challenge, l’instance publique n’est plus disponible, mais je peux toujours fournir le binaire !
Si vous avez des questions ou des suggestions, n’hésitez pas à me contacter.