← Back to Writeups
CUSTOMEasyMisc

Zuma

XESXOR9/2/20263 min read
#misc#custom#easy#binary_reversing#data_visualization#file_analysis

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.xml contains modified level parameters, including strange image-top attributes.
  • In the levels/claw/ directory, the claw.dat file 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:

  1. Header: Magic number and general information.
  2. Waypoints: Section with reference points (10 bytes per point).
  3. 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
import 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. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍