Dans cet article vous trouverez les write-ups de tous les challenges de reverse engineering du Midnight Flag, un CTF organisé sur la nuit du 10 au 11 avril par des élèves de l’ESNA auquel j’ai participé avec l’équipe Arn’Hack. Les challenges étaient assez sympas surtout le dernier que je fus le seul à flag pendant ce CTF (sans compter Mathis qui ne participait pas officiellement).
Fichiers des challenges
- Cadeau à la NSA – 150 points – 42 solves
- La grande évasion – 190 points – 38 solves
- Cryptographie louche – 250 points – 14 solves (first blood)
- La Bombe – 325 points – 13 solves
- Key Checker – 600 points – 1 solve (first et unique blood officiellement)
Cadeau à la NSA
Comme d’habitude on commence par faire un file sur le binaire pour savoir à quoi on a affaire.
$ file nsa_flag
nsa_flag: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=0744313f31c9ae3c01e384d721d7888e98e54efe, not stripped
On a donc ELF x86_64 non strippé. On peut ensuite l’exécuter pour avoir un aperçu de son fonctionnement.
$ ./nsa_flag
Usage: ./nsa_flag <>
$ ./nsa_flag passwd
WRONG! Get out of here! You are not the NSA!
On a donc un passage du mot de passe en argument, avec le message qui nous jette dehors si l’on entre pas le bon mot de passe. On peut ensuite ouvrir le binaire dans u désassembleur et observer la fonction main.
On voit bien ici le check d’argc pour récupérer le nombre d’arguments passés au programme, puis on voit que le flag va être déchiffré et comparé à notre input via la fonction strncmp sur 0x30 == 48 octets.
On pourrait alors se mettre à reverse la fonction “decrypt_flag” mais le flag sera comparé en clair à notre input ! Il suffit alors soit de mettre un breakpoint à l’appel à strncmp soit de lancer le binaire avec ltrace et l’option “-s 48” pour afficher les chaines de caractères de moins de 48 char en entier.
$ ltrace -s 48 ./nsa_flag ok
calloc(48, 1) = 0x56076aa052a0
strlen("\033"Qt)6.r\031nZV3&6jTp\022olm\025_\r&\035z8"a\034Ce\177t\033aS:PQV2") = 44
strncmp("MCTF{G3neR4l_K3n0b1…Y0u_Ar3_@_B0lD_0ne…}", "ok", 48) = -34
puts("WRONG! Get out of here! You are not the NSA!"WRONG! Get out of here! You are not the NSA!
) = 45
exit(1
+++ exited (status 1) +++
On récupère ainsi notre flag : MCTF{G3neR4l_K3n0b1…Y0u_Ar3_@_B0lD_0ne…}
La grande évasion
La description du challenge nous dit que ce dump provient d’un Arduino. On se renseigne alors sur l’assembleur utilisé et on trouve que c’est de l’avr au format ihex.
Un suite d’outils avr peut être installée avec la commande “apt-get install binutils-avr” et on a ensuite accès, entre autres, à un outil permettant de transformer notre .hex en fichier binaire comme ceci.
$ avr-objcopy -I ihex grande_evasion.hex -O binary output
On peut ensuite simplement faire un strings sur le binaire et récupérer le flag.
$ strings output
h>s@
/?O/s3'1 N[_O "P1 9 N__O$ .?O
"P1
a,q,
APP@
MCTF{Th3_Al4Rm_Ha$_G0n3_0ff}
Flag du challenge : MCTF{Th3_Al4Rm_Ha$_G0n3_0ff}
Cryptographie Louche
Comme pour le premier binaire on peut commencer par faire un file sur le fichier.
$ file KeeCustom
KeeCustom: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=71fb51db7e93079b5814815f4a65f956d1271266, not stripped
On a donc encore une fois un ELF x86_64 non strippé. On peut ensuite l’exécuter pour avoir un aperçu de son comportement.
$ ./KeeCustom
Hello, and welcome to our shared password manager. Enter the master password to unlock it:
> p4ssw0rd
Password incorrect.
> ah
Password incorrect.
> really ?
Password incorrect.
You will not fail me again..
Le programme nous demande donc un mot de passe et nous avons apparemment 3 essais pour rentrer de bon mot de passe, sans quoi le message “You will not fail me again” sera affiché. On peut alors désassembler le binaire pour voir ce qu’il se passe.
On voit bien ici les tours de boucle avec le compteur symbolisé par [rbp+var_4] qui sera comparé à 2 avec ensuite un saut conditionnel JLE (Jump if Lower or Equal) : ce qui fait donc bien 3 tours de boucle.
A chaque tour de boucle l’input passe dans une fonction “check_password” et si cette fonction renvoie TRUE (0) alors la boucle s’arrête et on appelle “unlock_content” qui va nous afficher les supposés mots de passes gardés dans ce super gestionnaire de mot de passes (rien d’intéressant ici).
On peut alors s’intéresser à la fonction “check_password” qui s’occupera de vérifier la conformité de l’input (on prendra ici le code issu du decompiler pour aller plus vite).
_BOOL8 __fastcall check_password(char *input) {
unsigned int v1; // ebx
unsigned __int8 v2; // al
unsigned __int8 v3; // al
unsigned __int8 v4; // al
signed int i; // [rsp+1Ch] [rbp-14h]
for ( i = 0; i <= 47; ++i ){
v1 = (unsigned __int8)input[i];
v2 = add((unsigned __int8)input[i], (unsigned __int8)i);
v3 = sub(105LL, v2);
v4 = multiply(v3, 66LL);
input[i] = divide(v4, v1);
}
return memcmp(input, &unk_2020, 0x30uLL) != 0;
}
Bon pour celui-ci je dois avouer que j’ai vu qu’il n’y avait encore aucun solve et que les conditions n’étaient pas difficiles à analyser automatiquement alors j’ai rush le first blood et j’ai sortit angr… Pour ça j’ai juste besoin d’une adresse atteinte uniquement quand l’input est le bon et une autre quand l’input n’est pas bon.
import angr
# +0x400000 car base addresse du binaire mappé en mémoire
win_addr = 0x14D3 + 0x400000
fail_addr = 0x14E9 + 0x400000
proj = angr.Project('KeeCustom')
simgr = proj.factory.simgr()
simgr.explore(find=win_addr, avoid=fail_addr)
s = simgr.found[0]
print(s.posix.dumps(0))
On peut ensuite exécuter le script avec Python.
$ python KeeCustom_angr.py
WARNING | 2021-04-13 14:10:31,367 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.
WARNING | 2021-04-13 14:10:32,515 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior.
WARNING | 2021-04-13 14:10:32,515 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING | 2021-04-13 14:10:32,515 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state
WARNING | 2021-04-13 14:10:32,515 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2021-04-13 14:10:32,515 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages.
WARNING | 2021-04-13 14:10:32,515 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0xc0000f70 with 81 unconstrained bytes referenced from 0x5a2280 (strchr+0x0 in libc.so.6 (0xa2280))
b'MCTF{D0_n0T_TrU$T_7h3_SyMb0l5,_Th3y_Ar3_Ly1nG}\x00\n'
On récupère ainsi le flag : MCTF{D0_n0T_TrU$T_7h3_SyMb0l5,_Th3y_Ar3_Ly1nG}
La Bombe
Comme d’habitude on commence par faire un file sur le binaire afin de voir à quoi on a affaire.
$ file BOMB.bin
BOMB.bin: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=262bc864391f272c9c13aa84a632d013392a9290, not stripped
On a cette fois un ELF x86 (32 bits) non strippé. On peut ensuite lancer le programme pour observer son comportement (si vous ne pouvez pas lancer des binaire x86 sur votre Linux avec un processeur x86_64 vous pouvez suivre ce tuto).
./BOMB.bin
Welcome to my fiendish little bomb! You have 3 phases in which to blow yourself up!
Have a nice day :D
> password
BOOM!! The bomb has blown up!!!
Le programme nous indique qu’il y a 3 phases, donc surement 3 mots de passe de plus en plus compliqués à trouver. On peut ensuite ouvrir le binaire dans un désassembleur et observer le code de la fonction main.
On voit donc ici 3 espaces alloués avec calloc(16, 1) destinés à recevoir les input qui seront donc d’au plus 16 caractères chacun. Ensuite, on voit bien l’appel aux différentes phase caractérisées par les fonctions “phase_1”, “phase_2” et “phase_3”. Le flag sera ensuite constitué des trois mots de passe.
Phase 1
On peut alors commencer par désassembler le code de la phase 1.
On voit ici que l’input sera directement comparé à la chaine “s3as0n4l” (ou aurait également pu le voir via ltrace comme pour le challenge “Cadeau à la NSA”). On peut ainsi passer à la phase 2.
$ ./BOMB.bin
Welcome to my fiendish little bomb! You have 3 phases in which to blow yourself up!
Have a nice day :D
> s3as0n4l
Well done! You have defused phase 1!
>
Phase 2
On peut alors s’intéresser à la fonction “phase_2”. Le code désassemblé ne tient pas en un screenshot et on peut se simplifier la vie en mettant un coup de décompilo sur la fonction.
int __cdecl phase_2(char *s){
readline(s);
if ( strlen(s) != 5 )
explode_bomb();
if ( s[4] != s[3] + 11 || s[3] != s[2] + 7 || s[1] != *s + 34 || s[2] != s[1] + 16 || s[2] != 2 * (*s + 1) )
explode_bomb();
return puts("Forwards, trooper!");
}
On voit donc ici une série de condition à satisfaire pour passer la phase 2, un petit système d’équation. On peut alors sortir notre ami z3 si on a la flemme comme moi 😄.
from z3 import *
flag = [BitVec(f"flag[{i}]", 8) for i in range(6)]
s = Solver()
s.add(flag[4] == flag[3] + 11)
s.add(flag[3] == flag[2] + 7)
s.add(flag[1] == flag[0] + 34)
s.add(flag[2] == flag[1] + 16)
s.add(flag[2] == 2 * (flag[0] + 1))
print(s.check())
if(str(s.check()) == "sat"):
print("[+] SOLUTION : ")
print(s.model())
On peut alors exécuter le script et récupérer le résultat pour décoder le flag.
$ python3 bomb.py
sat
[+] SOLUTION :
[flag[0] = 48,
flag[2] = 98,
flag[1] = 82,
flag[4] = 116,
flag[3] = 105]
flag = [0]*5
flag[0] = 48
flag[2] = 98
flag[1] = 82
flag[4] = 116
flag[3] = 105
print(f'[+] PHASE 2 : {"".join(chr(i) for i in flag)}')
On exécute et on récupère le mot de passe de la phase 2 !
$ python3 bomb_final.py
[+] PHASE 2 : 0Rbit
$ ./BOMB.bin
Welcome to my fiendish little bomb! You have 3 phases in which to blow yourself up!
Have a nice day :D
> s3as0n4l
Well done! You have defused phase 1!
> 0Rbit
Forwards, trooper!
>
Phase 3
Dernière étape du binaire, on touche au but ! On peut alors désassembler la fonction “phase_3”.
On peut meme passer un coup de decompilo pour voir si ca se simplifie un petit peu.
int __cdecl phase_3(char *input)
{
int v2; // [esp+Ch] [ebp-6Ch]
int v3; // [esp+12h] [ebp-66h]
int v4; // [esp+16h] [ebp-62h]
__int16 v5; // [esp+1Ah] [ebp-5Eh]
char v6; // [esp+1Ch] [ebp-5Ch]
int v7; // [esp+1Dh] [ebp-5Bh]
int i; // [esp+5Ch] [ebp-1Ch]
v2 = 0x4000;
v7 = *(_DWORD *)"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
*(int *)((char *)&i + (_DWORD)(&ITM_registerTMCloneTable_ptr - 4096)) = *(_DWORD *)"xyz";
qmemcpy(
(void *)(((_DWORD)&v7 + (_DWORD)&unk_4004 - 0x4000) & 0xFFFFFFFC),
(const void *)(8436 - ((_DWORD)&v7 - (((_DWORD)&v7 + (_DWORD)&unk_4004 - 0x4000) & 0xFFFFFFFC))),
4
(((unsigned int)((char *)&v7 - (((_DWORD)&v7 + (_DWORD)&unk_4004 - 0x4000) & 0xFFFFFFFC) + 63) & 0xFFFFFFFC) >> 2));
v3 = 51127818;
v4 = 822359859;
v5 = 780;
v6 = 0;
readline(input);
if ( strlen(input) != 10 )
explode_bomb();
for ( i = 0; i <= 9; ++i )
{
if ( input[i] != *((_BYTE *)&v7 + *((char )&v3 + i)) ) explode_bomb(); } return puts("Sht, you actually managed to defuse the bomb! Well done :/");
}
Bon c’est pas très joli tout ça, MAIS on peut ruser 😄. En effet on voit ici que l’input est directement comparé caractère par caractère au password déchiffré après ces opérations un peu obscures.
On peut donc sortir un debugger comme GDB (ici avec l’extension gef que je conseille au meme titre que peda ou pwndbg), poser un breakpoint à cette comparaison et observer la valeur à laquelle chaque caractère est comparé.
On pose donc un premier breakpoint au début de la fonction “phase_3”.
gdb -q ./BOMB.bin
Reading symbols from ./BOMB.bin…
(No debugging symbols found in ./BOMB.bin)
(gdb) b * phase_3
Breakpoint 1 at 0x14c8
(gdb) r
Starting program: /root/CTF/BOMB.bin
Welcome to my fiendish little bomb! You have 3 phases in which to blow yourself up!
Have a nice day :D
> s3as0n4l
Well done! You have defused phase 1!
> 0Rbit
On fait ensuite un disass pour localiser l’adresse de la comparaison qui nous intéresse.
(gdb) disass
Dump of assembler code for function phase_3:
=> 0x565a94c8 <+0>: push ebp
0x565a94c9 <+1>: mov ebp,esp
0x565a94cb <+3>: push edi
0x565a94cc <+4>: push esi
0x565a94cd <+5>: push ebx
0x565a94ce <+6>: sub esp,0x6c
0x565a94d1 <+9>: call 0x565a967c <__x86.get_pc_thunk.ax>
[...]
0x565a9563 <+155>: mov edx,DWORD PTR [ebp-0x1c]
0x565a9566 <+158>: mov eax,DWORD PTR [ebp+0x8]
0x565a9569 <+161>: add eax,edx
0x565a956b <+163>: movzx edx,BYTE PTR [eax]
0x565a956e <+166>: lea ecx,[ebp-0x66]
0x565a9571 <+169>: mov eax,DWORD PTR [ebp-0x1c]
0x565a9574 <+172>: add eax,ecx
0x565a9576 <+174>: movzx eax,BYTE PTR [eax]
0x565a9579 <+177>: movsx eax,al
0x565a957c <+180>: movzx eax,BYTE PTR [ebp+eax*1-0x5b]
0x565a9581 <+185>: cmp dl,al
0x565a9583 <+187>: je 0x565a958a
0x565a9585 <+189>: call 0x565a9239
[...]
0x565a95af <+231>: pop edi
0x565a95b0 <+232>: pop ebp
0x565a95b1 <+233>: ret
End of assembler dump.
On a donc notre “cmp dl, al” en 0x565a9581. On y pose un breakpoint en on continue la phase 3 avec comme input 10 fois le caractère “A” (0x41 en hexa).
(gdb) b * 0x565a9581
Breakpoint 2 at 0x565a9581
(gdb) c
Continuing.
> AAAAAAAAAA
A chaque break on observe la valeur de EAX et EDX qui sont comparés en gardant en tête que notre input est “0x41 * 10”. Pour cette première comparaison, 0x41 est comparé à 0x41 ce qui veut dire que la première lettre du mot de passe est un “A”. On continue.
Ici on voit que 0x41 est comparé avec 0x63. On note cette valeur sur un script python qu’on va compléter au fur et à mesure de debug.
flag = [0]*10
flag[0] = 0x41
flag[1] = 0x63
# flag[2] =
# flag[3] =
# flag[4] =
# flag[5] =
# flag[6] =
# flag[7] =
# flag[8] =
# flag[9] =
print(f"[+] PHASE 3 : {''.join(chr(i) for i in flag)}")
Ok top ! Maintenant avant d’entrer “c” pour “continue”, on va mettre la bonne valeur dans EDX pour que je programme n’arrête pas son exécution (et on vérifie que la valeur a bien été modifiée).
(gdb) set $edx=0x63
(gdb) p $edx
$1 = 0x63
On peut alors faire “c” et continuer jusqu’au prochain break, puis répéter les mêmes étapes jusqu’à la fin ! On termine alors de cette manière.
(gdb) set $edx=0x33
(gdb) c
Continuing.
Sh*t, you actually managed to defuse the bomb! Well done :/
You can submit this flag now…MCTF{s3as0n4l_0Rbit_AAAAAAAAAA}
[Inferior 1 (process 6827) exited normally]
On voit un flag cassé car il concatène les 3 input que nous avons faits, or le dernier input de base était bien AAAAAAAAAA. On peut alors utiliser notre script python établi au cours du debug pour trouver le 3ème password.
flag = [0]*10
flag[0] = 0x41
flag[1] = 0x63
flag[2] = 0x43
flag[3] = 0x33
flag[4] = 0x70
flag[5] = 0x74
flag[6] = 0x34
flag[7] = 0x6e
flag[8] = 0x43
flag[9] = 0x33
print(f"[+] PHASE 3 : {''.join(chr(i) for i in flag)}")
$ python phase_3.py
[+] PHASE 3 : AcC3pt4nC3
On a donc alors le flag MCTF{s3as0n4l_0Rbit_AcC3pt4nC3} !
Par acquis de conscience on essaie sur le programme.
./BOMB.bin
Welcome to my fiendish little bomb! You have 3 phases in which to blow yourself up!
Have a nice day :D
> s3as0n4l
Well done! You have defused phase 1!
> 0Rbit
Forwards, trooper!
> AcC3pt4nC3
Sh*t, you actually managed to defuse the bomb! Well done :/
You can submit this flag now… MCTF{s3as0n4l_0Rbit_AcC3pt4nC3}
Key Checker
// Soon ™