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/
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:
| Offset | Type | Value | Description |
|---|---|---|---|
| 0x00 | float | 144.0 | Player initial X position |
| 0x04 | float | 10.0 | Player initial Y position |
| 0x08 | int | 350 | Map width (tiles) |
| 0x0C | int | 21 | Map height (tiles) |
| 0x10 | int | N | Number of entities |
| ... | byte[] | 0/1/2 | Tile 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
- 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)
- Not all entities are equal. Three types of green_cube with different behaviors — a classic "decoy + target" pattern in game pwn
- Data in .rodata — waypoint tables, lookup tables, encrypted strings are often stored in the read-only data section
- Coordinate visualization — when data consists of coordinate pairs, always try to render them visually
(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
- [gamepwn][free]NoMap3D— HackTheBox
- [gamepwn][free]NoClip— HackTheBox
- [gamepwn][free]CubeMadness1— hackthebox
- [reverse][free]TunnelMadness— hackthebox
- [gamepwn][free]StayInTheBoxCorp— HackTheBox