┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
Hidden Cipher 1 — picoCTF 2026
~ Imattas aka Zemi
 Category: Reverse Engineering
 Difficulty: Easy
 Points: 100
 Author: Imattas aka Zemi

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

--[ Challenge Description ]--

 The flag is right in front of you; just slightly encrypted. All you have to do
is figure out the cipher and the key. You can download the binary.

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

--[ Recon / Initial Analysis ]--

This is an introductory reverse engineering challenge where a binary contains an
encrypted flag and a simple cipher algorithm. By reverse engineering the
encryption logic, we can write a decryption routine to recover the original
flag.

:: Binary Analysis

The binary likely:

1. Contains the encrypted flag as hardcoded data -- either embedded as a byte
array, a string literal, or stored in a separate file (e.g., rev_this or
encrypted.txt).
2. Implements a simple cipher to encrypt the flag. At 100 points, this will be a
basic transformation like:
  - XOR cipher with a single-byte or multi-byte key
  - Caesar/shift cipher with a fixed or alternating shift
  - Alternating arithmetic (e.g., add N on even indices, subtract M on odd
indices)
  - A combination of the above

Initial inspection of the downloaded files:
-- bash --
file chall        # Identify the binary type
strings chall     # Look for readable strings or the encrypted flag
If there is an accompanying data file (e.g., rev_this, enc_flag), examine it:
-- bash --
xxd rev_this      # View hex dump of the encrypted data
────────────────────────────────────────────────────────────────────────────────

--[ Vulnerability / Observation ]--

Using a disassembler (Ghidra, IDA, or even objdump), we decompile the main
function to understand the encryption logic. The typical pattern seen in picoCTF
challenges of this type is:
-- c --
for (int i = 0; i < len; i++) {
    if (i % 2 == 0) {
        encrypted[i] = flag[i] + KEY1;   // e.g., +5
    } else {
        encrypted[i] = flag[i] - KEY2;   // e.g., -2
    }
}
Or for XOR-based:
-- c --
for (int i = 0; i < len; i++) {
    encrypted[i] = flag[i] ^ key[i % key_len];
}
The hint "figure out the cipher and the key" suggests there is a specific key
value or set of values used in the transformation.

:: Known Patterns from picoCTF

A very common pattern in picoCTF reverse engineering challenges:
- Characters at even indices (0, 2, 4, ...) have a value added (e.g., +5)
- Characters at odd indices (1, 3, 5, ...) have a value subtracted (e.g., -2)
- The first few characters (e.g., indices 0-7 for "picoCTF{") may be left
unmodified

To decrypt:
- Even indices: subtract the added value
- Odd indices: add the subtracted value

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

--[ Exploitation / Solution ]--

:: Decryption Strategy

Once we identify the cipher:

1. For additive/subtractive ciphers: Reverse the operation (subtract where it
added, add where it subtracted).
2. For XOR ciphers: XOR is its own inverse -- apply the same XOR key to the
ciphertext to get plaintext.
3. For Caesar ciphers: Shift in the opposite direction by the same amount.

:: Step 1: Download and examine the binary
-- bash --
file chall        # Identify the binary type
strings chall     # Look for readable strings or the encrypted flag
If there is an accompanying data file (e.g., rev_this, enc_flag), examine it:
-- bash --
xxd rev_this      # View hex dump of the encrypted data
:: Step 2: Decompile with Ghidra

Open the binary in Ghidra, navigate to main(), and identify:
- Where the flag data is loaded
- The encryption loop
- The arithmetic/XOR operations and their constants (the "key")
- Which indices are treated differently (even vs. odd, or first N characters
skipped)

:: Step 3: Write the decryption script

Based on the identified cipher, write a Python script that applies the inverse
operations. The solve script below covers the most common cipher variants seen
in picoCTF.

:: Step 4: Run the decryption
-- bash --
python3 solve.py
If the binary comes with an encrypted data file:
-- bash --
python3 solve.py rev_this
────────────────────────────────────────────────────────────────────────────────

--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Hidden Cipher 1 - picoCTF 2026
Category: Reverse Engineering | Points: 100

The binary contains a flag encrypted with a simple cipher.
This script attempts multiple common cipher types to decrypt it.

Usage:
    python3 solve.py                     # Use embedded encrypted data
    python3 solve.py <encrypted_file>    # Read encrypted data from file
    python3 solve.py --binary <binary>   # Extract encrypted data from binary

Steps to adapt this script:
    1. Run the binary through Ghidra/IDA to identify the cipher
    2. Extract the encrypted flag bytes (from binary or data file)
    3. Update ENCRYPTED_FLAG and select the correct cipher method
    4. Run the script to decrypt
"""

import sys
import struct
import os

# ============================================================
# CONFIGURATION - Update these after reversing the binary
# ============================================================

# The encrypted flag bytes extracted from the binary or data file.
# Update this with the actual encrypted bytes from your instance.
# Example (from a typical picoCTF reverse cipher challenge):
ENCRYPTED_FLAG = b""

# If the encrypted flag is in a file, specify the filename here
# (or pass it as a command-line argument)
ENCRYPTED_FILE = "rev_this"

# Known plaintext prefix -- picoCTF flags always start with this
KNOWN_PREFIX = b"picoCTF{"

# Number of prefix characters left unencrypted (0 if all are encrypted)
UNENCRYPTED_PREFIX_LEN = 0

# ============================================================
# CIPHER IMPLEMENTATIONS
# ============================================================

def decrypt_alternating_add_sub(data, add_val=5, sub_val=2, start_index=0):
    """
    Decrypt alternating add/subtract cipher.
    Even indices had add_val added -> subtract it
    Odd indices had sub_val subtracted -> add it

    This is the most common cipher in picoCTF RE challenges.
    """
    result = bytearray()
    for i in range(len(data)):
        if i < start_index:
            # Characters before start_index were not encrypted
            result.append(data[i])
        elif (i - start_index) % 2 == 0:
            # Even offset: was encrypted by adding -> decrypt by subtracting
            result.append((data[i] - add_val) % 256)
        else:
            # Odd offset: was encrypted by subtracting -> decrypt by adding
            result.append((data[i] + sub_val) % 256)
    return bytes(result)


def decrypt_xor_single_byte(data, key):
    """Decrypt single-byte XOR cipher."""
    return bytes([b ^ key for b in data])


def decrypt_xor_multi_byte(data, key):
    """Decrypt multi-byte (repeating) XOR cipher."""
    if isinstance(key, str):
        key = key.encode()
    return bytes([b ^ key[i % len(key)] for i, b in enumerate(data)])


def decrypt_caesar(data, shift):
    """Decrypt Caesar cipher (shift all printable chars)."""
    result = bytearray()
    for b in data:
        if 32 <= b <= 126:
            result.append(((b - 32 - shift) % 95) + 32)
        else:
            result.append(b)
    return bytes(result)


def decrypt_subtract_constant(data, constant):
    """Decrypt by adding a constant (reverse of subtracting)."""
    return bytes([(b + constant) % 256 for b in data])


def decrypt_add_constant(data, constant):
    """Decrypt by subtracting a constant (reverse of adding)."""
    return bytes([(b - constant) % 256 for b in data])


# ============================================================
# KEY RECOVERY
# ============================================================

def try_recover_xor_key(data, known_prefix=KNOWN_PREFIX):
    """Try to recover XOR key using known plaintext attack."""
    if len(data) < len(known_prefix):
        return None

    # Recover key bytes from known prefix
    key_bytes = bytes([data[i] ^ known_prefix[i] for i in range(len(known_prefix))])

    # Check if the key repeats (common pattern)
    for key_len in range(1, len(known_prefix) + 1):
        candidate_key = key_bytes[:key_len]
        # Verify the key works for all known prefix bytes
        matches = all(
            data[i] ^ candidate_key[i % key_len] == known_prefix[i]
            for i in range(len(known_prefix))
        )
        if matches:
            return candidate_key

    return key_bytes


def try_recover_arithmetic_params(data, known_prefix=KNOWN_PREFIX, start_index=0):
    """Try to recover add/sub parameters using known plaintext."""
    if len(data) < len(known_prefix):
        return None, None

    diffs = []
    for i in range(start_index, min(len(data), len(known_prefix))):
        diffs.append(data[i] - known_prefix[i])

    if len(diffs) < 2:
        return None, None

    # Check for alternating pattern
    even_diffs = [diffs[i] for i in range(0, len(diffs), 2)]
    odd_diffs = [diffs[i] for i in range(1, len(diffs), 2)]

    if even_diffs and all(d == even_diffs[0] for d in even_diffs):
        add_val = even_diffs[0]
    else:
        add_val = None

    if odd_diffs and all(d == odd_diffs[0] for d in odd_diffs):
        sub_val = -odd_diffs[0]
    else:
        sub_val = None

    return add_val, sub_val


# ============================================================
# MAIN LOGIC
# ============================================================

def load_encrypted_data():
    """Load encrypted data from file or configuration."""
    # Check command line arguments
    if len(sys.argv) > 1 and sys.argv[1] != "--binary":
        filepath = sys.argv[1]
        if os.path.exists(filepath):
            with open(filepath, "rb") as f:
                data = f.read()
            print(f"[*] Loaded {len(data)} bytes from {filepath}")
            return data

    if len(sys.argv) > 2 and sys.argv[1] == "--binary":
        binary_path = sys.argv[2]
        if os.path.exists(binary_path):
            # Try to extract strings that look like encrypted flags
            with open(binary_path, "rb") as f:
                binary_data = f.read()
            # Look for data near "picoCTF" or known markers
            print(f"[*] Loaded binary {binary_path} ({len(binary_data)} bytes)")
            print("[*] Searching for encrypted flag data in binary...")
            return binary_data

    # Try default encrypted file
    if os.path.exists(ENCRYPTED_FILE):
        with open(ENCRYPTED_FILE, "rb") as f:
            data = f.read()
        print(f"[*] Loaded {len(data)} bytes from {ENCRYPTED_FILE}")
        return data

    # Try other common filenames
    for fname in ["enc_flag", "encrypted.txt", "flag.enc", "output.txt", "cipher.txt"]:
        if os.path.exists(fname):
            with open(fname, "rb") as f:
                data = f.read()
            print(f"[*] Loaded {len(data)} bytes from {fname}")
            return data

    # Use embedded data
    if ENCRYPTED_FLAG:
        print(f"[*] Using embedded encrypted data ({len(ENCRYPTED_FLAG)} bytes)")
        return ENCRYPTED_FLAG

    return None


def is_valid_flag(data):
    """Check if decrypted data looks like a valid picoCTF flag."""
    try:
        text = data.decode("ascii", errors="strict")
        if text.startswith("picoCTF{") and text.endswith("}"):
            # Check all chars are printable
            if all(32 <= ord(c) <= 126 for c in text):
                return True
    except Exception:
        pass

    # Partial check -- does it contain the flag format?
    try:
        text = data.decode("ascii", errors="replace")
        if "picoCTF{" in text:
            return True
    except Exception:
        pass

    return False


def extract_flag(data):
    """Extract picoCTF{...} from decrypted data."""
    try:
        text = data.decode("ascii", errors="replace")
        start = text.find("picoCTF{")
        if start == -1:
            return None
        end = text.find("}", start)
        if end == -1:
            return text[start:]
        return text[start:end + 1]
    except Exception:
        return None


def main():
    print("=" * 60)
    print(" Hidden Cipher 1 - picoCTF 2026 Solver")
    print("=" * 60)
    print()

    data = load_encrypted_data()

    if data is None:
        print("[!] No encrypted data found.")
        print("[!] Usage:")
        print("    python3 solve.py <encrypted_file>")
        print("    python3 solve.py --binary <binary_file>")
        print()
        print("[*] Or update ENCRYPTED_FLAG in this script with the")
        print("    encrypted bytes extracted from the binary/file.")
        print()
        print("[*] Example workflow:")
        print("    1. Open binary in Ghidra")
        print("    2. Find the encrypted flag data (byte array or string)")
        print("    3. Paste it into ENCRYPTED_FLAG in this script")
        print("    4. Run this script again")
        return

    print(f"[*] Encrypted data (hex): {data.hex()}")
    print(f"[*] Encrypted data (raw): {data}")
    print()

    # ----- Try all decryption methods -----

    results = []

    # Method 1: Alternating add/subtract (most common in picoCTF)
    print("[*] Trying alternating add/subtract ciphers...")
    for add_val in range(1, 20):
        for sub_val in range(1, 20):
            for start in [0, 8]:  # 0 = all encrypted, 8 = first 8 chars unencrypted
                dec = decrypt_alternating_add_sub(data, add_val, sub_val, start)
                if is_valid_flag(dec):
                    flag = extract_flag(dec)
                    results.append(("alternating_add_sub", f"add={add_val}, sub={sub_val}, start={start}", flag))

    # Method 1b: Use known-plaintext to recover arithmetic parameters
    add_val, sub_val = try_recover_arithmetic_params(data)
    if add_val is not None and sub_val is not None:
        print(f"[*] Recovered arithmetic params: add={add_val}, sub={sub_val}")
        for start in [0, 8]:
            dec = decrypt_alternating_add_sub(data, add_val, sub_val, start)
            flag = extract_flag(dec)
            if flag:
                results.append(("alternating_add_sub (recovered)", f"add={add_val}, sub={sub_val}, start={start}", flag))

    # Method 2: Single-byte XOR
    print("[*] Trying single-byte XOR...")
    for key in range(1, 256):
        dec = decrypt_xor_single_byte(data, key)
        if is_valid_flag(dec):
            flag = extract_flag(dec)
            results.append(("xor_single", f"key=0x{key:02x}", flag))

    # Method 3: Multi-byte XOR with known-plaintext recovery
    print("[*] Trying multi-byte XOR with known-plaintext recovery...")
    recovered_key = try_recover_xor_key(data)
    if recovered_key:
        dec = decrypt_xor_multi_byte(data, recovered_key)
        if is_valid_flag(dec):
            flag = extract_flag(dec)
            results.append(("xor_multi (recovered)", f"key={recovered_key.hex()}", flag))

    # Method 4: Caesar cipher
    print("[*] Trying Caesar cipher...")
    for shift in range(1, 95):
        dec = decrypt_caesar(data, shift)
        if is_valid_flag(dec):
            flag = extract_flag(dec)
            results.append(("caesar", f"shift={shift}", flag))

    # Method 5: Simple add/subtract constant
    print("[*] Trying constant add/subtract...")
    for const in range(1, 128):
        dec = decrypt_add_constant(data, const)
        if is_valid_flag(dec):
            flag = extract_flag(dec)
            results.append(("subtract_constant", f"const={const}", flag))

        dec = decrypt_subtract_constant(data, const)
        if is_valid_flag(dec):
            flag = extract_flag(dec)
            results.append(("add_constant", f"const={const}", flag))

    # ----- Report results -----
    print()
    if results:
        print("=" * 60)
        print(f" FOUND {len(results)} POSSIBLE DECRYPTION(S)")
        print("=" * 60)
        for method, params, flag in results:
            print(f"  Method: {method}")
            print(f"  Params: {params}")
            print(f"  Flag:   {flag}")
            print()
    else:
        print("[!] No valid decryption found with standard methods.")
        print("[*] Manual reverse engineering may be needed:")
        print("    1. Open the binary in Ghidra")
        print("    2. Find the encryption function")
        print("    3. Identify the exact cipher algorithm and key")
        print("    4. Update this script accordingly")
        print()
        print("[*] Hint: Look for loops with XOR, ADD, SUB, or ROL/ROR")
        print("    operations on character arrays in the binary.")


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

--[ Key Takeaways ]--

- Simple RE cipher challenges hide the flag with a reversible per-character
transform; identify the operation and its constants ("the key") in the
decompiled loop.
- Common cipher families: single/multi-byte XOR, Caesar/shift, alternating
add-on-even / subtract-on-odd, and constant add/subtract.
- XOR is its own inverse; additive/subtractive ciphers are reversed by applying
the opposite operation.
- The picoCTF{ prefix is reliable known plaintext — diff it against the
ciphertext to recover XOR keys or arithmetic offsets directly.
- Watch for the first N characters (often the 8-byte picoCTF{ prefix) being left
unencrypted.
- Tools used: file, strings, xxd, Ghidra/IDA, objdump, and a brute-force Python
decryptor.