Revenant - Midnight Flag CTF 2026
My writeup for the Revenant challenge:
Description
Something watches over you in this place. Every step, every decision — recorded, verified. It knows where you've been. It knows where you're going. It cannot be fooled. ...probably. Revenant.zip
Properties
| Text Only | |
|---|---|
1 2 3 4 5 6 7 8 | |
Writeup EN
French writeup and exploit below
The goal is to reach the win function, which gives us a shell:
| C | |
|---|---|
1 2 3 4 | |
And we quickly spot a stack buffer overflow in the play function:
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 | |
But you can't just do a simple ret2win because the return address of play is stored not only on the stack but also in the .bss section, and it is compared with the one on the stack before the return. If they are different, the return doesn't happen. This is what's called a Shadow Stack:
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
We therefore need to find a way to overwrite the return address stored in this shadow stack.
Note that shadow_stack is an array of 512 pointers and that shadow_stack_ptr serves as its index:
| C | |
|---|---|
1 2 3 4 | |
However, the program never checks that shadow_stack_ptr < 512! This means the top of the shadow stack can be moved outside the allocated area! Let's look at the other variables stored in the .bss section:
| Address | Variable |
|---|---|
0x407048 |
shadow_stack_base |
0x407040 |
shadow_stack_ptr |
0x407000 |
username |
0x406000 |
shadow_stack |
0x405010 |
entities |
0x40500c |
nights |
By incrementing shadow_stack_ptr enough, we can move the top of the stack into the space allocated for the username variable-which works out perfectly, since we can write 16 arbitrary bytes there!
| C | |
|---|---|
1 | |
So we'll write the 8 bytes of the win address inside it.
All that's left is to increment shadow_stack_ptr. To do this, we'll take advantage of the recursion of the play function via the do_reset function!
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Each recursive call adds a return address to the shadow stack and increments shadow_stack_ptr. By making 512 calls ((0x407000 - 0x406000) / 8) to the play function, the top of the shadow stack reaches username, and we can place the address of win there. All that's left is to overwrite the return address in the regular stack, and we're done!
Writeup FR
L'objectif est d'atteindre la fonction win qui nous donne un shell :
| C | |
|---|---|
1 2 3 4 | |
Et on aperçoit rapidement un Stack Buffer Overflow dans la fonction play:
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 | |
Mais on ne peut pas faire un simple ret2win car l'adresse de retour de play est stockée, en plus de la pile, dans la section .bss et elle est comparée avec celle de la stack avant le return. Si elles sont différentes, pas de return. C'est ce qu'on appelle une Shadow Stack :
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Il faut donc faut trouver un moyen de réécrire l'adresse de retour stockée dans cette shadow stack.
On peut remarquer que shadow_stack est un tableau de 512 pointeurs et que shadow_stack_ptr lui sert d'index :
| C | |
|---|---|
1 2 3 4 | |
Pour autant, le programme ne s'assure jamais que shadow_stack_ptr < 512 ! On peut donc déplacer le sommet de la shadow stack hors de la zone allouée ! Voyons les autres variables qui sont stockées dans la section .bss :
| Adresse | Variable |
|---|---|
0x407048 |
shadow_stack_base |
0x407040 |
shadow_stack_ptr |
0x407000 |
username |
0x406000 |
shadow_stack |
0x405010 |
entities |
0x40500c |
nights |
En incrémentant suffisamment shadow_stack_ptr on peut amener le sommet de la pile dans l'espace alloué pour la variable username et ça tombe bien car on peut écrire 16 octets arbitraires dedans !
| C | |
|---|---|
1 | |
On va donc écrire les 8 octets de l'adresse de win à l'intérieur.
Il reste à incrémenter shadow_stack_ptr. Pour ça on va profiter de la récursivité de la fonction play à travers la fonction do_reset !
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Chaque appel récursif va ajouter une adresse de retour dans la shadow stack et incrémenter shadow_stack_ptr. En effectuant 512 appels ((0x407000 - 0x406000) / 8) à la fonction play, le sommet de la shadow stack atteint username et on peut y mettre l'adresse de win. Plus qu'à écraser l'adresse de retour dans la pile classique et le tour est joué !
Exploit
| Python | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | |
Flag
| Text Only | |
|---|---|
1 | |