> For the complete documentation index, see [llms.txt](https://tazarkour.gitbook.io/blog/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tazarkour.gitbook.io/blog/writeups/block-ctf-or-red-flags-rev-godot-game-hacking.md).

# Block CTF | Red Flags (Rev, Godot Game Hacking)

<figure><img src="/files/Omgb2MgrCV9oGzJg2SUb" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/WSpf5vCndbb9XkHhDT2w" alt=""><figcaption></figcaption></figure>

We can decompile the game easily with [**gdsdecomp**](https://github.com/bruvzg/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 :&#x20;

arena.tscn :&#x20;

```gdscript
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 :&#x20;

```gdscript
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 :&#x20;

```gdscript
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.

```gdscript
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.

<figure><img src="/files/JBsqP8TNWt4iGtaBCI0v" alt=""><figcaption></figcaption></figure>

for that we would need to edit the movable\_char.gd and make it so it dosen't lerp to that destination

```gdscript
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.

```gdscript
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.

```gdscript
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 :&#x20;

<figure><img src="/files/Rgm3qg8hKCLlA0BvaUN3" alt=""><figcaption></figcaption></figure>

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