┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
Undo — picoCTF 2026
~ Imattas aka Zemi
 Category: General Skills
 Difficulty: Easy
 Points: 100
 Author: Imattas aka Zemi

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

--[ Challenge Description ]--

 Can you reverse a series of Linux text transformations to recover the original
flag?

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

--[ Recon / Initial Analysis ]--

We are given a file (or a command pipeline) that shows how a flag was
transformed using a series of standard Linux text-processing utilities. Our task
is to reverse each transformation in the correct order to recover the original
picoCTF{...} flag.

This is a classic "General Skills" challenge that tests familiarity with Linux
command-line text-processing tools. The challenge provides a transformed string
and either shows or implies the sequence of transformations that were applied.
To recover the flag, we must apply the inverse of each transformation in reverse
order.

:: Common Linux text transformations and their inverses

Forward Command             What It Does                  Inverse Command
base64                      Base64 encode                 base64 -d
rev                         Reverse each line charact...  rev (self-inverse)
tac                         Reverse line order (last ...  tac (self-inverse)
tr 'a-z' 'A-Z'              Lowercase to uppercase        tr 'A-Z' 'a-z'
tr 'A-Za-z' 'N-ZA-Mn-za-m'  ROT13                         tr 'A-Za-z'
'N-ZA-Mn-za-m...
xxd                         Hex dump                      xxd -r
xxd -p                      Plain hex dump                xxd -r -p
gzip / zlib                 Compression                   gzip -d / zlib-flate
-unc...
sed 's/old/new/g'           String substitution           sed 's/new/old/g'
tr 'abc' 'xyz'              Character transliteration     tr 'xyz' 'abc'
cut -c N-                   Remove first N-1 characters   (cannot directly undo
wit...
sort                        Sort lines alphabetically     (cannot directly undo)
fold -w N                   Wrap lines to N characters    tr -d 'n' (rejoin)


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

--[ Vulnerability / Observation ]--

The challenge is solvable because the forward transformations are all (or
mostly) reversible. Each encoding/cipher applied to the flag has a well-known
inverse, so the entire pipeline can be undone provided we apply the inverses in
the right (reverse) order. The key strategy:

1. Read the challenge instructions carefully. The transformations are usually
given explicitly (e.g., as a shell pipeline or a script).
2. Start from the final output and work backwards, applying the inverse of each
transformation.
3. Verify at each step that the intermediate result looks reasonable (e.g.,
valid base64, readable ASCII).
4. The final result should be a valid picoCTF{...} flag.

When the exact pipeline is unknown, the solver can detect likely encodings
(base64, hex/xxd, case) and brute-force combinations of inverse transformations
until the flag pattern appears.

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

--[ Exploitation / Solution ]--

:: Step 1: Examine the provided file/instructions

The challenge typically provides:
- A transformed data file (e.g., transformed.txt)
- The sequence of commands used to transform it (e.g., in a script or
description)

For example, if the challenge says the flag was transformed with:
-- bash --
cat flag.txt | base64 | rev | tr 'a-z' 'A-Z' > transformed.txt
:: Step 2: Reverse the pipeline

Apply the inverse of each command in reverse order:
-- bash --
cat transformed.txt | tr 'A-Z' 'a-z' | rev | base64 -d
Breaking this down:
1. Last transformation was `tr 'a-z' 'A-Z'` (uppercase) -> Undo with tr 'A-Z'
'a-z' (lowercase)
2. Second transformation was `rev` (reverse) -> Undo with rev (self-inverse)
3. First transformation was `base64` (encode) -> Undo with base64 -d (decode)

:: Step 3: Handle multi-step or nested transformations

If the challenge involves multiple rounds or nested encodings, keep peeling
layers:
-- bash --
# Example: double base64 + rev + ROT13
cat data.txt | tr 'A-Za-z' 'N-ZA-Mn-za-m' | rev | base64 -d | base64 -d
:: Step 4: Verify the flag

The output should match the pattern picoCTF{...}.

The accompanying solver reads the transformed data, applies common inverse
transformations, and uses pattern detection to automatically determine and
reverse the correct sequence.

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

--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Undo - picoCTF 2026
Category: General Skills | Points: 100

Automated solver that reverses Linux text transformations to recover the flag.

Usage:
    python3 solve.py <transformed_file>
    python3 solve.py <transformed_file> --transformations "base64 rev tr_upper rot13"
    echo "TRANSFORMED_STRING" | python3 solve.py -

If the transformations are known, pass them with --transformations in the ORDER
THEY WERE APPLIED (the script will reverse them automatically).

If not specified, the script will try to auto-detect and brute-force common
transformation sequences.

Supported transformations:
    base64      - base64 encoding
    rev         - character reversal per line
    tac         - line order reversal
    tr_upper    - lowercase -> uppercase
    tr_lower    - uppercase -> lowercase
    rot13       - ROT13 cipher
    xxd         - hex dump (xxd format)
    xxd_plain   - plain hex dump
    fold_N      - fold/wrap at N characters (e.g., fold_80)
"""

import argparse
import base64
import codecs
import itertools
import re
import sys


FLAG_PATTERN = re.compile(r'picoCTF\{[^}]+\}')


def undo_base64(data):
    """Reverse base64 encoding."""
    # Strip whitespace that might have been introduced
    cleaned = data.strip().replace("\n", "").replace("\r", "").replace(" ", "")
    # Add padding if needed
    padding = 4 - len(cleaned) % 4
    if padding != 4:
        cleaned += "=" * padding
    try:
        decoded = base64.b64decode(cleaned)
        return decoded.decode("utf-8", errors="replace")
    except Exception:
        return None


def undo_rev(data):
    """Reverse character order per line (inverse of `rev`)."""
    lines = data.split("\n")
    return "\n".join(line[::-1] for line in lines)


def undo_tac(data):
    """Reverse line order (inverse of `tac`)."""
    lines = data.split("\n")
    # Remove trailing empty line if present
    if lines and lines[-1] == "":
        lines = lines[:-1]
    return "\n".join(reversed(lines))


def undo_tr_upper(data):
    """Reverse tr 'a-z' 'A-Z' (convert uppercase back to lowercase)."""
    return data.lower()


def undo_tr_lower(data):
    """Reverse tr 'A-Z' 'a-z' (convert lowercase back to uppercase)."""
    return data.upper()


def undo_rot13(data):
    """Reverse ROT13 (self-inverse)."""
    return codecs.decode(data, "rot_13")


def undo_xxd(data):
    """Reverse xxd hex dump format."""
    result = []
    for line in data.strip().split("\n"):
        # xxd format: "00000000: 7069 636f 4354 467b  picoCTF{"
        parts = line.split(":")
        if len(parts) >= 2:
            hex_part = parts[1].split("  ")[0].strip()
            hex_clean = hex_part.replace(" ", "")
            try:
                result.append(bytes.fromhex(hex_clean).decode("utf-8", errors="replace"))
            except ValueError:
                continue
    return "".join(result) if result else None


def undo_xxd_plain(data):
    """Reverse xxd -p (plain hex dump)."""
    cleaned = data.strip().replace("\n", "").replace("\r", "").replace(" ", "")
    try:
        return bytes.fromhex(cleaned).decode("utf-8", errors="replace")
    except ValueError:
        return None


def undo_fold(data):
    """Reverse fold (rejoin wrapped lines)."""
    return data.replace("\n", "")


# Map of transformation names to their undo functions
UNDO_MAP = {
    "base64": undo_base64,
    "rev": undo_rev,
    "tac": undo_tac,
    "tr_upper": undo_tr_upper,
    "tr_lower": undo_tr_lower,
    "rot13": undo_rot13,
    "xxd": undo_xxd,
    "xxd_plain": undo_xxd_plain,
    "fold": undo_fold,
}


def check_flag(data):
    """Check if data contains a picoCTF flag."""
    if data is None:
        return None
    match = FLAG_PATTERN.search(data)
    return match.group(0) if match else None


def detect_encoding(data):
    """Try to detect what encoding/transformation was last applied."""
    detections = []

    stripped = data.strip()

    # Check for xxd format (lines starting with hex offset)
    if re.match(r'^[0-9a-f]{8}:', stripped, re.MULTILINE):
        detections.append("xxd")

    # Check for plain hex (only hex characters and newlines)
    hex_clean = stripped.replace("\n", "").replace("\r", "").replace(" ", "")
    if re.match(r'^[0-9a-fA-F]+$', hex_clean) and len(hex_clean) % 2 == 0:
        detections.append("xxd_plain")

    # Check for base64 (valid base64 chars, possibly with = padding)
    b64_clean = stripped.replace("\n", "").replace("\r", "")
    if re.match(r'^[A-Za-z0-9+/]+=*$', b64_clean) and len(b64_clean) >= 4:
        detections.append("base64")

    # Check if all uppercase (might be tr_upper)
    alpha_only = re.sub(r'[^a-zA-Z]', '', stripped)
    if alpha_only and alpha_only == alpha_only.upper():
        detections.append("tr_upper")

    # Check if all lowercase
    if alpha_only and alpha_only == alpha_only.lower():
        detections.append("tr_lower")

    return detections


def apply_undo_sequence(data, sequence):
    """Apply a sequence of undo operations (in reverse order of application)."""
    current = data
    for transform in reversed(sequence):
        # Handle fold_N pattern
        if transform.startswith("fold_"):
            undo_func = undo_fold
        elif transform in UNDO_MAP:
            undo_func = UNDO_MAP[transform]
        else:
            return None

        result = undo_func(current)
        if result is None:
            return None
        current = result

    return current


def brute_force_transformations(data, max_depth=4):
    """Try all combinations of transformations up to max_depth."""
    transforms = ["base64", "rev", "tac", "tr_upper", "rot13", "xxd_plain", "fold"]

    # First check the raw data
    flag = check_flag(data)
    if flag:
        return flag, []

    # Start with most likely single transforms, then increase depth
    for depth in range(1, max_depth + 1):
        print(f"[*] Trying depth {depth} ({len(transforms)**depth} combinations)...")

        for combo in itertools.product(transforms, repeat=depth):
            result = apply_undo_sequence(data, list(combo))
            if result is not None:
                flag = check_flag(result)
                if flag:
                    return flag, list(combo)

    return None, []


def smart_peel(data, max_depth=6):
    """Intelligently peel transformations one at a time using detection."""
    current = data
    applied = []

    for depth in range(max_depth):
        # Check if we already have the flag
        flag = check_flag(current)
        if flag:
            return flag, applied

        # Detect likely encodings
        detections = detect_encoding(current)

        # Also always try rev and rot13 since they're hard to detect
        to_try = list(dict.fromkeys(detections + ["rev", "rot13", "tac", "fold"]))

        found_progress = False
        for transform in to_try:
            undo_func = UNDO_MAP.get(transform)
            if not undo_func:
                continue

            result = undo_func(current)
            if result is None:
                continue

            # Check if this transformation produced a flag
            flag = check_flag(result)
            if flag:
                applied.append(transform)
                return flag, applied

            # Check if the result looks "more decoded" (contains more printable chars
            # or is closer to the flag pattern)
            if "pico" in result.lower() or "ctf{" in result.lower():
                current = result
                applied.append(transform)
                found_progress = True
                print(f"    [~] Applied undo_{transform}: partial flag visible")
                break

        if not found_progress:
            # Try each and see if we get closer (heuristic)
            for transform in to_try:
                undo_func = UNDO_MAP.get(transform)
                if not undo_func:
                    continue
                result = undo_func(current)
                if result and result != current:
                    # Recurse with this option
                    sub_flag = check_flag(result)
                    if sub_flag:
                        applied.append(transform)
                        return sub_flag, applied

            break  # No progress possible

    return None, applied


def main():
    parser = argparse.ArgumentParser(
        description="Undo solver - picoCTF 2026"
    )
    parser.add_argument(
        "input",
        help="Path to the transformed file, or '-' for stdin"
    )
    parser.add_argument(
        "--transformations", "-t",
        help="Space-separated list of transformations in order applied "
             "(e.g., 'base64 rev tr_upper rot13')",
        default=None
    )
    parser.add_argument(
        "--brute-force", "-b",
        action="store_true",
        help="Brute-force all transformation combinations (slow but thorough)"
    )
    parser.add_argument(
        "--max-depth", "-d",
        type=int, default=4,
        help="Maximum transformation depth for brute-force (default: 4)"
    )
    args = parser.parse_args()

    # Read input
    if args.input == "-":
        data = sys.stdin.read()
    else:
        try:
            with open(args.input, "r", errors="ignore") as f:
                data = f.read()
        except FileNotFoundError:
            print(f"[!] File not found: {args.input}")
            sys.exit(1)

    print(f"[*] Undo Solver - picoCTF 2026")
    print(f"[*] Input length: {len(data)} characters")
    print(f"[*] First 100 chars: {data[:100]!r}\n")

    # Check if flag is already visible in raw data
    flag = check_flag(data)
    if flag:
        print(f"[+] Flag found directly in input: {flag}")
        return

    # Method 1: If transformations are specified, apply them in reverse
    if args.transformations:
        transforms = args.transformations.strip().split()
        print(f"[*] Applying inverse of: {' | '.join(transforms)}")
        result = apply_undo_sequence(data, transforms)
        if result:
            flag = check_flag(result)
            if flag:
                print(f"\n{'='*60}")
                print(f"[+] FLAG FOUND: {flag}")
                print(f"{'='*60}")
                return
            else:
                print(f"[-] Result: {result[:200]!r}")
                print(f"[-] No flag pattern found in result")
        else:
            print(f"[-] Transformation sequence failed")
        return

    # Method 2: Smart detection-based peeling
    print(f"[*] Attempting smart detection-based decoding...")
    detections = detect_encoding(data)
    print(f"    Detected encodings: {detections if detections else 'none obvious'}\n")

    flag, applied = smart_peel(data)
    if flag:
        print(f"\n{'='*60}")
        print(f"[+] FLAG FOUND: {flag}")
        print(f"[+] Transformations undone: {' -> '.join(applied)}")
        print(f"{'='*60}")
        return

    # Method 3: Brute-force if requested or as fallback
    if args.brute_force or not applied:
        print(f"\n[*] Trying brute-force approach (max depth: {args.max_depth})...")
        flag, combo = brute_force_transformations(data, args.max_depth)
        if flag:
            print(f"\n{'='*60}")
            print(f"[+] FLAG FOUND: {flag}")
            print(f"[+] Original transformation sequence: {' | '.join(combo)}")
            print(f"[+] Undo sequence: {' | '.join(reversed(combo))}")
            print(f"{'='*60}")
            return

    # If nothing worked
    print(f"\n[-] Could not automatically recover the flag.")
    print(f"[*] Suggestions:")
    print(f"    1. Specify the transformations with --transformations")
    print(f"       Example: python3 solve.py data.txt -t 'base64 rev tr_upper'")
    print(f"    2. Try with --brute-force --max-depth 5 for deeper search")
    print(f"    3. Manually examine the data and identify transformations")
    print(f"    4. Common pipelines to try manually:")
    print(f"       cat data.txt | rev | base64 -d")
    print(f"       cat data.txt | tr 'A-Z' 'a-z' | rev | base64 -d")
    print(f"       cat data.txt | tr 'A-Za-z' 'N-ZA-Mn-za-m' | base64 -d")
    print(f"       cat data.txt | xxd -r -p")
    print(f"       cat data.txt | base64 -d | rev")
    print(f"       cat data.txt | base64 -d | base64 -d")


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

--[ Key Takeaways ]--

- To undo a transformation pipeline, apply each operation's inverse in reverse
order.
- Many Linux text tools are self-inverse (rev, tac, rot13, nibble-style
transforms), which simplifies reversal.
- Recognizing encodings by shape — base64 charset, xxd offset prefixes,
all-upper/all-lower casing — lets you auto-detect the last applied layer.
- When the pipeline is unknown, a bounded brute-force over common inverse
combinations, checking for the picoCTF{...} pattern, reliably recovers the flag.
- cut/sort are lossy and cannot be undone, so a solvable challenge avoids them.
- Verify intermediate results look reasonable (valid base64/readable ASCII) at
each peel step.