Zuma
Zuma
Platform: duckerz | Category: Misc | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-02-09 | Status: Solved Techniques: binary_reversing, data_visualization, file_analysis
Summary
Task: Find hidden flag in modified Zuma Deluxe game files. Solution: Reverse-engineered CURV path format and visualized abnormally large claw.dat file to reveal flag drawn by ball trajectory.
Zuma — Duckerz CTF
Description
A Misc/GamePwn challenge. We are provided with Zuma Deluxe game files, but some of them have been modified. We need to find the hidden flag.
Analysis
1. File Investigation
When comparing the provided files with the original Zuma Deluxe game, several changes were noticed:
levels/levels.xmlcontains modified level parameters, including strangeimage-topattributes.- In the
levels/claw/directory, theclaw.datfile is about 196 KB, while normal path files (.dat) in the game are around 12 KB.
This is a clear indicator that claw.dat contains hidden additional data.
2. Reverse Engineering
Analysis of Zuma.exe (which retained debug symbols) led to the _Level_Load function. This function is responsible for loading paths from .dat files. The path file format in Zuma is called CURV.
CURV structure:
- Header: Magic number and general information.
- Waypoints: Section with reference points (10 bytes per point).
- Points: Section with detailed path points encoded as deltas (4 bytes per point).
Each point in the Points section is described by 4 bytes:
flags(1 byte)flags(1 byte)dx(signed short / 2 bytes) — delta on X axis.dy(signed short / 2 bytes) — delta on Y axis.
In the game code, dx and dy are divided by 100.0 to get the offset in pixels.
Solution
To extract the flag, a Python script was written that parses claw.dat, reconstructs the coordinates of all points, and renders them on a canvas.
python read-only • copy allowedimport struct from PIL import Image, ImageDraw def parse_curv(filename): with open(filename, 'rb') as f: data = f.read() # Skip header and waypoints section # Zuma CURV files usually start with 'CURV' # We look for the start of the points section. # Based on reversing, points come after a certain offset. # For simplicity, we can find the pattern or use the offset from the header. # In this case, we know that points are 4-byte structures (short dx, short dy) # after the header. points = [] curr_x, curr_y = 0, 0 # Offset 0x20 - approximate offset of data start in Zuma .dat # The exact offset can be obtained from the header, but for CTF it's often enough # to start where the data looks like deltas. for i in range(0x20, len(data), 4): if i + 4 > len(data): break # Format: 2 bytes flags (sometimes), 2 bytes dx, 2 bytes dy # But in Zuma dx and dy often come as signed shorts dx, dy = struct.unpack('<hh', data[i:i+4]) curr_x += dx / 100.0 curr_y += dy / 100.0 points.append((curr_x, curr_y)) return points def render_points(points, output_file): if not points: return min_x = min(p[0] for p in points) max_x = max(p[0] for p in points) min_y = min(p[1] for p in points) max_y = max(p[1] for p in points) width = int(max_x - min_x) + 100 height = int(max_y - min_y) + 100 img = Image.new('RGB', (width, height), 'white') draw = ImageDraw.Draw(img) offset_points = [(p[0] - min_x + 50, p[1] - min_y + 50) for p in points] # Draw the path draw.point(offset_points, fill='black') img.save(output_file) print(f"Saved to {output_file}") points = parse_curv('claw.dat') render_points(points, 'flag.png')
When running the script, an image is generated where the ball trajectory draws out the flag text.