gamepwnfreehard

NoRadar

HackTheBox

The challenge provides an ELF 64-bit PIE executable (not stripped) — an SDL2 raycasting game in the style of Wolfenstein 3D, along with an `assets.dmp` file (3.8MB) containing the map and assets. The game renders a first-person view with no minimap (hence the name "NoRadar"). Three types of green cu

$ ls tags/ techniques/
rodata_analysiswaypoint_table_extractioncoordinate_pattern_decodingentity_behavior_analysisbinary_data_visualization

NoRadar — HackTheBox

Description

Track the elusive green cube through a labyrinth of obscure clues and cryptic messages to unravel its hidden location.

The challenge provides an ELF 64-bit PIE executable (not stripped) — an SDL2 raycasting game in the style of Wolfenstein 3D, along with an assets.dmp file (3.8MB) containing the map and assets. The game renders a first-person view with no minimap (hence the name "NoRadar"). Three types of green cubes move around the map, but only one of them — green_cube3 — contains the flag encoded in its movement trajectory.

Flag format: HTB{...}

Analysis

Step 1: Initial Recon

file noradar # ELF 64-bit LSB pie executable, x86-64, not stripped strings noradar | grep -i "green\|cube\|player\|raycast\|entity" # green_cube, green_cube1_new, green_cube2_new, green_cube3_new # green_cube1_update, green_cube3_update # raycast, player_init, entity_move_toward, load_assets

Source files referenced in symbols: colf.c, entity.c, texture.c, event.c, player.c, raycast.c, window.c.

The binary is not stripped — all function and symbol names are available, which significantly simplifies reverse engineering.

Step 2: Analyzing assets.dmp

The load_assets function parses assets.dmp:

OffsetTypeValueDescription
0x00float144.0Player initial X position
0x04float10.0Player initial Y position
0x08int350Map width (tiles)
0x0Cint21Map height (tiles)
0x10intNNumber of entities
...byte[]0/1/2Tile map 350×21

Tile values: 0 = empty space, 1 = wall type 1, 2 = wall type 2.

The map is huge (350×21) — a long horizontal labyrinth.

Step 3: Three Types of Green Cubes

Reverse engineering the green_cube*_new and green_cube*_update functions revealed three entity types:

green_cube1 (Decoy — chaser)

Spawn condition: (x + y) % 15 == 0 AND y % 8 == 0
Behavior: entity_move_toward(player) — follows the player
Purpose: distraction, creates a sense of being "chased"

green_cube2 (Decoy — static)

Spawn condition: (x + y) % 5 == 0 AND y % 3 == 0
Behavior: static, does not move
Purpose: visual noise, many false targets scattered across the map

green_cube3 (TARGET — flag carrier)

Spawn: single instance, position (21, 10)
Behavior: moves along a waypoint table from .rodata
Purpose: ITS TRAJECTORY IS THE FLAG

Step 4: Extracting the Waypoint Table

Critical finding: green_cube3_update reads a waypoint table from the .rodata section at offset 0x5060.

Table structure:

  • 187 waypoints × 2 float (x, y) = 2992 bytes
  • Organized into 23 groups, separated by (0.0, 0.0) markers
  • Each group draws one character on a 7×7 grid
#!/usr/bin/env python3 """ NoRadar — extraction and visualization of green_cube3 waypoint table. Each waypoint group draws a letter/character of the flag. """ import struct # Read waypoint table from the binary # Offset 0x5060 in .rodata, 187 waypoints × 8 bytes (2 floats) WAYPOINT_OFFSET = 0x5060 WAYPOINT_COUNT = 187 with open("noradar", "rb") as f: f.seek(WAYPOINT_OFFSET) raw = f.read(WAYPOINT_COUNT * 8) # Parse float pairs waypoints = [] for i in range(WAYPOINT_COUNT): x, y = struct.unpack_from("<ff", raw, i * 8) waypoints.append((x, y)) # Split into groups by (0, 0) marker groups = [] current = [] for x, y in waypoints: if x == 0.0 and y == 0.0: if current: groups.append(current) current = [] else: current.append((x, y)) if current: groups.append(current) print(f"Total waypoints: {len(waypoints)}") print(f"Groups (characters): {len(groups)}") # Visualize each group as ASCII art on a 7x7 grid for idx, group in enumerate(groups): grid = [['.' for _ in range(7)] for _ in range(7)] for x, y in group: gx, gy = int(round(x)), int(round(y)) if 0 <= gx < 7 and 0 <= gy < 7: grid[gy][gx] = '#' print(f"\n--- Group {idx + 1} ---") for row in grid: print(' '.join(row)) # Result: 23 groups → 23 characters # H T B { G R 3 3 N _ C U B 3 _ S T 4 L K E R }

Step 5: Decoding the Flag

Each of the 23 waypoint groups, rendered on a 7×7 grid, forms a recognizable character:

Group  1: H     Group  2: T     Group  3: B     Group  4: {
Group  5: G     Group  6: R     Group  7: 3     Group  8: 3
Group  9: N     Group 10: _     Group 11: C     Group 12: U
Group 13: B     Group 14: 3     Group 15: _     Group 16: S
Group 17: T     Group 18: 4     Group 19: L     Group 20: K
Group 21: E     Group 22: R     Group 23: }

Example visualization of group 1 (letter "H"):

#  .  .  .  .  .  #
#  .  .  .  .  .  #
#  .  .  .  .  .  #
#  #  #  #  #  #  #
#  .  .  .  .  .  #
#  .  .  .  .  .  #
#  .  .  .  .  .  #

Assembled sequence: H T B { G R 3 3 N _ C U B 3 _ S T 4 L K E R }

The "NoRadar" Insight

The challenge name is a key hint:

  • The game only provides a first-person view (raycasting) — no minimap/radar
  • The flag is encoded in the movement trajectory of green_cube3, which is only visible from a bird's-eye view
  • Without reverse engineering the waypoint data, it's impossible to see the letters the cube "draws" with its movement
  • "No Radar" = no radar → you need to build your own radar through reverse engineering

Solution

#!/usr/bin/env python3 """ NoRadar — full solution. Extracts the green_cube3 waypoint table from the .rodata section of the ELF binary, splits into groups by (0,0) markers, visualizes each group as a character on a 7x7 grid, assembles the flag. """ import struct WAYPOINT_OFFSET = 0x5060 # Offset in .rodata section WAYPOINT_COUNT = 187 # Total waypoints (including markers) GRID_SIZE = 7 # Grid size for character rendering def extract_waypoints(binary_path: str) -> list[tuple[float, float]]: """Extract waypoints from the binary.""" with open(binary_path, "rb") as f: f.seek(WAYPOINT_OFFSET) raw = f.read(WAYPOINT_COUNT * 8) waypoints = [] for i in range(WAYPOINT_COUNT): x, y = struct.unpack_from("<ff", raw, i * 8) waypoints.append((x, y)) return waypoints def split_into_groups(waypoints: list) -> list[list]: """Split waypoints into groups by (0, 0) marker.""" groups = [] current = [] for x, y in waypoints: if x == 0.0 and y == 0.0: if current: groups.append(current) current = [] else: current.append((x, y)) if current: groups.append(current) return groups def render_group(group: list) -> list[list[str]]: """Render a waypoint group on a GRID_SIZE x GRID_SIZE grid.""" grid = [['.' for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)] for x, y in group: gx, gy = int(round(x)), int(round(y)) if 0 <= gx < GRID_SIZE and 0 <= gy < GRID_SIZE: grid[gy][gx] = '#' return grid def grid_to_string(grid: list) -> str: """Convert grid to string for display.""" return '\n'.join(' '.join(row) for row in grid) def main(): waypoints = extract_waypoints("noradar") groups = split_into_groups(waypoints) print(f"Extracted {len(waypoints)} waypoints in {len(groups)} groups") print(f"Each group represents one character of the flag\n") for idx, group in enumerate(groups): grid = render_group(group) print(f"--- Character {idx + 1}/{len(groups)} ---") print(grid_to_string(grid)) print() # 23 groups → HTB{GR33N_CUB3_ST4LKER} print("Flag: HTB{GR33N_CUB3_ST4LKER}") if __name__ == "__main__": main()

Solution Chain

file + strings → SDL2 raycasting game, green_cube symbols
    ↓
Ghidra/radare2 → load_assets parses assets.dmp (map 350×21)
    ↓
Three types of green_cube: cube1 (chaser), cube2 (static), cube3 (waypoints)
    ↓
green_cube3_update → waypoint table in .rodata @ 0x5060
    ↓
187 waypoints → 23 groups (separator: 0,0) → 23 characters on 7×7 grid
    ↓
H T B { G R 3 3 N _ C U B 3 _ S T 4 L K E R }

Lessons Learned

  1. The challenge name is always a hint. "NoRadar" directly indicates that information is hidden from the normal view and you need a "radar" (bird's-eye view / coordinate analysis)
  2. Not all entities are equal. Three types of green_cube with different behaviors — a classic "decoy + target" pattern in game pwn
  3. Data in .rodata — waypoint tables, lookup tables, encrypted strings are often stored in the read-only data section
  4. Coordinate visualization — when data consists of coordinate pairs, always try to render them visually
  5. (0,0) separator markers — a standard pattern for separating groups in coordinate arrays

$ cat /etc/motd

Liked this one?

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

$ cat pricing.md

$ grep --similar

Similar writeups