$ Forensics: Meow Transmission Revenge

BITSCTF 2026 By TaklaMan
Flag: BITSCTF{r3v3ng3_0f_th3_c4t_m4p_m4st3r}

Initial Findings

  • exiftool metadata included a strong Arnold Cat Map hint with triple personality (R/G/B channels):
    • Red:
      • Styles: [1, 2, 1]
      • Spins: [47, 37, 29]
      • Sequence: [1, 2, 3]
    • Green:
      • Styles: [2, 1, 2]
      • Spins: [20, 50, 20]
      • Sequence: [2, 3, 1]
    • Blue:
      • Styles: [1, 1, 1]
      • Spins: [10, 80, 10]
      • Sequence: [3, 1, 2]
    • Hint: "I stopped counting my spins (periods). You figure it out."

Key Interpretation

  • This is a sequel of the previous Arnold-map challenge.
  • “Stopped counting periods” means periods are not explicitly given; derive/use known periodic behavior for the Arnold variants on N=128.
  • The red channel parameters match the previous challenge pattern; solving the LSB layer with the same Arnold conventions reveals the hidden text.

Core Method

  1. Load PNG as RGB and extract channel LSBs.
  2. Reuse the previous challenge Arnold brute-force conventions (coordinate mode + assignment mode + map candidates).
  3. Apply 3-stage reverse transform (decryption) in reverse stage order.
  4. For the winning config, decoded LSB renders the plaintext flag.

Winning practical config used:

  • m1 = [[2,1],[1,1]]
  • m2 = [[3,1],[2,1]]
  • coord_mode = 1
  • assign_mode = 0
  • inverse-mode iteration using periods:
    • style-1 period: 96
    • style-2 period: 64
    • iterations per stage: period - spin

Recovered Flag

BITSCTF{r3v3ng3_0f_th3_c4t_m4p_m4st3r}

Main Program

#!/usr/bin/env python3
from __future__ import annotations

import argparse
from pathlib import Path

import numpy as np
from PIL import Image

N = 128

# Working config recovered during solve
M1 = np.array([[2, 1], [1, 1]], dtype=np.int64)  # style 1
M2 = np.array([[3, 1], [2, 1]], dtype=np.int64)  # style 2

# Red personality (same critical structure as previous challenge)
STAGES = [
    (M1, 47, 96),  # (matrix, spin, period)
    (M2, 37, 64),
    (M1, 29, 96),
]

rows, cols = np.indices((N, N))

def arnold_step(arr: np.ndarray, A: np.ndarray, coord_mode: int, assign_mode: int) -> np.ndarray:
    """
    coord_mode:
      0 => x=row, y=col
      1 => x=col, y=row

    assign_mode:
      0 => out[r2,c2] = in[r,c]
      1 => out[r,c]  = in[r2,c2]
    """
    if coord_mode == 0:
        x, y = rows, cols
    else:
        x, y = cols, rows

    xp = (A[0, 0] * x + A[0, 1] * y) % N
    yp = (A[1, 0] * x + A[1, 1] * y) % N

    if coord_mode == 0:
        r2, c2 = xp, yp
    else:
        r2, c2 = yp, xp

    out = np.empty_like(arr)
    if assign_mode == 0:
        out[r2, c2] = arr[rows, cols]
    else:
        out[rows, cols] = arr[r2, c2]
    return out

def arnold_apply(arr: np.ndarray, A: np.ndarray, iterations: int, coord_mode: int, assign_mode: int) -> np.ndarray:
    out = arr
    for _ in range(iterations):
        out = arnold_step(out, A, coord_mode, assign_mode)
    return out

def solve(input_png: Path, out_png: Path) -> None:
    img = np.array(Image.open(input_png).convert("RGB"))

    # Red-channel LSB
    bit = (img[:, :, 0] & 1).astype(np.uint8)

    # Decrypt: reverse order + inverse iteration (period - spin)
    coord_mode = 1
    assign_mode = 0
    out = bit.copy()
    for A, spin, period in STAGES[::-1]:
        k = period - spin
        out = arnold_apply(out, A, k, coord_mode, assign_mode)

    Image.fromarray((out * 255).astype(np.uint8)).save(out_png)
    print(f"[+] Saved decoded bitmap to: {out_png}")
    print("[+] Read flag from decoded bitmap:")
    print("    BITSCTF{r3v3ng3_0f_th3_c4t_m4p_m4st3r}")

def main() -> None:
    p = argparse.ArgumentParser(description="Meow Transmission Revenge solver")
    p.add_argument("input", type=Path, help="Path to revenge_transmission.png")
    p.add_argument("--out", type=Path, default=Path("decoded_candidate_lsb.png"), help="Output decoded bitmap")
    args = p.parse_args()

    solve(args.input, args.out)

if __name__ == "__main__":
    main()

Reproduction

python3 solve_meow_revenge.py revenge_transmission.png --out decoded_candidate_lsb.png

Then open decoded_candidate_lsb.png (or upscaled version) and read the flag text.