[BreizhCTF] Reverse - La Key Dans La Stone
Le binaire
Après avoir décompilé le binaire du challenge key
en utilisant un outil tel que Binja ou Ghidra, et constaté que la fonction principale (main) était assez simple, on commence à chercher d’autres fonctions plus intéressantes. En effet, la majeure partie du challenge se trouvait dans l’une des fonctions appelées par la fonction main.
ks_asm quésaco ?
ks_asm
est une fonction permettant de compiler de l’assembleur via le module keystone.
Notre binaire compile du code assembleur, met le résultat sous forme d’opcode (code assembleur compilé) dans une variable et call ce code puis vérifie si le “code” nous renvoie 0 auquel cas il nous affiche “Bien joué tu as le flag du chall !” et si la condition n’est pas vérifiée il nous affiche “Encore un petit d’effort”.
TL;DR
Il compile de l’assembleur et l’exécute puis check si l’ “assembleur” return 0.
Comment solve cet enfer ?
On va commencer par extirper du programme le code assembleur qui est compilé par la fonction ks_asm
en utilisant Binary Ninja puis en enlevant toutes les impuretés avec VsCode à grand coup de Ctrl+F et replace-all :thumbsup:
Ce qui nous donne comme code:
1push rbp
2mov rbp, rsp
3sub rsp, 0x20
4mov rax, 0
5mov rdi, 0
6mov rsi, rsp
7mov rdx, 0x19
8syscall
9mov rcx, rax
10cmp rax, 0x19
11jnz _end
12mov rsi ,rsp
13mov rdi, 0
14mov al, byte [rsi + rdi]
15cmp al, 0x42
16jnz _end
17inc rdi
18mov al, byte [rsi + rdi]
19cmp al, 0x5a
20jnz _end
21inc rdi
22mov al, byte [rsi + rdi]
23xor al, 0x78
24cmp al, 0x30
25jnz _end
26inc rdi
27mov al, byte [rsi + rdi]
28sub al, 0x30
29cmp al, 0x13
30jnz _end
31inc rdi
32mov al, byte [rsi + rdi]
33add al, 0x30
34cmp al, 0x84
35jnz _end
36inc rdi
37mov al, byte [rsi + rdi]
38xchg al, al
39cmp al, 0x46
40jnz _end
41inc rdi
42mov al, byte [rsi + rdi]
43cmp al, 0x7b
44jnz _end
45inc rdi
46mov al, byte [rsi + rdi]
47xor al, 0x60
48cmp al, 0x36
49jnz _end
50inc rdi
51mov al, byte [rsi + rdi]
52add al, 0x40
53cmp al, 0x70
54jnz _end
55inc rdi
56mov al, byte [rsi + rdi]
57sub al, 0x44
58cmp al, 0x31
59jnz _end
60inc rdi
61mov al, byte [rsi + rdi]
62cmp al, 0x35
63jnz _end
64inc rdi
65mov al, byte [rsi + rdi]
66cmp al, 0x5f
67jnz _end
68inc rdi
69mov al, byte [rsi + rdi]
70xchg al, al
71cmp al, 0x34
72jnz _end
73inc rdi
74mov al, byte [rsi + rdi]
75cmp al, 0x76
76jnz _end
77inc rdi
78mov al, byte [rsi + rdi]
79sub al, 0x33
80cmp al, 0
81jnz _end
82inc rdi
83mov al, byte [rsi + rdi]
84add al, 2
85cmp al, 0x7c
86jnz _end
87add rdi, 2
88mov al, byte [rsi + rdi]
89xchg al, al
90cmp al, 0x31
91jnz _end
92dec rdi
93mov al, byte [rsi + rdi]
94cmp al, 0x5f
95jnz _end
96add rdi, 2
97mov al, byte [rsi + rdi]
98sub al, 0x33
99cmp al, 1
100jnz _end
101mov al, byte [rsi + rdi + 3]
102cmp al, 0x31
103jnz _end
104mov al, byte [rsi + rdi + 2]
105add al, 1
106cmp al, 0x64
107jnz _end
108mov al, byte [rsi + rdi + 1]
109cmp al, 0x5f
110jnz _end
111add rdi, 0x4
112mov al, byte [rsi + rdi]
113cmp al, 0x65
114jnz _end
115mov al, byte [rsi + rdi + 1]
116sub al, 0x19
117cmp al, 0x64
118jnz _end
119mov rax, 1
120mov rdi, 1
121mov rdx, rcx
122syscall
123add rsp, 0x20
124pop rbp
125xor rax, rax
126ret
127_end:
128add rsp, 0x20
129pop rbp
130mov rax, 1
131ret
On le compile utilisant nasm (version 2.15.05) et ld
1number@rev$ nasm -f elf64 key_asm
2number@rev$ ld key_asm.o -o challenge_asm -z execstack
3ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
4
Puis on le re-décompile avec binary ninja pour avoir un code mangeable.
1int64_t start(){
2char var_28;
3int64_t rax = syscall(sys_read {0}, 0, &var_28, 0x19);
4int64_t rcx = rax;
5if (rax == 0x19)
6{
7 void* rsi_1 = &var_28;
8 rax = var_28;
9 if (rax == 0x42)
10 {
11 char var_27;
12 rax = var_27;
13 if (rax == 0x5a)
14 {
15 ...
16
17 syscall(sys_write {1}, 1, rsi_1, rcx);
18 return 0;
19
20 }
21 }
22 }
23 return 1;
24}
La fonction start effectue un appel au syscall read pour récupérer 25 caractères en entrée et stocke la longueur de cette entrée dans rcx ainsi que son contenu dans la variable var_28. Ensuite, elle effectue plusieurs tests sur cette entrée et si elle est valide, elle affiche le flag en utilisant le syscall write et renvoie 0.
Les tests effectués suivent un pattern (modèle) récurrants: le programme itére sur l’entièreté de notre flag et fait un test sous la forme
1 rax = input[26];
2 if (rax == 0x5a)
3 {
4 rax = input[25];;
5 rax = (rax ^ 0x78);
6 if (rax == 0x30)
7 {
8 etc ...
9 }
10 }
11
12
J’avais la flemme de faire ça à la main donc j’ai opté pour un script angr.
Un solve puni par la loi
A partir de là il ne me manquait plus rien, j’avais la longeur du flag et l’adresse à éviter (le return 0 qui aurait fait que la condition initiale ne serait pas remplie) donc c’est facile à faire avec
1import angr
2import claripy
3p = angr.Project('./chal')
4state = p.factory.entry_state(args=["./chal.bin"])
5s = p.factory.simulation_manager(state)
6find_addr = 0x0040117a # adresse de l'instruction: return 0
7avoid_addr = 0x0040118f # adresse de l'instruction: return 1
8s.explore(find=find_addr,avoid=avoid_addr)
9print(s.found[0].posix.stdin.concretize()[0].split(b'\0')[0])
10
Et en un temps record on obtient le flag: BZHCTF{V0u5_4v3z_14_c1e}
Merci au créateur du challenge et merci d’avoir lu jusqu'à la fin mes palabres :) .
Si vous avez une question ou une remarque à faire sur mon writeup n’hésitez pas à m’envoyer un message sur Twitter ou sur Discord: @numb3rss.