Block CTF | Red Flags (Rev, Godot Game Hacking)
Difficulty : Beginner

I don't usually do reverse engineering but this is a very easy one.
This challenge consists of a godot game, this challenge has flags that would be activated or deactivated, each combination moves the flag letters, so unless we would try every possible combination we would need to find a way to hack the game.

We can decompile the game easily with gdsdecomp, after decompiling it we get the code.
We first see that the characters are each stored as char, there is no proper order in the code so we analyze further.
We have two GDScript files, arena.tscn for the game and flag.tscn for the flags :
arena.tscn :
extends Node2D
var flags
# Called when the node enters the scene tree for the first time.
func _ready():
flags = get_children().filter(func(child): return child.name.match(\"Flag_*\"))
func hex_byte_to_int(c):
if c >= 0x30 && c <= 0x39:
return c - 0x30
else:
return c - 0x37
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
var states = []
for flag in flags:
states.append(int(flag.target_state))
var flaggregate = \"\".join(states)
var sha = flaggregate.sha1_text().to_upper()
sha += flaggregate.md5_text().to_upper()
var chars = %FlagText.get_children()
for i in chars.size():
chars[i].target_x = hex_byte_to_int(sha.unicode_at(i * 2)) - 8
chars[i].target_y = hex_byte_to_int(sha.unicode_at((i * 2) + 1)) - 8
flag.tscn :
extends StaticBody2D
class_name Flag
const TRANSITION_TICKS = 100
var target_state = true
var is_transitioning = false
@export var current_state = 1.0
# Called when the node enters the scene tree for the first time.
func _ready():
%FlagSprite.material = %FlagSprite.material.duplicate()
%FlagSprite.material.set_shader_parameter(\"state\", current_state)
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
if is_transitioning:
%FlagSprite.material.set_shader_parameter(\"state\", current_state)
if target_state:
current_state += 1.0/TRANSITION_TICKS
if current_state >= 1:
current_state = 1
is_transitioning = false
else:
current_state -= 1.0/TRANSITION_TICKS
if current_state <= 0:
current_state = 0
is_transitioning = false
func _on_area_2d_body_entered(body):
is_transitioning = true
target_state = not target_state
pass
movable_char.gd that controls the movement of the chars :
extends Label
const FLY_SPEED = 5.0
const scalar = 50.0
var origin
var target_x = 0
var target_y = 0
# Called when the node enters the scene tree for the first time.
func _ready():
origin = position
func _physics_process(delta):
var destination = origin + (Vector2(target_x, target_y) * scalar)
position = position.lerp(destination, delta * FLY_SPEED)
Now we can see in the arena file that the game reads the flag combinations, generates a hash that it concats to another hash and then from it takes the position of the characters, so from that we can conclude we would need to brute force this.
My strategy was to bruteforce each combination and screenshot every frame, so first we need to change the arena file to include screenshotting and loop through every combination.
extends Node2D
var X=0
var flags
# Called when the node enters the scene tree for the first time.
var screenshot_counter = 0
func generate_poss():
var size = 10
var possibilities = []
# Loop through all numbers from 0 to 2^size - 1
for i in range(1 << size): # 1 << size is the same as 2^size
var binary_array = []
for j in range(size):
binary_array.append((i >> j) & 1) # Get the j-th bit of i
possibilities.append(binary_array)
return possibilities
var posses=generate_poss()
func _ready():
flags = get_children().filter(func(child): return child.name.match("Flag_*"))
func hex_byte_to_int(c):
if c >= 0x30 && c <= 0x39:
return c - 0x30
else:
return c - 0x37
func wait(seconds: float) -> void:
await get_tree().create_timer(seconds).timeout
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
var states = posses[X]
#for flag in flags:
#states.append(int(flag.target_state))
var flaggregate = "".join(states)
var sha = flaggregate.sha1_text().to_upper()
sha += flaggregate.md5_text().to_upper()
var chars = %FlagText.get_children()
for i in chars.size():
chars[i].target_x = hex_byte_to_int(sha.unicode_at(i * 2)) - 8
chars[i].target_y = hex_byte_to_int(sha.unicode_at((i * 2) + 1)) - 8
wait(1)
print (flaggregate)
if (X<1023):
X=X+1
var texture = get_viewport().get_texture()
var image = texture.get_image()
var screenshot_path = "<path_to_screenshots_folder>\\screenshot_" + str(screenshot_counter) + ".png"
image.save_png(screenshot_path)
screenshot_counter += 1
we added a function to generate every possible array of results and added a global variable X that would track the combinaitions, we also added screenshotting to every frame.
but now we have a problem, the letters don't just pop out to their detination, they would travel to that position.

for that we would need to edit the movable_char.gd and make it so it dosen't lerp to that destination
func _physics_process(delta):
var destination = origin + (Vector2(target_x, target_y) * scalar)
position=destination
an optional thing to do is to add a 30 fps maximum in arena.tscn to be able to see the flag when it pops out and get an approximate screenshot count for when the flag pops out.
func _ready():
flags = get_children().filter(func(child): return child.name.match("Flag_*"))
Engine.max_fps = 30
We can also reduce the amount of screenshots by seeing that the flag pops out at around the 500th screenshot.
if (X>500):
var texture = get_viewport().get_texture()
var image = texture.get_image()
var screenshot_path = "<path_to_screenshots_folder>\\screenshot_" + str(screenshot_counter) + ".png"
image.save_png(screenshot_path)
screenshot_counter += 1
we run the app now and get our flag :

Now that I think about it, I could've just read the letters and guessed the flag, it would've taken less time.
Last updated