$ cat writeup.md…
$ cat writeup.md…
HackTheBox
The challenge provides an ELF 64-bit PIE executable (not stripped) — an SDL2 raycasting game in Wolfenstein 3D style, along with an `assets.dmp` file (4.89 MB) containing the map, textures, and level data. The game renders a first-person spiral maze. The flag is encoded in textures of special walls
"The last resident seems to have found a way out of here."
The challenge provides an ELF 64-bit PIE executable (not stripped) — an SDL2 raycasting game in Wolfenstein 3D style, along with an assets.dmp file (4.89 MB) containing the map, textures, and level data. The game renders a first-person spiral maze. The flag is encoded in textures of special walls placed along the spiral path. Floor arrows indicate the traversal direction.
Flag format: HTB{...}
file noclip # ELF 64-bit LSB pie executable, x86-64, not stripped, dynamically linked strings noclip | grep -i "player\|raycast\|colf\|window\|event\|asset" # game, input, player_init, raycast, load_assets # colf.c, window.c, event.c, player.c, raycast.c
The binary is not stripped — all function names are available. Key modules:
colf.c — collision detection (wall collisions)player.c — player controlraycast.c — world rendering (raycasting)event.c — input handlingwindow.c — SDL2 windowThe name "NoClip" is a direct reference to the game cheat that allows walking through walls. This is a hint: to see all flag characters, you need to "walk through the walls" of the spiral maze.
The load_assets function parses assets.dmp in a chunk-based format:
Chunk 1 (id=1): Player position
| Offset | Type | Value | Description |
|---|---|---|---|
| 0x00 | uint32 | 1 | Chunk ID |
| 0x04 | double | 21.0 | Initial player X position |
| 0x0C | double | 10.0 | Initial player Y position |
Chunk 2 (id=2): Map data
| Offset | Type | Value | Description |
|---|---|---|---|
| 0x14 | uint32 | 2 | Chunk ID |
| 0x18 | uint32 | 43 | Map width |
| 0x1C | uint32 | 21 | Map height |
| 0x20 | byte[] | 43×21×3 | Map data (2709 bytes) |
Each map cell is 3 bytes (column-major order: map[x * height + y]):
0=empty, 1=standard wall, 2+=special textured wall1=wall, 2=floor, 3=arrow up, 4=arrow right, 5=arrow left, 6=arrow downChunk 3 (id=3): Skybox texture
| Field | Type | Value |
|---|---|---|
| Chunk ID | uint32 | 3 |
| Name | char[64] | skybox name |
| Width | uint32 | 1024 |
| Height | uint32 | 906 |
| Pixels | RGBA | 1024×906×4 bytes |
Chunk 4 (id=4): Wall/floor textures
| Field | Type | Description |
|---|---|---|
| Chunk ID | uint32 | 4 |
| Count | uint32 | Number of textures |
| Per texture: | ||
| — Name | char[64] | Texture name |
| — Width | uint32 | 128 |
| — Height | uint32 | 128 |
| — Pixels | RGBA | 128×128×4 bytes |
Textures from Chunk 4 (18 total):
| Index | Name | Purpose |
|---|---|---|
| 0 | htb | HTB logo texture |
| 1 | floor | Floor |
| 2 | up | Arrow up (↑) |
| 3 | right | Arrow right (→) |
| 4 | left | Arrow left (←) |
| 5 | down | Arrow down (↓) |
| 6 | 1 | Character '1' |
| 7 | 3 | Character '3' |
| 8 | 4 | Character '4' |
| 9 | B | Character 'B' |
| 10 | H | Character 'H' |
| 11 | T | Character 'T' |
| 12 | W | Character 'W' |
| 13 | _ | Character '_' |
| 14 | k | Character 'k' |
| 15 | r | Character 'r' |
| 16 | { | Character '{' |
| 17 | } | Character '}' |
The wall type in the map (byte 0) references a texture by index: tex_names[wall_type - 1]. Values 7–18 correspond to flag characters.
The 43×21 map is a spiral maze. The player starts at the center (21, 10). Floor arrows (floor texture indices 3–6) guide the player along the spiral from center outward.
Simplified maze visualization:
####################..#...#...#...#...##
...#...#...#...#...##...#...#...#...##.
..#...#...#...#...##...#...#...#...####
###################...#...#...#...##...
#...#...#...#...##...#...#...#...##...#
...#...#...#...##...#...#...#...#######
################...#...#...#...##...#..
.#...#...#...##..{...B...r...#...##...#
#...#...#...##...#...#...#...#..##B####
#####3#######...#...#...#...#...##...#.
..#...#...##..T...H...#...#...##...#..
.#...#...##...#...#...#...#...#########
##4#######...#...#...#...#...##...#...#
...#...##..T..._...k...#...##...#...#.
..#...##...#...#...#...#...##H#########
#######...#...#...#...#...##...#...#...
#...##...#...4...1...1...##...#...#...#
...##...#...#...#...#...##3##W#########
}###...#...#...#...#...##...#...#...#..
.##.._...#...#...#...##...#...#...#...
##...#...#...#...######################
19 special walls are placed along the spiral path. Each displays a texture with a single flag character.
Traversal algorithm:
(21,10) ^ -> wall (21, 8) = '{'
(21, 6) > -> wall (24, 6) = 'B'
(27, 6) > -> wall (30, 6) = 'r'
(33, 6) ^ -> wall (33, 4) = '3'
(33, 2) < -> wall (30, 2) = 'T'
(27, 2) < -> wall (24, 2) = 'H'
(21, 2) v -> wall (21, 4) = '4'
(21, 6) ... [continues through spiral]
Full character sequence when traversing the spiral:
H → T → B → { → B → r → 3 → 4 → k → _ → T → H → 3 → _ → W → 4 → 1 → 1 → }
#!/usr/bin/env python3 """ HTB Challenge: NoClip (GamePwn) Solution: Extract flag from SDL2 raycasting game binary + assets The game is a Wolfenstein-style raycaster with a spiral maze. Flag characters are encoded as wall textures on special walls. Floor arrow textures guide the player through the spiral path. Following the arrows and reading wall textures in order reveals the flag. The "NoClip" name refers to bypassing collision detection to walk through walls and see all flag characters - but we can extract it statically. """ import struct def solve(): with open("assets.dmp", "rb") as f: # === Parse chunk-based asset file === # Chunk 1: Player position chunk_id = struct.unpack("<I", f.read(4))[0] assert chunk_id == 1 player_x, player_y = struct.unpack("<dd", f.read(16)) print(f"Player start: ({player_x}, {player_y})") # Chunk 2: Map data chunk_id = struct.unpack("<I", f.read(4))[0] assert chunk_id == 2 map_width = struct.unpack("<I", f.read(4))[0] map_height = struct.unpack("<I", f.read(4))[0] print(f"Map size: {map_width}x{map_height}") # Map is stored column-major: map[x * height + y] with 3 bytes per cell # Byte 0: wall type (0=empty, 1=standard wall, 2+=special textured wall) # Byte 1: side texture index (unused in this map) # Byte 2: floor texture index (1=wall, 2=floor, 3=up, 4=right, 5=left, 6=down) map_data = f.read(map_width * map_height * 3) # Skip to chunk 4 to get texture names # Chunk 3: Skybox texture chunk_id = struct.unpack("<I", f.read(4))[0] assert chunk_id == 3 f.read(64) # name tw, th = struct.unpack("<II", f.read(8)) f.seek(tw * th * 4, 1) # skip pixels # Chunk 4: Wall/floor textures array chunk_id = struct.unpack("<I", f.read(4))[0] assert chunk_id == 4 num_textures = struct.unpack("<I", f.read(4))[0] tex_names = [] for i in range(num_textures): name = f.read(64).rstrip(b"\x00").decode() tw, th = struct.unpack("<II", f.read(8)) f.seek(tw * th * 4, 1) tex_names.append(name) print(f"Textures ({num_textures}): {tex_names}") # === Extract arrow directions and special walls === arrow_map = {3: "^", 4: ">", 5: "<", 6: "v"} arrows = {} special_walls = {} for x in range(map_width): for y in range(map_height): idx = (x * map_height + y) * 3 wall_type = map_data[idx] floor_tex = map_data[idx + 2] if wall_type == 0 and floor_tex in arrow_map: arrows[(x, y)] = arrow_map[floor_tex] if wall_type > 1: special_walls[(x, y)] = tex_names[wall_type - 1] print(f"\nArrow tiles: {len(arrows)}") print(f"Special walls: {len(special_walls)}") for pos, name in sorted(special_walls.items()): print(f" ({pos[0]:2d},{pos[1]:2d}): '{name}'") # === Trace spiral path following floor arrows === # Direction deltas: arrow -> (wall_offset, next_room_offset) # ^ = up (y decreases): wall at y-2, next room at y-4 # v = down (y increases): wall at y+2, next room at y+4 # > = right (x increases): wall at x+3, next room at x+6 # < = left (x decreases): wall at x-3, next room at x-6 flag_chars = [] x, y = int(player_x), int(player_y) # 21, 10 visited = set() print(f"\n=== Tracing spiral path from ({x},{y}) ===") while (x, y) in arrows and (x, y) not in visited: visited.add((x, y)) direction = arrows[(x, y)] if direction == "^": wall_pos = (x, y - 2) next_pos = (x, y - 4) elif direction == "v": wall_pos = (x, y + 2) next_pos = (x, y + 4) elif direction == ">": wall_pos = (x + 3, y) next_pos = (x + 6, y) elif direction == "<": wall_pos = (x - 3, y) next_pos = (x - 6, y) wall_char = special_walls.get(wall_pos, "") if wall_char: flag_chars.append(wall_char) print(f" ({x:2d},{y:2d}) {direction} -> wall {wall_pos} = '{wall_char}'") x, y = next_pos flag = "".join(flag_chars) print(f"\n{'=' * 50}") print(f"FLAG: {flag}") print(f"{'=' * 50}") return flag if __name__ == "__main__": solve()
green_cube3 entity via a waypoint table in .rodata$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar