CTF Team at the University of British Columbia

[corCTF 2021] smogofwar

25 Aug 2021 by David Zheng

[corCTF 2021] smogofwar

tl;dr

Beat a chess bot in “Smog of War” a variant of chess (almost?) identical to the Fog of War variation of chess by sending two different moves to the server.

Description

misc/smogofwar; strellic ; 7 solves / 497 points

Hey, I made a chess website to play my favorite variant, smog of war! Why don’t you check it out and play against my AI. He has some nasty surprises, but if you beat him I’ll give you a flag.. good luck :)

https://smogofwar.be.ax

You’re also given a zip file named smogofwar.zip that presumably has the contents of the website and the server that handles the chess game.

Solving the Challenge

The website contains a variation of chess against a bot where you can only see squares where either you have pieces, can move to, or capture. Beating the bot would give you the flag. As an added bonus, every time the bot captures your piece, it spits out a random piece of trash talk in the chat.

example

Playing around with the bot, the bot seems really strong, but seemed to move deterministically. I’m not good enough at chess for this, so I enlisting the help of an amateur chess friend Henry I had on hand. He tried to play the bot and realized it was not deterministic and seemed quite strong.

Looking at the source code for the bot, it turned out it was running the newest versions of the best chess bots: Stockfish 14.

class Enemy:
    def __init__(self, fen, emit):
        self.internal_board = chess.Board(fen)
        self.emit = emit
        self.stockfish = Stockfish("./stockfish_14_linux_x64_avx2/stockfish_14_x64_avx2", parameters={"Threads": 4})
        self.quit = False

Furthermore, it was given 10 seconds to think about each move.

        best_move = self.stockfish.get_best_move_time(10000)

On top of that our worst fears were confirmed when we realized that our move m1 was being sent to the bot (lemonthink), so the bot knew the entire state of the board while we were restricted by the fog of war!

        self.enemy.lemonthink(m1)

        enemy_move = self.enemy.normalthink(self.get_moves())
        self.play_move(enemy_move)

Henry informed me that even a professional chess player would struggle to beat stockfish, even without fog of war. We considered writing another bot running stockfish with more thinking time, but with fog of war and non-deterministic moves, it would have been difficult to beat the bot with another bot.

By looking closer at the code for the game, we found the following suspicious lines:

    def player_move(self, data):
        if self.get_turn() != chess.WHITE or self.is_game_over():
            return

        m0 = data
        m1 = data

        if isinstance(data, dict) and "_debug" in data:
            m0 = data["move"]
            m1 = data["move2"]

        if not self.play_move(m0):
            self.emit("chat", {"name": "System", "msg": "Invalid move"})
            return

If we sent a dictionary to the bot with the _debug field instead of a string containing our move, the move m0 would get played on the board but the move m1 would get sent to the bot. With this, we had could trick the bot by giving it a fake move, when we in fact performed another move!

However, it wasn’t that easy because the chess bot had various checks to make sure the game was progressing normally and would set self.quit=True if it detected anything out of the usual. If it quit, it wouldn’t output the flag:

    def resign(self):
        if self.quit:
            return

        self.chat("damn, how did you do that???")
        self.chat(FLAG)

Thus we had to beat the bot by only making moves that kept the bot’s internal board state consistent. The easiest way to do so would be to trick the bot one move before taking the king. I left the job up to Henry to find the best line, which he used a mix of playing the bot and https://lichess.org analysis tool to figure what the bot would likely do. In the end he came up with this sequence of moves where the bot behaved predictably.

winning

Then we would run this command in the javascript console:

socket.emit('move', {'_debug': true, 'move': 'd4e4', 'move2': 'd4f4'})

The command meant that we moved our queen to E4, which puts the king in check. This would normally prompt a block from the dark square bishop but the chess bot thought we moved the queen to F4. The bot didn’t know it was in check so this allowed us us to take the king and win!

wonned

corctf{"The opportunity of defeating the enemy is provided by the enemy himself." - Sun Tzu}

This was a pretty fun and interesting challenge. This was the intended solution for the challenge. The challenge authors speculated whether there were unintended solutions involving beating the bot legitimately, but we concluded that even if you had a professional chess player handy, it would be difficult.