Cracking the Flare-On 11 CTF 2024: Challenge 1 - Frog
Dive into my journey through the Flare-On 11 CTF Challenge! I tackled all 10 challenges this year, and in this series, I’ll break down my thought process, strategies, and discoveries. Whether you’re a beginner or an experienced CTF enthusiast, expect plenty of insights and detailed explanations to guide you through each step!
Overview
The Flare-On Challenge is the FLARE team’s annual Capture-the-Flag (CTF) contest. It is a single-player series of Reverse Engineering puzzles that runs for 6 weeks every fall. #flareon11 is launching Sept. 27th 2024 at 8pm EST.
This is the very first challenge and here is the description from the page.
1 - frog
Welcome to Flare-On 11! Download this 7zip package, unzip it with the password ‘flare’, and read the README.txt file for launching instructions. It is written in PyGame so it may be runnable under many architectures, but also includes a pyinstaller created EXE file for easy execution on Windows.
Your mission is get the frog to the "11" statue, and the game will display the flag. Enter the flag on this page to advance to the next stage. All flags in this event are formatted as email addresses ending with the @flare-on.com domain.
Download and unzip the file with password flare
we can see a few files that are good to start
1
2
3
4
5
6
$ ls
README.txt
fonts
frog.exe
frog.py
img
As we have the source code frog.py
, we can directly jump into the codes for understanding the game logic to find the flag, but what was the fun without seeing how does the game look like? There will be many approaches to solve this challenge, let start with normal one first.
Install dependencies
This step is optional for Windows users and for ones who don’t need to run the game.
The frog.py
is importing pygame
, hence we need to install this page. We can use pip3 to install pygame
package:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ pip3 install pygame
error: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try brew install
xyz, where xyz is the package you are trying to
install.
If you wish to install a Python library that isn't in Homebrew,
use a virtual environment:
python3 -m venv path/to/venv
source path/to/venv/bin/activate
python3 -m pip install xyz
If you wish to install a Python application that isn't in Homebrew,
it may be easiest to use 'pipx install xyz', which will manage a
virtual environment for you. You can install pipx with
brew install pipx
You may restore the old behavior of pip by passing
the '--break-system-packages' flag to pip, or by adding
'break-system-packages = true' to your pip.conf file. The latter
will permanently disable this error.
If you disable this error, we STRONGLY recommend that you additionally
pass the '--user' flag to pip, or set 'user = true' in your pip.conf
file. Failure to do this can result in a broken Homebrew installation.
Read more about this behavior here: <https://peps.python.org/pep-0668/>
note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.
As you can see, it recommends using a virtual environment instead of a system-wide installation to avoid risking issues with our Python setup or operating system. Let’s follow the instructions to create a virtual environment instead.
1
2
3
4
5
6
7
8
$ python3 -m venv path/to/venv
$ source path/to/venv/bin/activate
(venv) $ python3 -m pip install pygame
Collecting pygame
Using cached pygame-2.6.1-cp313-cp313-macosx_11_0_arm64.whl.metadata (12 kB)
Using cached pygame-2.6.1-cp313-cp313-macosx_11_0_arm64.whl (12.4 MB)
Installing collected packages: pygame
Successfully installed pygame-2.6.1
We just ran three commands to create, activate, and install pygame
in the virtual environment (you’ll notice the virtual environment is activated by the (venv)
prefix).
Start the game - Run frog.py
1
2
3
(venv) $ python3 frog.py
pygame 2.6.1 (SDL 2.28.4, Python 3.13.0)
Hello from the pygame community. https://www.pygame.org/contribute.html
After starting frog.py
from the command line, you can see the game window appears with a frog, a Flareon statue labeled 11
(the target where we need to move the frog), and walls (blocks) that protect the statue from the frog.
Figure: 2 - Starting frog position
We can use the arrow keys to move the frog step by step, but there is no way to get the frog close to the statue because the wall blocks our path.
Explore the source code - frog.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import pygame
pygame.init()
pygame.font.init()
screen_width = 800
screen_height = 600
tile_size = 40
tiles_width = screen_width // tile_size
tiles_height = screen_height // tile_size
screen = pygame.display.set_mode((screen_width, screen_height))
clock = pygame.time.Clock()
victory_tile = pygame.Vector2(10, 10)
pygame.key.set_repeat(500, 100)
pygame.display.set_caption('Non-Trademarked Yellow Frog Adventure Game: Chapter 0: Prelude')
dt = 0
floorimage = pygame.image.load("img/floor.png")
blockimage = pygame.image.load("img/block.png")
frogimage = pygame.image.load("img/frog.png")
statueimage = pygame.image.load("img/f11_statue.png")
winimage = pygame.image.load("img/win.png")
gamefont = pygame.font.Font("fonts/VT323-Regular.ttf", 24)
text_surface = gamefont.render("instruct: Use arrow keys or wasd to move frog. Get to statue. Win game.",
False, pygame.Color('gray'))
flagfont = pygame.font.Font("fonts/VT323-Regular.ttf", 32)
flag_text_surface = flagfont.render("nope@nope.nope", False, pygame.Color('black'))
class Block(pygame.sprite.Sprite):
def __init__(self, x, y, passable):
super().__init__()
self.image = blockimage
self.rect = self.image.get_rect()
self.x = x
self.y = y
self.passable = passable
self.rect.top = self.y * tile_size
self.rect.left = self.x * tile_size
def draw(self, surface):
surface.blit(self.image, self.rect)
class Frog(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.image = frogimage
self.rect = self.image.get_rect()
self.x = x
self.y = y
self.rect.top = self.y * tile_size
self.rect.left = self.x * tile_size
def draw(self, surface):
surface.blit(self.image, self.rect)
def move(self, dx, dy):
self.x += dx
self.y += dy
self.rect.top = self.y * tile_size
self.rect.left = self.x * tile_size
blocks = []
player = Frog(0, 1)
def AttemptPlayerMove(dx, dy):
newx = player.x + dx
newy = player.y + dy
# Can only move within screen bounds
if newx < 0 or newx >= tiles_width or newy < 0 or newy >= tiles_height:
return False
# # See if it is moving in to a NON-PASSABLE block. hint hint.
for block in blocks:
if newx == block.x and newy == block.y and not block.passable:
return False
player.move(dx, dy)
return True
def GenerateFlagText(x, y):
key = x + y*20
encoded = "\xa5\xb7\xbe\xb1\xbd\xbf\xb7\x8d\xa6\xbd\x8d\xe3\xe3\x92\xb4\xbe\xb3\xa0\xb7\xff\xbd\xbc\xfc\xb1\xbd\xbf"
return ''.join([chr(ord(c) ^ key) for c in encoded])
def main():
global blocks
blocks = BuildBlocks()
victory_mode = False
running = True
while running:
# poll for events
# pygame.QUIT event means the user clicked X to close your window
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_w or event.key == pygame.K_UP:
AttemptPlayerMove(0, -1)
elif event.key == pygame.K_s or event.key == pygame.K_DOWN:
AttemptPlayerMove(0, 1)
elif event.key == pygame.K_a or event.key == pygame.K_LEFT:
AttemptPlayerMove(-1, 0)
elif event.key == pygame.K_d or event.key == pygame.K_RIGHT:
AttemptPlayerMove(1, 0)
# draw the ground
for i in range(tiles_width):
for j in range(tiles_height):
screen.blit(floorimage, (i*tile_size, j*tile_size))
# display the instructions
screen.blit(text_surface, (0, 0))
# draw the blocks
for block in blocks:
block.draw(screen)
# draw the statue
screen.blit(statueimage, (240, 240))
# draw the frog
player.draw(screen)
if not victory_mode:
# are they on the victory tile? if so do victory
if player.x == victory_tile.x and player.y == victory_tile.y:
victory_mode = True
flag_text = GenerateFlagText(player.x, player.y)
flag_text_surface = flagfont.render(flag_text, False, pygame.Color('black'))
print("%s" % flag_text)
else:
screen.blit(winimage, (150, 50))
screen.blit(flag_text_surface, (239, 320))
# flip() the display to put your work on screen
pygame.display.flip()
# limits FPS to 60
# dt is delta time in seconds since last frame, used for framerate-
# independent physics.
dt = clock.tick(60) / 1000
pygame.quit()
return
def BuildBlocks():
blockset = [
Block(3, 2, False),
Block(4, 2, False),
Block(5, 2, False),
Block(6, 2, False),
Block(7, 2, False),
Block(8, 2, False),
Block(9, 2, False),
Block(10, 2, False),
Block(11, 2, False),
Block(12, 2, False),
Block(13, 2, False),
Block(14, 2, False),
Block(15, 2, False),
Block(16, 2, False),
Block(17, 2, False),
Block(3, 3, False),
Block(17, 3, False),
Block(3, 4, False),
Block(5, 4, False),
Block(6, 4, False),
Block(7, 4, False),
Block(8, 4, False),
Block(9, 4, False),
Block(10, 4, False),
Block(11, 4, False),
Block(14, 4, False),
Block(15, 4, True),
Block(16, 4, False),
Block(17, 4, False),
Block(3, 5, False),
Block(5, 5, False),
Block(11, 5, False),
Block(14, 5, False),
Block(3, 6, False),
Block(5, 6, False),
Block(11, 6, False),
Block(14, 6, False),
Block(15, 6, False),
Block(16, 6, False),
Block(17, 6, False),
Block(3, 7, False),
Block(5, 7, False),
Block(11, 7, False),
Block(17, 7, False),
Block(3, 8, False),
Block(5, 8, False),
Block(11, 8, False),
Block(15, 8, False),
Block(16, 8, False),
Block(17, 8, False),
Block(3, 9, False),
Block(5, 9, False),
Block(11, 9, False),
Block(12, 9, False),
Block(13, 9, False),
Block(15, 9, False),
Block(3, 10, False),
Block(5, 10, False),
Block(13, 10, True),
Block(15, 10, False),
Block(16, 10, False),
Block(17, 10, False),
Block(3, 11, False),
Block(5, 11, False),
Block(6, 11, False),
Block(7, 11, False),
Block(8, 11, False),
Block(9, 11, False),
Block(10, 11, False),
Block(11, 11, False),
Block(12, 11, False),
Block(13, 11, False),
Block(17, 11, False),
Block(3, 12, False),
Block(17, 12, False),
Block(3, 13, False),
Block(4, 13, False),
Block(5, 13, False),
Block(6, 13, False),
Block(7, 13, False),
Block(8, 13, False),
Block(9, 13, False),
Block(10, 13, False),
Block(11, 13, False),
Block(12, 13, False),
Block(13, 13, False),
Block(14, 13, False),
Block(15, 13, False),
Block(16, 13, False),
Block(17, 13, False)
]
return blockset
if __name__ == '__main__':
main()
The game screen has a size of 800 x 600
, and each tile is 40 x 40
, which means there are 800 / 40 = 20
columns and 600 / 40 = 15
rows.
The code also includes a hard-coded victory_tile = pygame.Vector2(10, 10)
, meaning that if the frog reaches the tile position (10, 10)
, we win the game and can get the flag. Let’s note this for reference.
There is a Block
class that stores each block’s location and indicates if it’s passable (allowing the frog to move over it). Additionally, there is a Frog
class, which contains the frog’s current location, an array of blocks
containing Block
instances (representing the wall we see on the game screen that blocks the frog from reaching the statue), an AttemptPlayerMove
function to validate if the new position is a valid move (checking if the frog is out of screen boundaries or hitting the blocks), a main
function to handle user keyboard inputs for moving the frog, and finally, a GenerateFlagText
function, which decodes the encoded flag and displays it on the screen. With this information, we have enough to solve this challenge. There are several approaches to solve it, both with and without running the game.
Solutions
1st approach - Modify player starting position
In the source code, the player starts at column 1 and row 0 with player = Frog(0, 1)
. Since we know the hard-coded victory_tile = pygame.Vector2(10, 10)
position, we can change the player’s starting position to player = Frog(10, 10)
instead and re-run the game. Kaboom!
Figure: 3 - Modify player starting position - Kaboom!!!
2nd approach - Make blocks passable
Now let’s have a bit of fun and allow the frog to leap over the blocks! To do this, we can force passable
to always be True
regardless of the value passed into the Block
constructor, like this:
1
2
3
4
5
6
7
8
9
10
11
class Block(pygame.sprite.Sprite):
def __init__(self, x, y, passable):
super().__init__()
self.image = blockimage
self.rect = self.image.get_rect()
self.x = x
self.y = y
# self.passable = passable # commented out this original code
self.passable = True # force all blocks are passable, which means it's nolonger a block :D
self.rect.top = self.y * tile_size
self.rect.left = self.x * tile_size
Re-run the game, and let the kids have fun! ^_^
Figure: 4 - Do you see the frog can leap?
3rd approach - Find the XOR key
The above approaches require patching and running the game, which means setting up a Python environment. But what if we don’t want that and prefer to just use static analysis?
Since we know that the GenerateFlagText(x, y)
method is responsible for decoding the flag, let’s try to understand what it does in this context.
1
2
3
4
def GenerateFlagText(x, y):
key = x + y*20
encoded = "\xa5\xb7\xbe\xb1\xbd\xbf\xb7\x8d\xa6\xbd\x8d\xe3\xe3\x92\xb4\xbe\xb3\xa0\xb7\xff\xbd\xbc\xfc\xb1\xbd\xbf"
return ''.join([chr(ord(c) ^ key) for c in encoded])
This method contains a hardcoded encoded Unicode string with characters represented by escape sequences like \xa5
, which are hexadecimal byte values. It iterates over each character c
in the encoded
string, converts c
to its integer ASCII (or byte) representation using ord(c)
, and then XOR
s it with the key to decode the character. The result is converted back into a character using chr()
.
Since we know the victory_tile
earlier is (10, 10)
, we can calculate the XOR
key: key = 10 + 10 * 20 = 210
.
Now, let’s assume the challenge didn’t hardcode the plain victory_tile
value, and we don’t know the key
value. How could we determine the key
to decode the flag?
Looking again at the GenerateFlagText
function, it uses a single key
to XOR
with each encoded character. From the challenge description, we also know that “All flags in this event are formatted as email addresses ending with the @flare-on.com domain”. This allows us to use a known-plaintext attack to recover the key
value and decode the entire encoded
flag.
Here’s the formula we have: ord(encrypted_c) ^ key = ord(decrypted_c)
, and we can recover the key using key = ord(encrypted_c) ^ ord(decrypted_c)
.
We know some decrypted_c
characters from the plain text email. Let’s take the last character, decrypted_c = 'm'
, and the last byte character from encoded string is ord(encrypted_c) = 0xbf
. Plugging this into the formula, we get:
key = ord(encrypted_c) ^ ord(decrypted_c) = 0xbf ^ ord('m') = 210
Now we have the key
value recovered and can use this key to decode the flag.
1
2
3
4
5
6
7
8
9
$ python3
Python 3.13.0 (main, Oct 7 2024, 05:02:14) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> encoded = "\xa5\xb7\xbe\xb1\xbd\xbf\xb7\x8d\xa6\xbd\x8d\xe3\xe3\x92\xb4\xbe\xb3\xa0\xb7\xff\xbd\xbc\xfc\xb1\x\
bd\xbf"
>>> flag = ''.join([chr(ord(c) ^ 210) for c in encoded])
>>> print(flag)
welcome_to_11@flare-on.com
>>>
Conclusion
The first challenge is usually an easy warm-up, and we’ve explored a few approaches to tackle it. In any case, this has been a fun game to play with the kids ^_^