HTB: NoRadar Challenge

NoRadar - HackTheBox Challenge Writeup

Challenge Information

FieldValue
NameNoRadar
CategoryMisc
DifficultyEasy
Authord3vn0mi

Description

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

The challenge ships a small game binary (noradar) plus an asset dump (assets.dmp). The premise is a maze full of decoy “green cubes” — the flag is hidden in how the real cube moves, not in anything rendered on screen.

Solution

The staged challenge archive initially looked broken — the extracted noradar binary was 0 bytes. Re-extracting straight from the source zip with the correct password recovered the real 36 KB ELF and a 3.8 MB assets.dmp file.

From there the approach was pure static reverse engineering: no need to actually run the game or navigate the maze.

  1. Reverse-engineered the asset format. assets.dmp uses a simple TLV (tag-length-value) layout: tag 1 is the player’s starting position (two doubles), tag 2 is a 350×21 tile map (3 bytes/cell), tag 3 is a single texture, and tag 4 is a texture table (count + N × [64-byte name, u32 width, u32 height, RGBA pixels]). Extracting and rendering every texture (skybox, htb, floor, green_cube) confirmed none of them carried the flag — they’re just cosmetic, and the maze pattern itself is a repeating tile, not a message.

  2. Found which cube actually mattered. Disassembling main showed the game spawns large swarms of decoy cubes:

    • green_cube1_new at every cell where (x+y) % 15 == 0 && y % 8 == 0
    • green_cube2_new at every cell where (x+y) % 5 == 0 && y % 3 == 0

    That’s the “no radar” trick — dozens of green cubes scattered across the map to bury the real one. But main calls green_cube3_new exactly once, and only that instance is wired to a per-frame green_cube3_update patrol routine.

  3. Extracted the patrol waypoints. green_cube3_update walks a hardcoded array of 374 doubles (187 x/y pairs) stored in .rodata at offset 0x5060. Dumping and pairing those doubles gave the full patrol path the cube walks across the map.

  4. Rendered the path as glyphs. A waypoint of exactly (0, 0) acts as a pen-lift / letter-break marker: each time it appears, a static offset_x accumulator increases by 14. Plotting every waypoint at (x + offset_x, y) onto the 350×21 map — i.e. treating the cube’s route as a polyline draw call — produces 23 distinct stroke glyphs laid out left to right. Read together, those glyphs spell the flag. The “maze” was never meant to be solved by walking it; the cube’s movement itself is the message.

  5. Verified the flag via HTB’s submission API against challenge ID 452, which returned {'message': 'Congratulations!'}.

Key Steps

Recover the real artifacts (fix the silently-failed extraction):

Terminal window
# The staged binary was 0 bytes; the source zip had the real sizes.
unzip -P hackthebox a12c736e-853d-4a25-a233-f1106ea2f90b.zip
# -> noradar (36KB ELF), assets.dmp (3.8MB)

Parse the TLV asset dump to rule out texture-based flags:

import struct
d = open('assets.dmp', 'rb').read()
off = 0
while off < len(d):
tag = struct.unpack_from('<I', d, off)[0]
# tag 1 = player pos (2 doubles)
# tag 2 = 350x21 map, 3 bytes/cell
# tag 3 = single texture
# tag 4 = count + N textures: 64B name, u32 w, u32 h, RGBA pixels
... # extracted & rendered every texture -> no flag present

Find the one “real” cube among the decoy swarm:

# From objdump -d --no-show-raw-insn -M intel noradar:
#
# main:
# for each maze cell (x, y):
# if (x+y) % 15 == 0 and y % 8 == 0: green_cube1_new(x, y) # decoy swarm
# if (x+y) % 5 == 0 and y % 3 == 0: green_cube2_new(x, y) # decoy swarm
# green_cube3_new(...) <-- called exactly once
# register_update(green_cube3_update) <-- only this one patrols

Extract and rasterize the patrol path from .rodata:

import struct
d = open('noradar', 'rb').read()
base = 0x5060
n = 374 # 187 (x, y) waypoint pairs
vals = struct.unpack_from(f'<{n}d', d, base)
points = list(zip(vals[0::2], vals[1::2]))
offset_x = 0
glyph_map = {} # (x, y) -> stroked pixel
for x, y in points:
if (x, y) == (0.0, 0.0):
offset_x += 14 # pen-lift: start next letter
continue
glyph_map[(int(x) + offset_x, int(y))] = True
# Rendering glyph_map onto the 350x21 grid draws 23 letterforms:
# the flag, spelled out by the cube's own movement.

Verify against the HTB API:

import sys
sys.path.insert(0, '/app/lib')
from htb_api import HTBClient
c = HTBClient()
print(c.submit_challenge(452, "HTB{REDACTED}"))
# -> {'message': 'Congratulations!'}

Tools Used

  • objdump (disassembly of main, green_cube3_new, green_cube3_update)
  • nm (symbol enumeration)
  • strings
  • unzip (password-protected source archive)
  • Python 3 (struct for binary parsing, PIL for texture rendering/verification)
  • HTB submission API (htb_api.HTBClient)

Key Learnings

  • Not every visual element in a challenge is the puzzle. The maze, the textures, and dozens of decoy cubes were all red herrings by design — “NoRadar” refers to burying the signal in noise, not to any in-game mechanic.
  • Static analysis beats dynamic play when the “gameplay” is scripted. The cube’s path was a hardcoded waypoint table, not something reactive to player input, so there was no need to run the binary or solve the maze at all — just find and decode the table.
  • Watch for sentinel values in coordinate data. The (0, 0) waypoint wasn’t a real position — recognizing it as a pen-lift/letter-break marker was the key that turned a jumble of points into readable glyphs.
  • TLV/asset dump formats are worth reverse-engineering fully, even when a lead turns out to be a dead end (the textures here), because ruling out the “obvious” location narrows down where the real answer must be.
  • Verify infrastructure failures before assuming a challenge is broken. A 0-byte staged binary looked like a broken challenge but was just a failed extraction step — re-extracting from source with the known password resolved it immediately.