Pour commencer ce challenge de reverse, je commence par décompiler le binaire afin d’en savoir plus sur son fonctionnement. Pour cela j’utilise Ghidra (je m’excuse auprès de tous les fans d’IDA pour cette hérésie).
Analyse statique
Le programme prend un seul argument passé par l’utilisateur. La taille de ce dernier doit faire une taille de 39 caractères et doit respecter un certain nombre de conditions qui sont ici toutes représentées par une succession (interminable) de conditions IF. Si jamais une de ces conditions n’est pas respectée, la fonction revolte()
est appelée.
void revolte(void)
{
puts("REVOLTE !!!"); // On comprend bien que c'est le game over.
exit(0);
}
A la toute fin de notre fonction main()
bien fournie se trouve l’affichage du message suivant : Félicitations, vous pouvez valider avec ce flag!
aka le St Graal.
Maintenant que nous savons que notre but est d’atteindre ce message de félicitation, nous pouvons sortir notre outil préféré: ANGR !!
Version non optimisée
Je vais commencer par vous introduire la version non optimisée (et ignoble) de mon script ayant servi à résoudre le challenge.
Voilà à quoi ce dernier ressemble :
import angr
import claripy
import sys
def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
arg1 = claripy.BVS('arg1', 8*60)
initial_state = project.factory.entry_state(args=["breton", arg1])
simulation = project.factory.simgr(initial_state)
good_address = 0x4015dd
avoid_address = 0x401156
simulation.explore(find=good_address , avoid=avoid_address)
if simulation.found:
solution_state = simulation.found[0]
print(solution_state.solver.eval(arg1, cast_to = bytes))
else:
raise Exception('Could not find the solution')
if __name__ == '__main__':
main(sys.argv)
Ce script est un usage basique et très classique de ANGR. Tout d’abord, il va falloir indiquer à ANGR où regarder pour réaliser sa magie noire. C’est pour cela que l’on déclare un bitvector symbolique (BVS
). Ici je lui est donné une taille arbitraire (il faut flag rapidement pas le temps de lire la première condition sur la taille de l’input ^^).
On initialise le moteur de ANGR avec les fonctions entry_state()
et simgr()
. Il n’y a rien à dire sur ces dernières puisque nous ne faisons rien de vraiment avancé.
On va ensuite donner à ANGR des indications sur la ou les adresses à ne pas (ou à) atteindre. Notre but est de ne pas arriver dans la fonction revolte()
et par conséquent on va donc indiquer à ANGR d’exclure l’adresse correspondante. Pour lui simplifier un peu la recherche, on va lui indiquer l’adresse qu’il devra atteindre même si dans notre cas c’est un peu overkill.
On le laisse faire son exploration est normalement au bout de quelques minutes on obtient le flag : BZHCTF{R3V3r5eR_l177le_ch3E5er_uhu-uhu}
Note : la solution de l’auteur est disponible ici.
Bonus
Actuellement notre script est fonctionnel, mais on peut dans un but d’optimisation l’améliorer afin de réduire le temps d’exécution. Je n’ai pas réalisé, lors du CTF, les optimisations qui vont être présentées.
Déjà on sait que tous les flags sont de la forme BZHCTF{ ... }
et que le contenu du flag est en caractères imprimables. On peut donc ajouter des contraintes à notre input dans le but d’orienter ANGR dans sa recherche. Le code suivant va nous permettre de réaliser l’application des contraintes :
begin_flag = 'BZHCTF{'
for index in range(len(begin_flag)):
initial_state.add_constraints(arg1.chop(8)[index] == ord(begin_flag[index]))
initial_state.add_constraints(arg1.chop(8)[38] == ord('}'))
for index in range(7, 38, 1):
initial_state.add_constraints(arg1.chop(8)[index] >= 0x20)
initial_state.add_constraints(arg1.chop(8)[index] <= 0x7e)
Et puis pour des questions d’optimisation de place mémoire, on va réduire la taille de notre BVS
a seulement 39 caractères.
Il existe de nombreux autres moyens d’optimiser la recherche ainsi que d’autres comportements internes d’ANGR mais je ne vais pas les aborder ici.
À propos de l’auteur
Article écrit par Maël FRAZAO alias Morncraban, Expert cybersécurité chez ACCEIS.