Prechall
- Catégorie: Cracking et programming
- Date de résolution : 30/03/2024
- URL : https://france-cybersecurity-challenge.fr/teasing
1. Première étape
Il s'agit du jeu dino créé par Google il y a 10 ans.
Le jeu est codé en WebAssembly et le fichier wasm se trouve à l'adresse : https://france-cybersecurity-challenge.fr/files/teasing/fcsc2024-dino.wasm
Le code source du format WebAssembly est accessible dans le debugger de Chrome (onglet sources) mais il n'est bien sûr pas possible de modifier directement le code ici.
J'ai utilisé le toolkit wabt pour reverse le game.
1.1 from binary to text - wasm2wat
$ bin/wasm2wat fcsc2024-dino.wasm -o fcsc2024-dino-hack.wat
Ne connaissant pas le format texte WebAssembly, j'ai cherché s'il n'existait pas un autre code source plus commenté et surtout avec des noms de fonctions et variables plus explicites...
Et je suis tombé sur ce code
En particulier, le début de code où sont instanciées les variables globales indiquent :
1;; A timer used for animation.
2(global $timer (mut i32) (i32.const 0))
3
4;; The current score (only increments while playing).
5(global $score (mut i32) (i32.const 0))
6
7;; The current game state (see the main br_table below)
8;; 0 => initial state
9;; 1 => dino is running
10;; 2 => dino is rising (first part of jump)
11;; 3 => dino is falling (second part of jump)
12;; 4 => dino is dead
13(global $game_state (mut i32) (i32.const 0))
14
15;; The dino's y velocity. Negative is going up, positive is going down.
16(global $dino_jump_vy (mut f32) (f32.const 0))
17
18;; The camera's scrolling speed. In actuality there is no camera, this speed is
19;; instead used to move all game objects. Each object multiplies its movement
20;; speed by this global $speed to provide a parallax effect.
21(global $speed (mut f32) (f32.const -0.5))
Ainsi on comprend mieux l'utilisation des variables :
- $a = timer
- $b = score
- $c = game_state
- $d = y_velocity
- $e = speed
Ces variables $a, $b, $c, $d, $e étant ainsi nommées après usage de wasm2wat
D'abord, je cherche à être invincible et à changer le set(game_state) = 4. Mais je comprends que le code FCSC ici diffère du code trouvé...
Je cherche alors si nous devons dépasser un score et je trouve :
1 0x0430 i32.const 500000000
2 0x0436 global.get $b
3 0x0438 i32.lt_u
500 millions à la main sans tricher : ça fait beaucoup... J'essaie de changer cette valeur en mettant 0 😎
Je compile :
$ bin/wat2wasm fcsc2024-dino-hack.wat -o fcsc2024-dino-hack.wasm
Je crée un fichier html pour placer le canvas qui contient le .wasm que j'héberge sur mon serveur web local et je lance le jeu.
tada...
2. Deuxième étape
On accède maintenant à un code python pour lequel il faut trouver la bonne entrée :
1from hashlib import sha256
2
3CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!?€$*"
4L1 = "$ABFIZht!HPUYhirKOXdhjxy?DGJSWehCENahklo*cghmpu€MQRVfhvzLTbhnqsw"
5L2 = "!FOWamwzFRSTUdlp*EFVXbei?FMNPgjsCDFHcnvxFJQYkqy€FGKLforu"
6L3 = "!FOWamwz$RcejkrwDIKNVYpwGHMXltw€*APSovwy?BCQdiuwEJUZfgwx"
7
8def check(A, B):
9 res = True
10 for a in range(7):
11 for b in range(7):
12 S = set(A[a])
13 for x in range(7):
14 y = (a * x + b) % 7
15 S = S.intersection(set(B[y][x]))
16 res &= (len(S) == 1)
17 for x in range(7):
18 S = set(A[7])
19 for y in range(7):
20 S = S.intersection(set(B[y][x]))
21 res &= (len(S) == 1)
22 return res
23
24s = input(">>> ")
25
26assert len(s) == 8 * (8 * 7 + 1)
27assert all(x in CHARSET for x in s)
28
29s1, s2 = s[:8 * 8], s[8 * 8:]
30A = [ s1[j:j + 8] for j in range(0, 8 * 8, 8) ]
31B = [ [s2[i:i + 8 * 7][j:j + 8] for j in range(0, 8 * 7, 8) ] for i in range(0, len(s2), 8 * 7)]
32
33assert L1 == "".join(A)
34assert L2 == "".join([ B[0][i] for i in range(7) ])
35assert L3 == "".join([ B[i][0] for i in range(7) ])
36assert all([ sorted(B[i][j]) == list(B[i][j]) for i in range(7) for j in range(7) ])
37assert check(A, B)
38
39h = sha256(s.encode()).hexdigest()
40print(f"Congrats! You can now go here: https://france-cybersecurity-challenge.fr/{h}")
A la lecture du code, on comprend que :
- s = l'entrée recherchée
- len(s) = 456
- s = s1 + s2
- s1 = L1
- s2 est décomposé en bloc de 8 x 7 x 7 chars :
- B[y][x] avec x,y dans [0,6]
- B[y][x] = set(8 chars)
- B[0][0]+B[0][1]+..+B[0][6] = L2
donc s = L1 + L2 + B
On comprend également que L3 forme les prochains blocs comme suit :
- B[i][0] = L3[ix8:ix8+8] pour i allant de 0 à 6
Passons maintenant à la fonction check(A,B)
res ne doit jamais être égal à 0, sinon la valeur de retour est 0 (opération &=)
Cette fonction (lignes 10 à 16) teste pour chaque portion de L1 qu'au moins 1 caractère parmi les 8 est bien inclus dans 7 blocs définis de B.
En faisant des print dans la boucle, on constate que lorsque S est instanciée, elle comprend 8 chars mais qu'à la première itération et intersection, il ne reste qu'un caractère dans S. Ce caractère doit donc être présent obligatoirement dans les autres blocs. Il est donc possible en modifiant un peu le code de déterminer les caractères attendus pour chaque bloc.
Voici la fonction modifiée :
1def check(A, B):
2 res = True
3 listsort = [ [ [] for j in range(7) ] for i in range(7) ]
4 for a in range(7):
5 for b in range(7):
6 S = set(A[a])
7 for x in range(7):
8 y = (a * x + b) % 7
9 T = set(B[y][x])
10 S = S.intersection(set(B[y][x]))
11 if (len(S) == 1):
12 motif = "".join(S)
13 if (motif):
14 listsort[y][x].append(motif)
15 print(f"B{y}{x} {motif}")
16 res &= (len(S) == 1)
17 for x in range(7):
18 S = set(A[7])
19 print(f"S = {S}")
20 for y in range(7):
21 T = set(B[y][x])
22 S = S.intersection(set(B[y][x]))
23 res &= (len(S) == 1)
24 print("listsort = ", [sorted(listsort[i][j]) for i in range(7) for j in range(7)])
25 return res
Et le résultat est :
| B[][0] | B[][1] | B[][2] | B[][3] | B[][4] | B[][5] | B[][6] | |
|---|---|---|---|---|---|---|---|
| B[0][] | !FOWamz | FRSUdlp | *EFVXei | ?FMNPgj | CDFHcvx | FJQYky€ | FGKforu |
| B[1][] | $Rcejkr | !$?Vox€ | $DMUauy | $JKilmv | $EGOPQp | $*HNWdf | $CSXYgz |
| B[2][] | DIKNVYp | *CIJMOr | !GIdgkv | IQUWXco | ISafij€ | IPeluxz | ?EHIRmy |
| B[3][] | GHMXlt€ | EWYjtuv | NQSmrtx | !Cefpty | *?KUktz | DORgiot | JPVacdt |
| B[4][] | *APSovy | AHKQaeg | ?AOYcfl | ADEdrz€ | !AJNRXu | ACGUVjm | AMWikpx |
| B[5][] | ?BCQdiu | BDPXfkm | BHJjopz | *BGRYax | BVWglry | !BEKMSc | BNOUev€ |
| B[6][] | EJUZfgx | GNZciyz | CKPRWZ€ | HOSVZku | MYZdemo | ?XZaprv | !*DQZjl |
Ces blocs sont composés de 7 caractères et il en manque un pour atteindre les 8 requis.
C'est la deuxième boucle for x in range(7) qui permet de déterminer de la même manière ce 8ième caractère.
Au final l'entrée attendue obtenue est :
$ABFIZht!HPUYhirKOXdhjxy?DGJSWehCENahklo*cghmpu€MQRVfhvzLTbhnqsw!FOWamwzFRSTUdlp*EFVXbei?FMNPgjsCDFHcnvxFJQYkqy€FGKLforu$Rcejkrw!$?TVox€$DMUabuy$JKilmsv$EGOPQnp$*HNWdfq$CLSXYgzDIKNVYpw*CIJMOTr!GIbdgkvIQUWXcosISafijn€IPelquxz?EHILRmyGHMXltw€ETWYjtuvNQSbmrtx!Cefpsty*?KUkntzDORgioqtJLPVacdt*APSovwyAHKQTaeg?AOYbcflADEdrsz€!AJNRXnuACGUVjmqALMWikpx?BCQdiuwBDPTXfkmBHJbjopz*BGRYasxBVWglnry!BEKMScqBLNOUev€EJUZfgwxGNTZciyzCKPRWZb€HOSVZksuMYZdemno?XZapqrv!*DLQZjl
FCSC{892e99c79e21dc688583812d93064773609e6ce39cc37cae2d17da9a5535d1a6}