← Back to Writeups
HTBMediumReversing

TunnelMadness

XESXOR9/10/20266 min read
#reversing#htb#medium#binary_maze_extraction#dfs_backtracking#dynamic_maze_solving#server_interaction#linux

TunnelMadness

Platform: HackTheBox | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-01-29 | Status: Solved Techniques: binary_maze_extraction, dfs_backtracking, dynamic_maze_solving, server_interaction

Summary

Task: Navigate a 3D maze in an ELF binary with remote server interaction. Solution: Used DFS with backtracking to dynamically explore the unknown server-side maze, as the binary contained different test data.

TunnelMadness — HackTheBox

Description

"Within Vault 8707 are located master keys used to access any vault in the country. Unfortunately, the entrance was caved in long ago. There are decades old rumors that the few survivors managed to tunnel out deep underground and make their way to safety. Can you uncover their tunnel and break back into the vault?"

The challenge provided:

  • A downloadable binary file
  • A remote server: nc 83.136.248.107 38062

Analysis

Initial Reconnaissance

Downloaded and extracted the challenge files. Found an ELF 64-bit binary called tunnel.

bash
$ file tunnel
tunnel: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked...

String Analysis

Found interesting strings that reveal the challenge mechanics:

bash
$ strings tunnel | grep -E "(Direction|Cannot|flag|vault)"
Direction (L/R/F/B/U/D/Q)?
Cannot move that way
/flag.txt
HTB{fake_flag_for_testing}
You break into the vault and read the secrets within...

Key findings:

  • Navigation prompt: "Direction (L/R/F/B/U/D/Q)?" — 6 directions for 3D movement plus Quit
  • Wall collision: "Cannot move that way" — indicates invalid moves
  • Flag path: "/flag.txt" — server reads flag from file
  • Test flag: "HTB{fake_flag_for_testing}" — embedded fake flag for local testing
  • Success message: "You break into the vault..." — indicates reaching the goal

Function Analysis

Using objdump to identify key functions:

bash
$ objdump -t tunnel | grep -E "(main|get_cell|prompt|flag)"

Key functions identified:

  • main — Main game loop
  • get_cell — Calculate cell position in 3D maze
  • prompt_and_update_pos — Handle movement input
  • get_flag — Read and print flag on success

Understanding the Maze Structure

From disassembly of get_cell:

c
// Pseudo-code reconstruction
struct Cell {
    int x, y, z;    // Coordinates
    int type;       // 0=start, 1=path, 2=wall, 3=goal
};

// Maze is 20x20x20 = 8000 cells
// Each cell is 16 bytes (4 ints)
Cell* get_cell(int x, int y, int z) {
    return &maze_data[z * 400 + y * 20 + x];
}

Maze parameters:

  • Dimensions: 20x20x20 (indices 0-19)
  • Cell size: 16 bytes (x, y, z, type)
  • Cell types: 0=start, 1=path, 2=wall, 3=goal
  • Start position: (0, 0, 0)
  • Goal position: (19, 19, 19)

Direction mapping:

DirectionDelta (x, y, z)
B (Back)(-1, 0, 0)
F (Forward)(+1, 0, 0)
L (Left)(0, -1, 0)
R (Right)(0, +1, 0)
D (Down)(0, 0, -1)
U (Up)(0, 0, +1)

Main Loop Logic

The main loop:

  1. Prompts for direction input
  2. Calculates new position based on direction
  3. Checks if new cell is valid (not wall, within bounds)
  4. If cell type == 3 (goal), calls get_flag() and exits
  5. Otherwise, updates position and continues

Solution

First Attempt: Static Maze Extraction (Failed)

Initially tried to extract the maze from the binary and solve it offline:

python
#!/usr/bin/env python3
"""
Attempt 1: Extract maze from binary and solve with BFS
"""
from collections import deque

# Maze data found at offset 0x20e0 in binary
# Extracted all cells and built adjacency graph
# BFS found path from (0,0,0) to (19,19,19)

path = "UUUFRUFUFFRFFRRUURUFFURURRFRURUUUURRFFUUURUFRDRRURRFFFFFRFF"

Problem: When sending this path to the server, got "Cannot move that way" errors. The server has a DIFFERENT maze than the binary!

The binary contains a fake/test maze for local development. The actual challenge maze is generated server-side.

Second Attempt: Dynamic Maze Solving (Success)

Since the maze is unknown, implemented DFS with backtracking to explore dynamically:

python
#!/usr/bin/env python3
"""
TunnelMadness - Dynamic 3D Maze Solver
Uses DFS with backtracking to explore unknown maze
"""
from pwn import *

context.log_level = 'error'

# Direction definitions
directions = ['F', 'R', 'U', 'B', 'L', 'D']
reverse_dir = {'B': 'F', 'F': 'B', 'L': 'R', 'R': 'L', 'D': 'U', 'U': 'D'}
dir_delta = {
    'B': (-1, 0, 0), 'F': (1, 0, 0),
    'L': (0, -1, 0), 'R': (0, 1, 0),
    'D': (0, 0, -1), 'U': (0, 0, 1)
}
DIM = 20  # Maze dimension

def solve_maze():
    r = remote('83.136.248.107', 38062)
    
    visited = set()
    walls = set()
    current_pos = (0, 0, 0)
    visited.add(current_pos)
    path = []  # Stack of moves for backtracking
    
    def try_move(direction):
        """Attempt to move in given direction, return result"""
        r.recvuntil(b'?')
        r.sendline(direction.encode())
        line = r.recvline()
        
        # Check for success (reached goal)
        if b'break into' in line.lower() or b'secrets' in line.lower():
            print(f"[+] SUCCESS!")
            print(f"FLAG: {line.decode()}")
            rest = r.recvall(timeout=5)
            print(f"{rest.decode()}")
            return 'flag'
        
        # Check for wall collision
        if b'Cannot move' in line:
            return 'wall'
        
        return 'ok'
    
    move_count = 0
    while True:
        moved = False
        
        # Try each direction (DFS exploration)
        for direction in directions:
            dx, dy, dz = dir_delta[direction]
            new_pos = (current_pos[0]+dx, current_pos[1]+dy, current_pos[2]+dz)
            
            # Skip if out of bounds
            if not all(0 <= c < DIM for c in new_pos):
                continue
            
            # Skip if already visited or known wall
            if new_pos in visited or new_pos in walls:
                continue
            
            # Try the move
            result = try_move(direction)
            move_count += 1
            
            if result == 'flag':
                print(f"[+] Solved in {move_count} moves")
                print(f"[+] Path length: {len(path) + 1}")
                return
            
            if result == 'ok':
                # Move succeeded
                visited.add(new_pos)
                current_pos = new_pos
                path.append(direction)
                moved = True
                break
            else:
                # Hit a wall
                walls.add(new_pos)
        
        # If no valid move found, backtrack
        if not moved:
            if not path:
                print("[-] No solution found!")
                break
            
            # Backtrack: reverse last move
            last_move = path.pop()
            back_dir = reverse_dir[last_move]
            
            result = try_move(back_dir)
            move_count += 1
            
            if result == 'flag':
                print(f"[+] Solved in {move_count} moves")
                return
            
            # Update position
            dx, dy, dz = dir_delta[back_dir]
            current_pos = (current_pos[0]+dx, current_pos[1]+dy, current_pos[2]+dz)

if __name__ == '__main__':
    solve_maze()

Execution

bash
$ python3 solve.py
[+] SUCCESS!
FLAG: You break into the vault and read the secrets within...
HTB{REDACTED}
[+] Solved in 118 moves

The DFS algorithm explored the 3D maze, backtracking when hitting dead ends, until reaching the goal cell at (19, 19, 19).

Notes

Why Static Analysis Failed

Many CTF challenges with a remote component contain test data in the binary for local debugging. The actual data is generated or stored on the server. Always verify whether local and server data match!

DFS vs BFS Algorithm

  • BFS (Breadth-First Search) — finds the shortest path but requires knowledge of the entire graph in advance
  • DFS (Depth-First Search) with backtracking — works with an unknown graph, explores as it progresses

For dynamic maze exploration, DFS is ideal because:

  1. Does not require knowledge of the entire structure
  2. Backtracking allows returning from dead ends
  3. Memory O(depth) instead of O(all nodes)

3D Maze Navigation

6 directions in 3D:

  • Horizontal plane: Forward/Back (X), Left/Right (Y)
  • Vertical axis: Up/Down (Z)

This is a classic structure for 3D mazes, commonly found in CTF and game challenges.