Fentastic Moves
5 minutes to read
We are given a remote instance to connect to:
$ nc 188.166.175.58 32136
Let's see if you can find the best moves for 25 puzzles! (Don't take too long tho :P)
White to Move (always)
Example: e2e4
8
♚ 7
♛ ♘ 6
♞ 5
♖ ♟ 4
♜ 3
♔ 2
♞ 1
a b c d e f g h
What's the best move?
Solution
In this challenge, we are given a chess board and we are asked for the next best move. Moreover, the challenge description says:
Garry told me to catch some fish 20 meters deep
There are several hints here:
- “fish” refers to a well-known chess engine called Stockfish
- “20 meters deep” tells to use
go depth 20
as a command forstockfish
- The challenge name contains the substring “Fen”, which refers to Forsyth–Edwards Notation (FEN)
Parsing the chess board
First of all, we must parse the chess board to a FEN string in order to input that into stockfish
. For that, we need to understand how FEN works and then program it in Python. I found that chess pieces can be represented in Unicode:
pieces = {
u'\u2654': 'K',
u'\u2655': 'Q',
u'\u2656': 'R',
u'\u2657': 'B',
u'\u2658': 'N',
u'\u2659': 'P',
u'\u265A': 'k',
u'\u265B': 'q',
u'\u265C': 'r',
u'\u265D': 'b',
u'\u265E': 'n',
u'\u265F': 'p',
}
Now, we can use pwntools
to handle the TCP connection and start parsing the chess board into a FEN string. We just need to have a bit of patience to parse all ANSI escape codes:
host, port = sys.argv[1].split(':')
io = remote(host, port)
def board_to_fen() -> str:
fen = ''
io.recvuntil(b'\x1b[')
io.recvline()
for _ in range(8):
empty = 0
for data in io.recvline().strip().split(b'\x1b[')[2:-2]:
piece = data[data.index(b'm') + 1:]
if piece == b' ':
empty += 1
elif piece.decode() in pieces:
fen += str(empty or '') + pieces[piece.decode()]
empty = 0
fen += str(empty or '') + '/'
io.recvline()
return fen[:-1] + ' w - - 0 1'
As a sanity check, we can test that the FEN works using a web application like this one.
Working with stockfish
Once we have downloaded stockfish
, we can learn how to use it. There is also an online API, but it is not free for depths bigger than 13.
Therefore, we can enter the FEN string, add go depth 20
and take the line that contains bestmove
. For example:
$ ./stockfish/stockfish-ubuntu-x86-64
Stockfish 16 by the Stockfish developers (see AUTHORS file)
position fen 2n4N/3b4/R4R2/5B2/3Q2P1/7K/p5P1/5k2 w - - 0 1
go depth 20
info string NNUE evaluation using nn-5af11540bbfe.nnue enabled
info depth 1 seldepth 1 multipv 1 score cp 2306 nodes 107 nps 5944 hashfull 0 tbhits 0 time 18 pv f5e6 f1e2
info depth 2 seldepth 2 multipv 1 score cp 2295 nodes 178 nps 8090 hashfull 0 tbhits 0 time 22 pv f5d7 f1e2
info depth 3 seldepth 3 multipv 1 score cp 2260 nodes 256 nps 10666 hashfull 0 tbhits 0 time 24 pv f5d7 f1e1 a6a2
info depth 4 seldepth 4 multipv 1 score mate 2 nodes 338 nps 13520 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 5 seldepth 4 multipv 1 score mate 2 nodes 410 nps 16400 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 6 seldepth 4 multipv 1 score mate 2 nodes 482 nps 19280 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 7 seldepth 4 multipv 1 score mate 2 nodes 554 nps 22160 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 8 seldepth 4 multipv 1 score mate 2 nodes 626 nps 25040 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 9 seldepth 4 multipv 1 score mate 2 nodes 698 nps 27920 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 10 seldepth 4 multipv 1 score mate 2 nodes 770 nps 30800 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 11 seldepth 4 multipv 1 score mate 2 nodes 844 nps 33760 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 12 seldepth 4 multipv 1 score mate 2 nodes 918 nps 36720 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 13 seldepth 4 multipv 1 score mate 2 nodes 1021 nps 40840 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 14 seldepth 4 multipv 1 score mate 2 nodes 1097 nps 43880 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 15 seldepth 4 multipv 1 score mate 2 nodes 1193 nps 47720 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 16 seldepth 4 multipv 1 score mate 2 nodes 1265 nps 50600 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 17 seldepth 4 multipv 1 score mate 2 nodes 1337 nps 53480 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 18 seldepth 4 multipv 1 score mate 2 nodes 1409 nps 56360 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 19 seldepth 4 multipv 1 score mate 2 nodes 1481 nps 59240 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
info depth 20 seldepth 4 multipv 1 score mate 2 nodes 1553 nps 62120 hashfull 0 tbhits 0 time 25 pv f5d7 f1e1 a6e6
bestmove f5d7 ponder f1e1
We can use this tool with pwntools
too:
stockfish = process('./stockfish/stockfish-ubuntu-x86-64')
stockfish.recv()
def get_bestmove(fen: str):
stockfish.sendline(f'position fen {fen}\ngo depth 20'.encode())
stockfish.recvuntil(b'bestmove ')
return stockfish.recvline().decode().split(' ')[0]
The last thing we need is loop for 25 times:
round_prog = io.progress('Round')
for r in range(25):
round_prog.status(f'{r + 1} / 25')
fen = board_to_fen()
bestmove = get_bestmove(fen)
io.sendlineafter(b"What's the best move?\n", bestmove.encode())
io.recvline()
round_prog.success('25 / 25')
io.success(io.recvline().decode())
Flag
If we run the script, after a lot of attempts, we eventually find the correct move 25 times in a row and get the flag:
$ python3 solve.py 167.99.85.216:31668
[+] Starting local process './stockfish/stockfish-ubuntu-x86-64': pid 22354
[+] Opening connection to 167.99.85.216 on port 31668: Done
[+] Round: 25 / 25
[+] You did it! Here's your flag: HTB{th4nk_g0d_f0r_st0ckf1sh}
[*] Closed connection to 167.99.85.216 port 31668
The full script can be found in here: solve.py
.