← Back to Writeups
CUSTOMMediumMisc

Неопознанный объект

XESXOR9/10/20264 min read
#misc#custom#medium#custom-script-for-texture-extr#file-format-identification-via#visual-inspection-of-extracted#wad3-texture-format-parsing#linux

Неопознанный объект

Platform: hackerlab | Category: Misc | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2025-12-26 | Status: Solved Techniques: Custom script for texture extraction, File format identification via magic bytes, Visual inspection of extracted images, WAD3 texture format parsing

Summary

Task: Unknown file found on desktop. Solution: Identified WAD3 format (Half-Life/Counter-Strike textures) via magic bytes, wrote Python script to extract textures, found flag on bcontainer2.png texture.

Неопознанный объект — hackerlab

Description

Я нашел этот файл у себя на рабочем столе, но не имею понятия, что он из себя представляет. Поможешь мне узнать это?

Files: task.zip (contains file "file")

Analysis

Step 1: File Format Identification

bash
$ file file
file: data

The file command did not recognize the format. Let's look at the hex dump:

bash
$ xxd file | head -5
00000000: 5741 4433 1600 0000 c0e3 0200 a0e2 0200  WAD3............
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

WAD3 is a texture format used in games on the GoldSrc engine (Half-Life, Counter-Strike 1.6).

Step 2: WAD3 Format Structure

A WAD3 file consists of:

  1. Header (12 bytes):

    • Magic: "WAD3" (4 bytes)
    • Number of textures (4 bytes)
    • Offset to texture table (4 bytes)
  2. Directory entries (32 bytes each):

    • Data offset (4 bytes)
    • Size in file (4 bytes)
    • Size in memory (4 bytes)
    • Type (1 byte)
    • Compression (1 byte)
    • Padding (2 bytes)
    • Texture name (16 bytes) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
  3. Texture data (miptex format):

    • Name (16 bytes)
    • Width, height (4 bytes each)
    • Offsets to 4 mipmap levels (4 bytes each)
    • Pixel data (palette indices)
    • Palette (256 * 3 = 768 bytes RGB)

Solution

Python Script for Texture Extraction

python
#!/usr/bin/env python3
"""WAD3 texture extractor for Half-Life/Counter-Strike"""

import struct
from PIL import Image
import os

def extract_wad3(wad_path, output_dir="textures"):
    os.makedirs(output_dir, exist_ok=True)
    
    with open(wad_path, 'rb') as f:
        # Read header
        magic = f.read(4)
        if magic != b'WAD3':
            print(f"Not a WAD3 file: {magic}")
            return
        
        num_textures = struct.unpack('<I', f.read(4))[0]
        dir_offset = struct.unpack('<I', f.read(4))[0]
        
        print(f"WAD3 file with {num_textures} textures")
        
        # Read directory
        f.seek(dir_offset)
        entries = []
        for i in range(num_textures):
            offset = struct.unpack('<I', f.read(4))[0]
            disk_size = struct.unpack('<I', f.read(4))[0]
            mem_size = struct.unpack('<I', f.read(4))[0]
            tex_type = struct.unpack('<B', f.read(1))[0]
            compression = struct.unpack('<B', f.read(1))[0]
            f.read(2)  # padding
            name = f.read(16).rstrip(b'\x00').decode('ascii', errors='ignore')
            entries.append({
                'name': name,
                'offset': offset,
                'size': disk_size,
                'type': tex_type
            })
        
        # Extract each texture
        for entry in entries:
            f.seek(entry['offset'])
            
            # Read miptex header
            tex_name = f.read(16).rstrip(b'\x00').decode('ascii', errors='ignore')
            width = struct.unpack('<I', f.read(4))[0]
            height = struct.unpack('<I', f.read(4))[0]
            
            # Mipmap offsets (relative to texture start)
            mip_offsets = struct.unpack('<4I', f.read(16))
            
            # Read mip0 (full resolution) pixel data
            f.seek(entry['offset'] + mip_offsets[0])
            pixel_data = f.read(width * height)
            
            # Calculate palette offset (after all 4 mipmaps + 2 bytes padding)
            mip_sizes = [
                width * height,
                (width // 2) * (height // 2),
                (width // 4) * (height // 4),
                (width // 8) * (height // 8)
            ]
            palette_offset = entry['offset'] + mip_offsets[0] + sum(mip_sizes) + 2
            
            # Read palette (256 RGB colors)
            f.seek(palette_offset)
            palette_data = f.read(768)
            
            if len(palette_data) < 768:
                print(f"  Skipping {entry['name']}: incomplete palette")
                continue
            
            # Build palette
            palette = []
            for i in range(256):
                r = palette_data[i * 3]
                g = palette_data[i * 3 + 1]
                b = palette_data[i * 3 + 2]
                palette.append((r, g, b))
            
            # Create image
            img = Image.new('RGB', (width, height))
            pixels = img.load()
            
            for y in range(height):
                for x in range(width):
                    idx = pixel_data[y * width + x]
                    pixels[x, y] = palette[idx]
            
            # Save
            output_path = os.path.join(output_dir, f"{entry['name']}.png")
            img.save(output_path)
            print(f"  Extracted: {entry['name']} ({width}x{height})")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

if __name__ == "__main__":
    extract_wad3("file", "extracted_textures")

Extraction Result

WAD3 file with 22 textures
  Extracted: c_bldg1 (128x128)
  Extracted: c_bldg2 (128x128)
  Extracted: c_bldg3 (128x128)
  Extracted: c_viewscreen1 (256x256)
  Extracted: c_viewscreen2 (256x256)
  Extracted: bcontainer1 (256x256)
  Extracted: bcontainer2 (256x256)    <-- FLAG HERE
  Extracted: bcontainer3 (256x256)
  ... (and other textures)

Step 3: Visual Inspection

When viewing the extracted textures, the flag was found on bcontainer2.png: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The texture shows a blue container with the text CODEBY{REDACTED}

WAD3 Reference

FieldSizeDescription
Magic4"WAD3"
NumTextures4Number of textures
DirOffset4Table offset
Directory32*NTexture entries
TexturesvarMiptex data

The palette is always located after 4 mipmap levels + 2 bytes padding. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍