Task | Nim |
---|---|
Competition | Dragon CTF 2021 |
Location | Online |
Category | Exploitation |
Platform | Linux x64 |
Scoring | 408 pts (medium) |
Number of solves | 5 out of 247 teams |
Let's play a game!
Click to expand
The task is a x64 ELF file with all mitigations enabled (NX, PIE, RELRO etc.) and it implements the Nim game in a player vs dealer (computer) mode. It is designed to highlight the fact that in modern versions of clang for x64 targets, the 7th and further function arguments (i.e. ones passed through the stack) are not copied below local buffers, which makes them subject to potential corruption via buffer overflows.
The two vulnerabilities are:
- Using the address of libc's
rand
as the seed for the internal PRNG state. - A continuous stack-based buffer overflow with 32-bit increments, triggered during the initialization of Nim heap sizes.
The second bug can be used to establish a 32-bit write-what-where primitive while saving the player's high score to the (overwritten) pointer passed through the 8th function argument. Immediately following this write, the program detects the corrupted cookie and calls __stack_chk_fail
. Further down in __fortify_fail
-> __libc_message
, the code uses unprotected, writable .got pointers to strchrnul
, strlen
and mempcpy
. If the previous write-what-where condition was used to overwrite either of these pointers, the control flow can be redirected to any address with the same upper 32 bits as the libc base.
The full exploitation process is as follows:
- Start the game and calculate the full 64-bit address of
rand
based on the first two Nim heap sizes generated by the program's PRNG. This both leaks the address of libc, and exposes the internal state of the generator. From this point, we can predict all future heap sizes. - Win several games against the dealer, until the score reaches the desired "what" value in the arbitrary write primitive. This is possible by carefully choosing the heap sizes to put the dealer in a losing starting position, and then playing the winning strategy for the game.
- Trigger the stack buffer overflow to set up the "write" part (libc .got section address), and leave the game. This performs the single controlled write, goes to
__stack_chk_fail
, and eventually calls our controlled address. - At this point, a one-gadget ROP, a stack pivot + a full ROP, or some other technique can be used to get code execution and read the flag.