miscfreemedium

Jumper

TJCTF 2026

Task: Godot 4.6 platformer game exported to WebAssembly with a hidden flag scene placed above the viewport, reachable only via an unbound 'mega_jump' action. Solution: Extract and decompile the .pck package with GDRE Tools, analyze the flag scene (f.tscn) containing 56 ColorRect nodes, render their coordinates to reveal the flag text.

$ ls tags/ techniques/
godot_pck_extractiongdscript_bytecode_decompilationtscn_scene_coordinate_renderinginput_action_mapping_analysis

Jumper — TJCTF 2026

Description

Sometimes, it takes a broken pair of legs to accomplish your goals. URL: https://jumper.tjc.tf/

A Godot 4.6 platformer game exported to WebAssembly. The player controls a character that can move left/right and jump. The goal is to find the hidden flag, which is placed above the normal viewport and unreachable through normal gameplay because the required "mega_jump" action has no key bound to it.

Analysis

1. Reconnaissance

The target URL serves a Godot 4.6 game via WebAssembly. The HTML page loads three key files:

  • jumper.pck — Godot package file (1.2 MB) containing all game assets
  • jumper.wasm — WebAssembly binary (Godot runtime)
  • jumper.js — JavaScript glue code

2. PCK Extraction

Downloaded jumper.pck and identified it as a Godot 4 pack (version 3 format) with GDPC magic bytes. The package contains Zstandard-compressed GDScript bytecode (GDSC format) with XOR-encoded (0xb6b6b6b6) UTF-32LE identifiers.

Initial manual parsing with a custom Python script (extract_pck.py) successfully listed all 23 files and extracted raw data, but the GDScript files were in compiled bytecode format. Used GDRE Tools (gdsdecomp) v2.5.0-beta.5 with --headless --recover CLI to fully decompile all GDScript source and scene files.

3. Code Analysis

player.gd — Standard platformer controller (CharacterBody2D):

const SPEED = 200 const JUMP_VELOCITY = 370 const GRAVITY = 10 func _physics_process(_delta): if Input.is_action_just_pressed("mega_jump") and is_on_floor(): velocity.y -= 500 # Very powerful jump if jump_input and can_jump: velocity.y = -JUMP_VELOCITY # Regular jump (-370)

Two jump types exist:

  • Regular jump (Space/Z): velocity.y = -370 — standard height
  • mega_jump: velocity.y -= 500 — much more powerful, only works on floor

project.godot — Input action mappings:

mega_jump={ "deadzone": 0.2, "events": [] # <-- NO KEY BOUND! } jump={ "deadzone": 0.2, "events": [Space, Z] }

The mega_jump action has an empty events array — no key is assigned to trigger it. This is the "broken pair of legs" from the challenge hint: the powerful jump needed to reach the flag is intentionally broken.

world.gd — Loads the flag scene:

func _ready(): var f = load("uid://oehyajwxl0hy").instantiate() f.position = Vector2(400, -120) # Above viewport (y-down coords) f.scale = Vector2(0.5, 0.5) add_child(f)

The flag scene (f.tscn) is placed at y = -120, which is above the normal viewport in Godot's y-down coordinate system. The regular jump cannot reach this height, and the mega_jump that could reach it has no key binding.

4. Flag Scene Analysis

f.tscn contains 56 ColorRect nodes (all dark red, Color(0.545, 0, 0, 1)) positioned to spell out text in blocky pixel-art letters. Key observations:

  • Coordinates range from x=-19 to x=2996, y=-53 to y=250
  • Some rectangles use rotation properties (for diagonal strokes in letters like "A")
  • Some use negative scale = Vector2(-0.92, 1) (for mirroring the closing } brace from {)

Solution

Rather than trying to patch the game to bind the mega_jump key, the flag can be extracted directly by rendering the ColorRect coordinates from f.tscn to an image.

#!/usr/bin/env python3 """Render f.tscn ColorRect nodes to reveal the hidden flag text""" from PIL import Image, ImageDraw import re import math # Parse f.tscn for ColorRect nodes scene_file = "recovered/f.tscn" with open(scene_file, 'r') as f: content = f.read() # Extract all ColorRect blocks blocks = content.split('[node name=') rects = [] for block in blocks: if 'ColorRect' not in block: continue left = float(re.search(r'offset_left\s*=\s*([-\d.]+)', block).group(1)) if re.search(r'offset_left', block) else 0 top = float(re.search(r'offset_top\s*=\s*([-\d.]+)', block).group(1)) if re.search(r'offset_top', block) else 0 right = float(re.search(r'offset_right\s*=\s*([-\d.]+)', block).group(1)) if re.search(r'offset_right', block) else 0 bottom = float(re.search(r'offset_bottom\s*=\s*([-\d.]+)', block).group(1)) if re.search(r'offset_bottom', block) else 0 rotation = 0 rot_match = re.search(r'rotation\s*=\s*([-\d.]+)', block) if rot_match: rotation = float(rot_match.group(1)) scale_x, scale_y = 1.0, 1.0 scale_match = re.search(r'scale\s*=\s*Vector2\(([-\d.]+),\s*([-\d.]+)\)', block) if scale_match: scale_x = float(scale_match.group(1)) scale_y = float(scale_match.group(2)) rects.append((left, top, right, bottom, rotation, scale_x, scale_y)) # Determine canvas bounds min_x = min(r[0] for r in rects) - 20 min_y = min(r[1] for r in rects) - 20 max_x = max(r[2] for r in rects) + 20 max_y = max(r[3] for r in rects) + 20 width = int(max_x - min_x) height = int(max_y - min_y) img = Image.new('RGB', (width, height), 'white') draw = ImageDraw.Draw(img) for left, top, right, bottom, rotation, sx, sy in rects: # Apply offset to canvas coordinates x1 = left - min_x y1 = top - min_y x2 = right - min_x y2 = bottom - min_y if rotation != 0: # For rotated rects, draw as polygon cx, cy = x1, y1 # Rotation origin is top-left in Godot w = x2 - x1 h = y2 - y1 corners = [(0, 0), (w, 0), (w, h), (0, h)] rotated = [] for px, py in corners: rx = px * math.cos(rotation) - py * math.sin(rotation) + cx ry = px * math.sin(rotation) + py * math.cos(rotation) + cy rotated.append((rx, ry)) draw.polygon(rotated, fill=(139, 0, 0)) else: draw.rectangle([x1, y1, x2, y2], fill=(139, 0, 0)) img.save('flag_render.png') print(f"Rendered {len(rects)} rectangles to flag_render.png") # Result: the image spells out tjctf{PAST_THE_WALL}

The rendered image clearly shows the text tjctf{PAST_THE_WALL} spelled out in dark red block letters.

$ cat /etc/motd

Liked this one?

Pro unlocks every writeup, every flag, and API access. $9/mo.

$ cat pricing.md

$ grep --similar

Similar writeups