Lately I was busy with finalizing my LuaJIT Research and some other stuff too, but I decided to take a small break for this weekend to play the Hack.lu CTF(organized by @fluxfingers) which was very fun. Even though I’m a C person; for the last couple of months I learned some C++(personal side-projects) from a dev perspective and when I saw the placemat challenge I decided it’s time to deal with my all-time nemesis: pwning in the land of C++. Plus, the primitives in this chall were quite straight-forward, which is a great opportunity to practice. So without further ado, let’s get to it.
Task
We were given a zip file, containing:
- A compiled
placemat
binary(32bit, NX enabled, PIE disabled) - Sources and a
meson.build
file in case you’d want to create a build of your own
For some reason, I didn’t have all the symbols in the pre-compiled binary that they shipped in the zip file, so I decided to go with the following strategy: compile my own version → develop an exploit to fetch the demo flag → adjust some addresses in order to make it work on the production server to fetch the real flag.
It required me to spend some time fighting with the meson build system and compiler flags, but eventually I got it:
Technical background
The binary is a tic-tac-toe game, allowing you to play against a human(‘multiplayer’) or a bot(‘single player’). In order to get the flag you’ll have to win the game against the bot(which is practically impossible to defeat).
Both Human
and Bot
has a very similar layout, and they are both inheriting from the Player
class:
To start a new game, the binary has another class called Game
, which contains pointers to the current player, its opponent, etc.:
When starting a game(whether it’s against yourself or a bot) the player and opponent objects are allocated on the stack in the same order:
The layout here is important, visually this is how the objects are stored in memory:
4 bytes 20 bytes 4 bytes 20 bytes 4 bytes 4 bytes
+-------------------+------------------+------------------+----------------+--------------+----------*------+-----+
| Human vtable ptr | human->name[20] | Bot vtable ptr | bot->name[20] | *game->player | *game->opponent | ... |
+-------^-----------+------------------+--------^---------+----------------+-------|------+-------|---------+-----+
|_______________________________________|__________________________________| |
| |
| |
|_________________________________________________|
Spotting the vuln
The vulnerabillity was in the Human::requestName()
function:
It uses scanf
with the %s
format specifier, which only stops at the first whitespace character/has no actual size limit. This allows us to overflow past the human->name[20]
buffer into the opponent object.
Initial exploit (fail, partial)
The following script was the initial prototype for my exploit. My strategy was: to play against a Bot
object(because that’s the only way to get the flag) and replace the opponent’s vtable with a Human
vtable. This way, I’ll be the one who picks the moves for the opponent.
The following py script will generate a out.bin file that can be provided as input(i.e: cat out.bin | ./placemat
)
And it actually worked(well, almost). I managed to win & make the binary execute the Game::congratulate()
function.
However, even though my player won the game, the type-check in Game::congratulate()
prevented me from fetching the flag ;_; oof
Overcoming the type-check
As mentioned in the beginning of the post: I’m (somewhat) familiar with C++ concepts but from a dev perspective. So I know some internals on a very basic level. However, to solve this I realized it’s time for me to dig into some C++ compiler internals(a.k.a: the black magic).
To do that, I fired-up IDA and analyzed my local build to see what happens inside the std::type_info::operator!=(std::type_info const&)
function. Scary name I know, but bear with me.
We’ll start our analysis from our binary code to have the full context, then dive into the generated cpp code of the type_info !=
operator.
How types are compared
This is how the call/the type-check looks like from within our chall binary:
Essentially, when calling this function the binary provides a pointer to _ZTI3Bot
: this data structure contains pointers to the class’ type information(such as: its name, its parent’s type information):
The interesting part that caught my attention was that C++ compilers tags it with a name, which is saved as a string 3Bot
(highlighted).
Turns out, the way that std::type_info::operator!=(std::type_info const&)
compare object types is just by strcmp
ing their ‘tag’ string.
Note: The screenshot above shows the
==
and not the!=
operator because the implementation of!=
is just a wrapper of the function above. It takes the result and inverts it with xor operation. I chose to skip this part because prefer not too add too much useless noise to the analysis & jump straight to the point.
I also verified that by setting a breakpoint in gdb:
Knowing that, I had a new plan on how to overcome the type check.
New plan
So in order to bypass the type check and give the opponent a Human
vtable: we’ll have to craft a very specific struct. This struct will contain a type information pointer pointing to a Bot type info AND a vtable of a Human object.
Originally, this is how a Human
object looks in memory:
gef➤ print *this->player
$7 = {
_vptr.Player = 0x804c368 <vtable for Human+8>,
name = "lmao", '\000' <repeats 15 times>
}
gef➤ telescope 0x804c368-4
0x804c364│+0x0000: 0x804c378 → 0x804eedc → 0xf7e6dee0 → <__cxxabiv1::__si_class_type_info::~__si_class_type_info()+0>
0x804c368│+0x0004: 0x804b076 → <Human::~Human()+0>
0x804c36c│+0x0008: 0x804b09c → <Human::~Human()+0>
0x804c370│+0x000c: 0x804b0c6 → <Human::requestName()+0>
0x804c374│+0x0010: 0x804b100 → <Human::takeTurn(Board&)+0>
Side-note: A pointer to the type information is saved behind the vtable address(at
(&vtable)-4
or(&vtable-8
on 64bit).
So our spoofed object’s layout should look like the following:
+0 ptr to Bot's type info struct
+4 Human vtable function ptr #1
+8 Human vtable function ptr #2
+12 Human vtable function ptr #3
+16 Human vtable function ptr #4
It adds up to a total of 20 bytes(five 32bit pointers). This size matches up exactly with our input buffer :^), nice! so we’ll store it in our player’s name(name[20]
) and make the opponent’s vtable ptr to point there. But to do that, we’ll have to get some leaks.
Getting leaks
We’ll need to know beforehand the address of player->name[]
because we’re going to overwrite the opponent’s vtable ptr with this value.
Getting leaks wasn’t too difficult: in order to understand where our payload begins in memory we can fill the opponent->name[20]
buffer with exactly 20 bytes, all the way to the beginning of the Game
object(where pointers to player
and opponent
are stored) without sending a nullbyte.
It will lead to a creation of a one contiguous string(opponent’s name, followed by Game
’s properties). Thus, when the players’ names are printed to the screen - the program will spill 20 bytes of the opponent’s name followed by the pointers of the Game
object.
Final exploit
To add everything together, the final plan is as follows:
- Play 1st time:
- Leaks: to understand where our payload starts, we’ll play once with a buffer of size 20 as the opponent’s name.
- Play 2nd time:
- Craft a data structure that points to a
Bot
type information and has aHuman
vtable function pointers. - Corrupt opponent’s vtable ptr and make it point to our spoofed struct(we know this address using the leak)
- Play, win, get the flag
- Craft a data structure that points to a
Below is the final exploit:
output:
$ ./jax-prod.py REMOTE
[+] Opening connection to flu.xxx on port 11701: Done
[*] Starting a new game
[*] Getting leaks
[*] this->player :: 0xff842eec
[*] this->opponent :: 0xff842f04
[*] this->activePlayer :: 0xff842f04
[*] Playing ...
[*] Starting a new game
[*] Pwning opponent's vtable ptr
[*] Playing ...
[*] Switching to interactive mode
X \xd4\xc1\x04Ю\x04\xf2\xa\x18\x04N\xaf\x04\xf4.\x84\xfflmao-poggers lmao-poggers O
A B C
1 X │ O │
───┼───┼───
2 X │ O │
───┼───┼───
3 X │ │
\xd4\xc1\x04Ю\x04\xf2\xa\x18\x04N\xaf\x04\xf4.\x84\xfflmao-poggers won!
Congratulations for defeating lmao-poggers.
The redemption code for your free dessert is: FLAG{They_told_me_the_only_winning_move_was_not_to_play._Yet_I_lost}
Thanks for the challenge!