Toujours dans l'optique de maitriser radare2, aujourd'hui on s'attaque au challenge "Be Quick or Be dead" du picoCTF2018.

Be quick or be dead et on parle pas de Nesquick ici !

Init

Comme d'habitude, commençons par quelques vérifications.

rabin2 -I be-quick-or-be-dead-1 

arch     x86
baddr    0x400000
binsz    7308
bintype  elf
bits     64
canary   false
class    ELF64
compiler GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609
crypto   false
endian   little
havecode true
intrp    /lib64/ld-linux-x86-64.so.2
laddr    0x0
lang     c
linenum  true
lsyms    true
machine  AMD x86-64 architecture
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
relro    partial
rpath    NONE
sanitiz  false
static   false
stripped false
subsys   linux
va       true
Non stripped et lié dynamiquement, poursuivons...
file be-quick-or-be-dead-1 

be-quick-or-be-dead-1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=909cc4117c2c766583b0633d70b84771e50160d6, not stripped
_$ rabin2 -zz be-quick-or-be-dead-1 


[Strings]
nth paddr      vaddr      len size section            type    string
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
4   0x0000039b 0x0040039b 4   5    .dynstr            ascii   exit
5   0x000003a0 0x004003a0 4   5    .dynstr            ascii   puts
6   0x000003a5 0x004003a5 7   8    .dynstr            ascii   putchar
7   0x000003ad 0x004003ad 6   7    .dynstr            ascii   printf
8   0x000003b4 0x004003b4 5   6    .dynstr            ascii   alarm
9   0x000003ba 0x004003ba 13  14   .dynstr            ascii   _
19  0x00000900 0x00400900 35  36   .rodata            ascii   You need a faster machine. Bye bye.

20  0x00000928 0x00400928 95  96   .rodata            ascii   \n\nSomething went terribly wrong. \nPlease contact the admins with "be-quick-or-be-dead-1.c:%d".\n
21  0x00000988 0x00400988 18  19   .rodata            ascii   Calculating key...
22  0x0000099b 0x0040099b 20  21   .rodata            ascii   Done calculating key
23  0x000009b0 0x004009b0 14  15   .rodata            ascii   Printing flag:
24  0x000009c0 0x004009c0 21  22   .rodata            ascii   Be Quick Or Be Dead 1

72  0x00001b0c 0x0000026c 4   5    .strtab            ascii   main
73  0x00001b11 0x00000271 13  14   .strtab            ascii   calculate_key
74  0x00001b1f 0x0000027f 19  20   .strtab            ascii   _Jv_RegisterClasses
75  0x00001b33 0x00000293 10  11   .strtab            ascii   print_flag
J'ai fais le tri dans ce que nous propose cette commande, histoire d'y voir plus clair

On remarque la présence d'une alarm, si on se fie au nom du challenge et au autres strings, il y a une vérification de la rapidité d'exécution du programme.

Lançons le programme:

Un fameux problème de vitesse

C'est donc un classique du challenge de reverse en CTF que nous avons ici.

Analyse statique

r2 be-quick-or-be-dead-1 
 -- Are you a wizard?
 
[0x004005a0]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for vtables
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information
[x] Use -AA or aaaa to perform additional experimental analysis.

[0x004005a0]> afl
0x004005a0    1 41           entry0
0x00400560    1 6            sym.imp.__libc_start_main
0x004005d0    4 50   -> 41   sym.deregister_tm_clones
0x00400610    4 58   -> 55   sym.register_tm_clones
0x00400650    3 28           sym.__do_global_dtors_aux
0x00400670    4 38   -> 35   entry.init0
0x004008e0    1 2            sym.__libc_csu_fini
0x00400696    6 112          sym.decrypt_flag
0x00400723    1 31           sym.alarm_handler
0x00400530    1 6            sym.imp.puts
0x00400580    1 6            sym.imp.exit
0x004007e9    4 62           sym.header
0x004008e4    1 9            sym._fini
0x00400796    1 43           sym.get_key
0x00400706    3 29           sym.calculate_key
0x00400870    4 101          sym.__libc_csu_init
0x00400827    1 62           main
0x00400742    3 84           sym.set_timer
0x00400570    1 6            sym.imp.__sysv_signal
0x00400540    1 6            sym.imp.printf
0x00400550    1 6            sym.imp.alarm
0x004007c1    1 40           sym.print_flag
0x004004e8    3 26           sym._init
0x00400520    1 6            sym.imp.putchar
[0x004005a0]> 

On remarque déjà quelques fonctions intéressantes:

  1. sym.decrypt_flag
  2. sym.calculate_key
  3. sym.print_flag

Explorons le main:

+

On va donc explorer les fonctions sym.header,  sym.set_timer, sym.get_key ,sym.print_flag une à une.

sym.header

Cette fonction est en charge d'afficher le message d'intro. La partie en rouge sur cette image. On aurait pu le deviner en se fiant uniquement à son nom mais rien ne vaut la certitude de ne pas passer à coté d'indice.

Explorons plus en détail le fonctionnement de cette fonction:

En rouge nous avons la partie en charge de l'affichage de "Be Quick Or Be Dead 1".

En bleu nous avons la boucle en charge de l'affichage des 22 caractères "=".

En violet nous avons le code en charge de l'affichage du retour à la ligne.

Ici on peut voir le contenu de la mémoire qui va être affiché. Il s'agit d'un retour à la ligne dans le style de linux

Passons à la suite.

sym.set_timer

Cette fonction est plus complexe mais grâce à r2decce n'est pas un soucis.

Cette fonction met en place un timer d'une seconde. CF son manuel.

Le fameux timer

Si la mise en place du timer déconne alors un message d'erreur est affiché.

Comme on sait que l'opération de déchiffrement de la clé prend un certains temps, on pourrait rallonger le timer.

Pour ça il va falloir patcher le binaire. Heureusement pour nous, R2 intègre un assembleur en ligne de commande.

On va commencer par ré-ouvrir le binaire en mode écriture

r2 -w be-quick-or-be-dead-1 

On relance les analyses pour retrouver nos petits et on passe en mode visuel (V!).

On se rend à la fonction qui nous intéresse

s main
<enter>
2 # Pour suivre directement le call a sym.set_timer

Ensuite on descend le curseur pour le placer sur la ligne qui nous intéresse.

Pour finir on appuie sur A (la majuscule est importante)

Et on écrit ceci

mov dword [rbp-0xC], 10

NB: Il se peut que R2 remplace le nom du registre par "var_ch", dans ce cas le générateur d'assembleur ne peut pas fonctionner. Pour avoir le véritable nom du registre il suffit de lire le prologue de la fonction.

Si tout ce passe bien, R2 est capable de compiler le code qu'on lui donne directement en assembleur.

On appuie sur entrée et on confirme le changement (cf le bas de l'écran)

Normalement on obtient ça:

On ferme R2 pour libérer l'attribution du fichier, sinon on à ce genre de message:

Et si tout est bon alors on obtient ça:

Mais cet article ne serait pas complet sans une étude "presque" approfondie du système de génération de la clé.

sym.get_key

Cette fonction est très simpliste puisqu'elle ce contente d'afficher quelques informations et d'appeler la fonction en charge du calcul de la clé.
Explorons cette fonction.

sym.calculate_key

Cette fonction se contente d'incrémenter une variable. Rien de bien folichon, juste des calculs pour perdre du temps.

Remontons dans l'arborescence pour explorer la 4ème fonction du main.

sym.print_flag

Cette fonction aussi n'est pas très utile, elle se contente d'appeler une fonction au doux nom de sym.decrypt_flag. Allons y jeter un coup d'oeil.

sym.decrypt_flag

Ouch c'est trapu comme fonction !

A première vue, on a une boucle qui fait 58 itérations (1er passage + 57 itérations). Ce qui correspondrait au 58 caractères de notre flag.

On pourrait penser à partir de là que le calcul de la clé fait précédemment est utilisé dans cette fonction. Pour vérifier ça, rien de plus simple il suffit de patcher à nouveau le binaire pour supprimer l'appel à sym.get_key dans la fonction main.

Pour ce faire, on se rend dans le main

s main

Ensuite on descend le curseur jusqu'à l'appel qui nous intéresse et on le remplace par 0x9090909090.

s 0x004085f
wx 0X9090909090

Normalement on obtient ça:

Et si on réexécute le programme on voit bien que cette fonction fournit bien à la suivante la clé de déchiffrement.

Bon, il est temps de mettre fin à cette digression. Peut être que j'aborderais plus en détails le fonctionnement de la fonction de déchiffrement du flag dans un futur article.

Conclusion

Un challenge classique de CTF, un "must have done" comme dirait les bilingues.
Pas trop dur mais on a couvert une nouvelle fonctionnalité de radare2: la patching à la volée.

Pour plus d'info sur le fonctionnement de R2, je vous conseille la playlist youtube de Binary Adventure.

Social et Media

Comme toujours je suis disponible sur Twitter et cie, si vous avez des questions, des remarques, des suggestions etc. N’hésitez pas !

Twitter: @GhostAgs

Discord: hackraw