┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
Timeline 0 — picoCTF 2026
~ Imattas aka Zemi
 Category: Forensics
 Difficulty: Easy
 Points: 100
 Author: Imattas aka Zemi

────────────────────────────────────────────────────────────────────────────────

--[ Challenge Description ]--

 Can you find the flag in this disk image? Wrap what you find in the picoCTF
flag format. Download the disk image.

────────────────────────────────────────────────────────────────────────────────

--[ Recon / Initial Analysis ]--

We are given a disk image file and need to extract a hidden flag. The challenge
name "Timeline" strongly hints at using filesystem timeline analysis with Sleuth
Kit tools (fls, mactime) to locate the flag within the image.

A filesystem timeline is a chronological record of all file operations
(creation, modification, access, change) in a disk image. The Sleuth Kit
provides two key tools for this:

1. `fls` -- Lists files and directories in a filesystem image, including deleted
files. With the -m flag, it outputs in "body file" format suitable for timeline
creation.
2. `mactime` -- Reads the body file and produces a human-readable sorted
timeline of all MAC (Modified, Accessed, Changed) timestamps.

The high-level analysis workflow is:

1. Examine the disk image with mmls to identify partitions and their offsets.
2. Generate a body file using fls -m "/" -r to recursively list all files.
3. Create a timeline using mactime to sort all entries chronologically.
4. Search for the flag by grepping through filenames, or by extracting
suspicious files with icat.

Start by inspecting the image and its partition layout:
-- bash --
# Check what type of image we have
file disk.img

# List partitions
mmls disk.img
Example output from mmls:
DOS Partition Table
Offset Sector: 0
Units are in 512-byte sectors

      Slot      Start        End          Length       Description
000:  Meta      0000000000   0000000000   0000000001   Primary Table (#0)
001:  -------   0000000000   0000002047   0000002048   Unallocated
002:  000:000   0000002048   0000206847   0000204800   Linux (0x83)
────────────────────────────────────────────────────────────────────────────────

--[ Vulnerability / Observation ]--

For a 100-point challenge, the flag is intentionally easy to locate once you
know where to look. The key insight is that the flag lives somewhere inside the
filesystem — either as a regular file, a deleted file, or as raw strings — and
standard Sleuth Kit timeline tooling will surface it quickly.

For a 100-point challenge, the flag might also be findable via quicker, more
direct methods:
- strings disk.img | grep -i "picoCTF\|flag" -- brute-force string search
- fls -r -o <offset> disk.img | grep -i flag -- search the file listing directly
- Mounting the image and browsing the filesystem

────────────────────────────────────────────────────────────────────────────────

--[ Exploitation / Solution ]--

:: Step 1: Examine the disk image
-- bash --
# Check what type of image we have
file disk.img

# List partitions
mmls disk.img
:: Step 2: List files in the partition
-- bash --
# Use the offset from mmls (e.g., 2048 sectors)
fls -o 2048 -r disk.img
:: Step 3: Generate timeline body file
-- bash --
# Create body file with recursive file listing
fls -m "/" -o 2048 -r disk.img > body.txt

# Generate the timeline
mactime -b body.txt > timeline.txt
:: Step 4: Search for the flag
-- bash --
# Search in the timeline for flag-related filenames
grep -i "flag" timeline.txt

# Or search for any interesting files
grep -i "flag\|secret\|hidden\|picoctf" timeline.txt

# Also try raw strings search
strings disk.img | grep -i "picoCTF"
:: Step 5: Extract the flag file
-- bash --
# Once you identify the file and its inode from fls output:
# fls output might show something like:
#   r/r 42: flag.txt
# Extract it:
icat -o 2048 disk.img 42
:: Step 6: Wrap the flag

The challenge says "Wrap what you find in the picoCTF flag format," meaning the
raw content might not already be in picoCTF{...} format. Take whatever string
you find and wrap it:
picoCTF{found_string_here}
────────────────────────────────────────────────────────────────────────────────

--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Timeline 0 - picoCTF 2026
Category: Forensics | Points: 100

This script automates the analysis of a disk image to find a hidden flag
using Sleuth Kit tools (fls, mactime, icat) and string searching.

Prerequisites:
    - sleuthkit (apt install sleuthkit)
    - Python 3.6+

Usage:
    python3 solve.py <disk_image>
    python3 solve.py disk.img
"""

import subprocess
import sys
import os
import re
import tempfile


def run_cmd(cmd, description="", capture=True):
    """Run a shell command and return output."""
    if description:
        print(f"[*] {description}")
    try:
        result = subprocess.run(
            cmd, shell=True, capture_output=capture,
            text=True, timeout=120
        )
        if capture:
            return result.stdout, result.stderr, result.returncode
        return "", "", result.returncode
    except FileNotFoundError:
        return "", "Command not found", 1
    except subprocess.TimeoutExpired:
        return "", "Command timed out", 1


def check_dependencies():
    """Verify required tools are installed."""
    tools = ["mmls", "fls", "mactime", "icat", "strings"]
    missing = []
    for tool in tools:
        _, _, rc = run_cmd(f"which {tool}")
        if rc != 0:
            missing.append(tool)
    if missing:
        print(f"[!] Missing tools: {', '.join(missing)}")
        print("[!] Install with: sudo apt install sleuthkit")
        if "strings" in missing:
            print("[!] strings is usually in binutils: sudo apt install binutils")
        return False
    return True


def get_partitions(image):
    """Use mmls to find partition offsets."""
    stdout, stderr, rc = run_cmd(f"mmls '{image}'", "Examining partition table")

    if rc != 0:
        print(f"[*] mmls failed (rc={rc}), image may be a raw filesystem")
        print(f"[*] Trying offset=0...")
        return [0]

    print(stdout)

    # Parse partition offsets - look for Linux/data partitions
    offsets = []
    for line in stdout.split("\n"):
        # Match lines with partition entries (not meta or unallocated)
        if any(x in line.lower() for x in ["linux", "ntfs", "fat", "hfs", "ext"]):
            parts = line.split()
            for part in parts:
                try:
                    offset = int(part)
                    if offset > 0:
                        offsets.append(offset)
                        break
                except ValueError:
                    continue

    if not offsets:
        # Try all numeric values that could be offsets
        for line in stdout.split("\n"):
            parts = line.split()
            if len(parts) >= 3 and "Unallocated" not in line and "Meta" not in line:
                try:
                    offset = int(parts[2])  # Start column
                    if offset > 0:
                        offsets.append(offset)
                except (ValueError, IndexError):
                    continue

    if not offsets:
        print("[*] No partitions found, trying offset=0")
        offsets = [0]

    return offsets


def search_strings(image):
    """Brute-force string search for the flag."""
    print("\n[*] === Method 1: Raw String Search ===")
    stdout, _, _ = run_cmd(
        f"strings '{image}' | grep -i 'picoCTF'",
        "Searching for picoCTF strings in image"
    )

    flags = []
    if stdout.strip():
        for line in stdout.strip().split("\n"):
            print(f"    [+] Found: {line.strip()}")
            flags.append(line.strip())

    # Also search for common flag file content patterns
    stdout2, _, _ = run_cmd(
        f"strings '{image}' | grep -iE '(flag|secret|hidden|key)' | head -20",
        "Searching for flag-related strings"
    )
    if stdout2.strip():
        for line in stdout2.strip().split("\n"):
            print(f"    [?] Possible: {line.strip()}")

    return flags


def analyze_filesystem(image, offset):
    """Use fls to list files and search for flag-related entries."""
    print(f"\n[*] === Method 2: Filesystem Analysis (offset={offset}) ===")

    offset_arg = f"-o {offset}" if offset > 0 else ""

    # List all files recursively
    stdout, stderr, rc = run_cmd(
        f"fls {offset_arg} -r -p '{image}'",
        f"Listing all files (offset={offset})"
    )

    if rc != 0:
        print(f"[!] fls failed: {stderr.strip()}")
        return []

    findings = []
    all_files = stdout.strip().split("\n") if stdout.strip() else []
    print(f"[*] Found {len(all_files)} file entries")

    # Search for flag-related files
    for line in all_files:
        lower = line.lower()
        if any(keyword in lower for keyword in ["flag", "secret", "hidden", "picoctf", "key.txt", "password"]):
            print(f"    [+] Interesting: {line.strip()}")
            findings.append(line.strip())

    # Try to extract interesting files
    for finding in findings:
        # Parse inode from fls output (format: "r/r INODE: filename")
        match = re.search(r'[rd]/[rd*-]\s+(\d+)(?:\(\w+\))?:', finding)
        if match:
            inode = match.group(1)
            content, _, rc = run_cmd(
                f"icat {offset_arg} '{image}' {inode}",
                f"Extracting inode {inode}"
            )
            if content.strip():
                print(f"    [+] Content: {content.strip()}")
                findings.append(content.strip())

    return findings


def create_timeline(image, offset):
    """Generate a filesystem timeline and search for the flag."""
    print(f"\n[*] === Method 3: Timeline Analysis (offset={offset}) ===")

    offset_arg = f"-o {offset}" if offset > 0 else ""

    with tempfile.NamedTemporaryFile(mode='w', suffix='.body', delete=False) as body_file:
        body_path = body_file.name

    with tempfile.NamedTemporaryFile(mode='w', suffix='.timeline', delete=False) as timeline_file:
        timeline_path = timeline_file.name

    try:
        # Generate body file
        stdout, stderr, rc = run_cmd(
            f"fls -m '/' {offset_arg} -r '{image}' > '{body_path}'",
            "Generating timeline body file"
        )

        # Check body file has content
        if os.path.getsize(body_path) == 0:
            print("[!] Body file is empty")
            return []

        # Generate timeline
        stdout, stderr, rc = run_cmd(
            f"mactime -b '{body_path}' > '{timeline_path}'",
            "Creating timeline with mactime"
        )

        # Read and search timeline
        with open(timeline_path, 'r', errors='replace') as f:
            timeline_content = f.read()

        # Search for flag references
        findings = []
        for line in timeline_content.split("\n"):
            lower = line.lower()
            if any(kw in lower for kw in ["flag", "secret", "hidden", "picoctf", "key"]):
                print(f"    [+] Timeline hit: {line.strip()}")
                findings.append(line.strip())

        # Also search the body file for flag-like filenames
        with open(body_path, 'r', errors='replace') as f:
            body_content = f.read()

        for line in body_content.split("\n"):
            if "flag" in line.lower() or "picoctf" in line.lower():
                print(f"    [+] Body file hit: {line.strip()}")

        # Print timeline summary
        total_lines = len(timeline_content.split("\n"))
        print(f"[*] Timeline contains {total_lines} entries")

        # Print first and last few entries for context
        lines = timeline_content.strip().split("\n")
        if len(lines) > 10:
            print("[*] First 5 timeline entries:")
            for line in lines[:5]:
                print(f"    {line}")
            print("[*] Last 5 timeline entries:")
            for line in lines[-5:]:
                print(f"    {line}")
        else:
            print("[*] Full timeline:")
            for line in lines:
                print(f"    {line}")

        return findings

    finally:
        # Cleanup temp files
        for path in [body_path, timeline_path]:
            try:
                os.unlink(path)
            except OSError:
                pass


def main():
    print("=" * 60)
    print("  Timeline 0 - picoCTF 2026 Solver")
    print("  Forensics Disk Image Analysis")
    print("=" * 60)
    print()

    # Get disk image path
    if len(sys.argv) < 2:
        # Look for common image filenames in current directory
        common_names = [
            "disk.img", "disk.raw", "image.img", "image.raw",
            "timeline.img", "timeline.raw", "disk.dd", "image.dd",
            "challenge.img", "disk.flag.img"
        ]
        image = None
        for name in common_names:
            if os.path.exists(name):
                image = name
                break

        if not image:
            # Check for any .img or .raw files
            for f in os.listdir("."):
                if f.endswith((".img", ".raw", ".dd", ".iso", ".E01")):
                    image = f
                    break

        if not image:
            print("Usage: python3 solve.py <disk_image>")
            print("\nNo disk image found in current directory.")
            sys.exit(1)
    else:
        image = sys.argv[1]

    if not os.path.exists(image):
        print(f"[!] File not found: {image}")
        sys.exit(1)

    print(f"[*] Analyzing: {image}")
    print(f"[*] Size: {os.path.getsize(image)} bytes")
    print()

    # Check dependencies
    if not check_dependencies():
        print("\n[!] Some tools missing, results may be incomplete")

    # Method 1: Raw string search (quick win)
    flags = search_strings(image)
    if flags:
        for flag in flags:
            if "picoCTF{" in flag:
                print(f"\n{'='*60}")
                print(f"[FLAG] {flag}")
                print(f"{'='*60}")
                return

    # Method 2 & 3: Filesystem and timeline analysis
    offsets = get_partitions(image)

    all_findings = []
    for offset in offsets:
        findings = analyze_filesystem(image, offset)
        all_findings.extend(findings)

        timeline_findings = create_timeline(image, offset)
        all_findings.extend(timeline_findings)

    # Summary
    print(f"\n{'='*60}")
    print("[*] Analysis complete!")

    if all_findings:
        print("[*] Findings:")
        for f in set(all_findings):
            print(f"    - {f}")

        # Check if any finding contains the flag pattern
        for f in all_findings:
            match = re.search(r'picoCTF\{[^}]+\}', f)
            if match:
                print(f"\n[FLAG] {match.group(0)}")
                return

        # If flag content found but not in picoCTF format
        print("\n[*] Remember: the challenge says to wrap the answer")
        print("[*] in picoCTF{} format: picoCTF{your_finding_here}")
    else:
        print("[-] No flag-related content found automatically.")
        print("[*] Try manual analysis:")
        print("    1. Mount the image: sudo mount -o loop,ro disk.img /mnt")
        print("    2. Browse files: ls -laR /mnt")
        print("    3. Check all text files: find /mnt -type f -exec grep -l picoCTF {} \\;")

    print(f"{'='*60}")


if __name__ == "__main__":
    main()
────────────────────────────────────────────────────────────────────────────────

--[ Key Takeaways ]--

- Filesystem timeline analysis with The Sleuth Kit (mmls, fls, mactime, icat) is
the canonical way to find artifacts in a disk image.
- mmls reveals partition offsets; that offset (-o) is required for every
subsequent TSK command.
- fls -m "/" -r produces a body file that mactime converts into a sorted
MAC-time timeline.
- For low-point forensics, a quick strings | grep picoCTF often finds the flag
before any timeline work is needed.
- icat -o <offset> image <inode> extracts a specific file by inode once fls
identifies it.
- The challenge wording ("wrap what you find") is a reminder that the recovered
content may need to be placed inside picoCTF{...} manually.