│ Category: Reverse Engineering
│ Difficulty: Easy
│ Points: 100
│ Author: Imattas aka Zemi
────────────────────────────────────────────────────────────────────────────────
--[ Challenge Description ]--
│ The flag is right in front of you... kind of. You just need to solve a basic
math problem to see it. But to get the real flag, you'll have to figure out the
cipher.
────────────────────────────────────────────────────────────────────────────────
--[ Recon / Initial Analysis ]--
This is a beginner-friendly reverse engineering challenge (100 points) where the
flag has been obfuscated using a mathematical cipher applied to each character.
The description gives two major hints:
1. "solve a basic math problem" -- The cipher involves simple arithmetic
operations (addition, subtraction, XOR, multiplication, modular arithmetic)
applied to the characters of the flag.
2. "figure out the cipher" -- We need to reverse-engineer the transformation to
recover the original flag characters.
:: Typical Pattern for This Type of Challenge
The challenge likely provides either:
- A compiled binary (ELF/PE) that contains the encrypted flag and the cipher
logic, or
- Source code (C, Python, or Java) that shows how the flag was encrypted.
:: Analysis Steps
1. Examine the binary/source: Use a disassembler (Ghidra, IDA) or just read the
source code to find where the encrypted flag data is stored and how the cipher
operates.
2. Identify the cipher: Look for loops that iterate over the flag characters and
apply arithmetic operations. Note:
- What operation is applied (add, subtract, XOR, multiply)?
- Is the key constant or does it change per position (e.g., using the index
i)?
- Are different operations applied to even vs. odd indices?
────────────────────────────────────────────────────────────────────────────────
--[ Vulnerability / Observation ]--
The cipher is usually a character-by-character transformation such as:
- encrypted[i] = flag[i] + key (Caesar-style shift)
- encrypted[i] = flag[i] ^ key (XOR with a constant or rotating key)
- encrypted[i] = flag[i] + i (position-dependent shift)
- encrypted[i] = flag[i] * a + b (affine cipher)
- A combination: even-index characters get one operation, odd-index characters
get another
Given the "Hidden Cipher 2" name (implying a sequel to a simpler version), the
cipher likely involves a position-dependent or alternating arithmetic operation
-- slightly more complex than a flat shift but still "basic math."
Each transform is reversible by applying its inverse:
- If enc[i] = flag[i] + key, then flag[i] = enc[i] - key
- If enc[i] = flag[i] ^ key, then flag[i] = enc[i] ^ key (XOR is its own
inverse)
- If enc[i] = flag[i] + i, then flag[i] = enc[i] - i
────────────────────────────────────────────────────────────────────────────────
--[ Exploitation / Solution ]--
:: Step 1: Extract the encrypted data and cipher logic
Open the binary in Ghidra or strings it to find the encrypted flag data. Look
for:
- An array of bytes or characters (the encrypted flag)
- A loop with arithmetic operations
For example, if the source/decompilation looks like:
-- c --
char encrypted[] = { ... }; // encrypted flag bytes
for (int i = 0; i < len; i++) {
if (i % 2 == 0) {
encrypted[i] = flag[i] + 5;
} else {
encrypted[i] = flag[i] - 3;
}
}
:: Step 2: Reverse the cipher
The inverse of the above example would be:
-- python --
for i in range(len(encrypted)):
if i % 2 == 0:
flag[i] = encrypted[i] - 5
else:
flag[i] = encrypted[i] + 3
:: Step 3: Run the solve script
The solve script provides a generic framework that:
1. Reads the encrypted data (from the binary or as input).
2. Tries common cipher reversals (shift, XOR, position-dependent, alternating).
3. Checks if the result starts with picoCTF{ and ends with }.
If the challenge provides a binary, use strings or Ghidra to extract the
encrypted bytes first, then feed them to the script.
────────────────────────────────────────────────────────────────────────────────
--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Hidden Cipher 2 - picoCTF 2026
Category: Reverse Engineering | Points: 100
The flag is obfuscated using a simple mathematical cipher. This script
tries common reversals (constant shift, XOR, position-dependent shift,
alternating operations) to recover the picoCTF{...} flag.
Usage:
# If the encrypted bytes are hardcoded (edit ENCRYPTED_DATA below):
python3 solve.py
# If you want to provide encrypted bytes from a file:
python3 solve.py encrypted.bin
# If you have a C/binary and extracted the encrypted array as hex:
python3 solve.py --hex "70 6a 65 71 47 56 4a ..."
# If extracted as comma-separated decimal:
python3 solve.py --dec "112,106,101,113,67,86,74,..."
"""
import sys
import re
import os
# =========================================================================
# ENCRYPTED DATA -- paste the encrypted flag bytes here after extracting
# them from the challenge binary (using Ghidra, strings, or hexdump).
# Examples of format:
# As a byte string: b'\x70\x6a\x65\x71...'
# As a list of ints: [112, 106, 101, 113, ...]
# As a hex string: "706a6571..."
# =========================================================================
ENCRYPTED_DATA = None # <-- Replace with actual encrypted data from the challenge
# If you already know the cipher parameters, set them here:
# Otherwise the script will brute-force common patterns.
KNOWN_CIPHER = None # e.g., {"type": "alternating_add", "even_key": 5, "odd_key": -3}
# ---------------------------------------------------------------------------
# Cipher reversal functions
# ---------------------------------------------------------------------------
def reverse_constant_shift(data, shift):
"""Reverse: enc[i] = flag[i] + shift => flag[i] = enc[i] - shift"""
return bytes([(b - shift) % 256 for b in data])
def reverse_constant_xor(data, key):
"""Reverse: enc[i] = flag[i] ^ key => flag[i] = enc[i] ^ key"""
return bytes([b ^ key for b in data])
def reverse_position_add(data):
"""Reverse: enc[i] = flag[i] + i => flag[i] = enc[i] - i"""
return bytes([(b - i) % 256 for i, b in enumerate(data)])
def reverse_position_sub(data):
"""Reverse: enc[i] = flag[i] - i => flag[i] = enc[i] + i"""
return bytes([(b + i) % 256 for i, b in enumerate(data)])
def reverse_position_xor(data):
"""Reverse: enc[i] = flag[i] ^ i => flag[i] = enc[i] ^ i"""
return bytes([b ^ i for i, b in enumerate(data)])
def reverse_alternating_add(data, even_key, odd_key):
"""Reverse: enc[i] = flag[i] + even_key if i%2==0 else flag[i] + odd_key"""
result = []
for i, b in enumerate(data):
if i % 2 == 0:
result.append((b - even_key) % 256)
else:
result.append((b - odd_key) % 256)
return bytes(result)
def reverse_alternating_add_sub(data, add_val, sub_val):
"""Reverse: even positions had +add_val, odd positions had -sub_val applied."""
result = []
for i, b in enumerate(data):
if i % 2 == 0:
result.append((b - add_val) % 256)
else:
result.append((b + sub_val) % 256)
return bytes(result)
def reverse_key_add(data, key):
"""Reverse: enc[i] = flag[i] + key[i % len(key)]"""
result = []
for i, b in enumerate(data):
result.append((b - key[i % len(key)]) % 256)
return bytes(result)
def reverse_key_xor(data, key):
"""Reverse: enc[i] = flag[i] ^ key[i % len(key)]"""
return bytes([b ^ key[i % len(key)] for i, b in enumerate(data)])
def is_valid_flag(text):
"""Check if text looks like a valid picoCTF flag."""
return text.startswith('picoCTF{') and text.endswith('}') and all(
32 <= ord(c) <= 126 for c in text
)
def looks_printable(data):
"""Check if most bytes are printable ASCII."""
try:
text = data.decode('ascii')
return all(32 <= ord(c) <= 126 for c in text)
except Exception:
return False
# ---------------------------------------------------------------------------
# Brute-force all common cipher patterns
# ---------------------------------------------------------------------------
def try_all_ciphers(data):
"""Try all common cipher reversals and return any valid flags found."""
results = []
# 1. Constant shift (Caesar): try shifts 1-255
for shift in range(1, 256):
candidate = reverse_constant_shift(data, shift)
try:
text = candidate.decode('ascii')
if is_valid_flag(text):
results.append((f"Constant shift +{shift}", text))
except Exception:
pass
# 2. Constant XOR: try keys 1-255
for key in range(1, 256):
candidate = reverse_constant_xor(data, key)
try:
text = candidate.decode('ascii')
if is_valid_flag(text):
results.append((f"XOR key=0x{key:02x}", text))
except Exception:
pass
# 3. Position-dependent: enc[i] = flag[i] + i
candidate = reverse_position_add(data)
try:
text = candidate.decode('ascii')
if is_valid_flag(text):
results.append(("Position add (enc[i]=flag[i]+i)", text))
except Exception:
pass
# 4. Position-dependent: enc[i] = flag[i] - i
candidate = reverse_position_sub(data)
try:
text = candidate.decode('ascii')
if is_valid_flag(text):
results.append(("Position sub (enc[i]=flag[i]-i)", text))
except Exception:
pass
# 5. Position-dependent XOR: enc[i] = flag[i] ^ i
candidate = reverse_position_xor(data)
try:
text = candidate.decode('ascii')
if is_valid_flag(text):
results.append(("Position XOR (enc[i]=flag[i]^i)", text))
except Exception:
pass
# 6. Alternating add/sub: common in picoCTF reverse_cipher style
for add_val in range(1, 20):
for sub_val in range(1, 20):
candidate = reverse_alternating_add_sub(data, add_val, sub_val)
try:
text = candidate.decode('ascii')
if is_valid_flag(text):
results.append(
(f"Alternating +{add_val}/-{sub_val}", text)
)
except Exception:
pass
# 7. Alternating two different shifts
for even_key in range(-20, 21):
for odd_key in range(-20, 21):
if even_key == 0 and odd_key == 0:
continue
candidate = reverse_alternating_add(data, even_key, odd_key)
try:
text = candidate.decode('ascii')
if is_valid_flag(text):
results.append(
(f"Alternating even+{even_key}/odd+{odd_key}", text)
)
except Exception:
pass
# 8. Position + constant: enc[i] = flag[i] + i + c
for c in range(1, 50):
candidate = bytes([(b - i - c) % 256 for i, b in enumerate(data)])
try:
text = candidate.decode('ascii')
if is_valid_flag(text):
results.append((f"Position add + constant {c}", text))
except Exception:
pass
# 9. Multiply (affine): enc[i] = (flag[i] * a + b) mod 256
# Only try small values of a that have modular inverse
for a in range(1, 20, 2): # odd values have inverse mod 256
# Compute modular inverse of a mod 256
try:
a_inv = pow(a, -1, 256)
except ValueError:
continue
for b in range(0, 50):
candidate = bytes([(a_inv * (byte - b)) % 256 for byte in data])
try:
text = candidate.decode('ascii')
if is_valid_flag(text):
results.append((f"Affine a={a}, b={b}", text))
except Exception:
pass
return results
def parse_input_data(raw):
"""Parse encrypted data from various formats."""
raw = raw.strip()
# Try as raw bytes (if it came from a binary file)
if isinstance(raw, bytes):
return raw
# Try as hex string (e.g., "706a6571...")
try:
cleaned = re.sub(r'[\s,0x\\x]+', '', raw)
if re.fullmatch(r'[0-9a-fA-F]+', cleaned) and len(cleaned) % 2 == 0:
return bytes.fromhex(cleaned)
except Exception:
pass
# Try as comma-separated decimals (e.g., "112,106,101,113,...")
try:
parts = re.split(r'[\s,]+', raw)
nums = [int(p) for p in parts]
if all(0 <= n <= 255 for n in nums):
return bytes(nums)
except Exception:
pass
# Try as space-separated hex (e.g., "70 6a 65 71 ...")
try:
parts = raw.split()
if all(re.fullmatch(r'[0-9a-fA-F]{2}', p) for p in parts):
return bytes(int(p, 16) for p in parts)
except Exception:
pass
# Fall back to treating as raw ASCII bytes
return raw.encode('latin-1')
def main():
print("=" * 60)
print("Hidden Cipher 2 - picoCTF 2026")
print("Reverse the math cipher to recover the flag")
print("=" * 60)
data = None
# Handle command-line arguments
if len(sys.argv) > 1:
if sys.argv[1] == '--hex' and len(sys.argv) > 2:
data = parse_input_data(sys.argv[2])
elif sys.argv[1] == '--dec' and len(sys.argv) > 2:
data = parse_input_data(sys.argv[2])
elif os.path.exists(sys.argv[1]):
with open(sys.argv[1], 'rb') as f:
data = f.read()
print(f"[*] Read {len(data)} bytes from {sys.argv[1]}")
else:
data = parse_input_data(sys.argv[1])
# Use hardcoded data if available
if data is None and ENCRYPTED_DATA is not None:
if isinstance(ENCRYPTED_DATA, (list, tuple)):
data = bytes(ENCRYPTED_DATA)
elif isinstance(ENCRYPTED_DATA, str):
data = parse_input_data(ENCRYPTED_DATA)
else:
data = ENCRYPTED_DATA
# Interactive input
if data is None:
if not sys.stdin.isatty():
raw = sys.stdin.buffer.read()
data = raw
else:
print("\n[*] No encrypted data provided.")
print("[*] Paste the encrypted bytes (hex, decimal, or raw), then press Enter:")
try:
raw = input().strip()
data = parse_input_data(raw)
except EOFError:
print("[!] No input.")
sys.exit(1)
if not data:
print("[!] No data to analyze.")
sys.exit(1)
print(f"\n[*] Encrypted data ({len(data)} bytes):")
hex_preview = ' '.join(f'{b:02x}' for b in data[:64])
print(f" Hex: {hex_preview}{'...' if len(data) > 64 else ''}")
try:
ascii_preview = data[:64].decode('ascii', errors='replace')
print(f" ASCII: {ascii_preview}")
except Exception:
pass
# If a known cipher is set, use it directly
if KNOWN_CIPHER:
ct = KNOWN_CIPHER["type"]
if ct == "alternating_add":
result = reverse_alternating_add(data, KNOWN_CIPHER["even_key"],
KNOWN_CIPHER["odd_key"])
elif ct == "alternating_add_sub":
result = reverse_alternating_add_sub(data, KNOWN_CIPHER["add_val"],
KNOWN_CIPHER["sub_val"])
elif ct == "constant_shift":
result = reverse_constant_shift(data, KNOWN_CIPHER["shift"])
elif ct == "xor":
result = reverse_constant_xor(data, KNOWN_CIPHER["key"])
elif ct == "position_add":
result = reverse_position_add(data)
else:
print(f"[!] Unknown cipher type: {ct}")
sys.exit(1)
text = result.decode('ascii', errors='replace')
print(f"\n[+] Decrypted with known cipher ({ct}): {text}")
if is_valid_flag(text):
print(f"\n[+] FLAG: {text}")
return
# Brute-force all cipher types
print("\n[*] Trying all common cipher reversals...")
results = try_all_ciphers(data)
if results:
# Deduplicate
seen = set()
unique = []
for method, flag in results:
if flag not in seen:
seen.add(flag)
unique.append((method, flag))
print(f"\n[+] Found {len(unique)} valid flag(s):\n")
for method, flag in unique:
print(f" Method: {method}")
print(f" Flag: {flag}\n")
else:
print("\n[!] No valid flag found with standard cipher reversals.")
print("[*] The cipher might be more complex. Suggestions:")
print(" 1. Open the binary in Ghidra and look at the encryption function.")
print(" 2. Check if the first 8 bytes should decode to 'picoCTF{' --")
print(" this gives you known plaintext to derive the key.")
print(" 3. Known plaintext attack:")
known_plain = b'picoCTF{'
if len(data) >= len(known_plain):
print(f"\n[*] Known-plaintext analysis (first {len(known_plain)} bytes):")
print(f" Encrypted: {' '.join(f'{b:02x}' for b in data[:len(known_plain)])}")
print(f" Expected: {' '.join(f'{b:02x}' for b in known_plain)}")
diffs = [(e - p) % 256 for e, p in zip(data[:len(known_plain)], known_plain)]
xors = [e ^ p for e, p in zip(data[:len(known_plain)], known_plain)]
print(f" Differences (enc-plain mod 256): {diffs}")
print(f" XOR values: {xors}")
if len(set(diffs)) == 1:
print(f" -> Constant shift of {diffs[0]} detected!")
elif diffs == list(range(len(diffs))):
print(f" -> Position-dependent shift (enc[i] = flag[i] + i) detected!")
if len(set(xors)) == 1:
print(f" -> Constant XOR key 0x{xors[0]:02x} detected!")
sys.exit(1)
if __name__ == "__main__":
main()
────────────────────────────────────────────────────────────────────────────────
--[ Key Takeaways ]--
- Position-dependent and alternating ciphers (e.g. enc[i] = flag[i] + i, or
different ops on even/odd indices) are a step up from a flat shift but still
fully reversible.
- Cover the common families: constant shift, constant/positional XOR, positional
add/sub, alternating ops, and affine (a*x + b, only odd a is invertible mod
256).
- Use the picoCTF{ prefix as known plaintext: the byte-wise difference reveals a
constant shift, a range(0..n) pattern reveals a positional shift, and a constant
XOR reveals an XOR key.
- A brute-force solver that validates against the picoCTF{...} format quickly
identifies the right transform without fully reversing the binary.
- Tools used: Ghidra/IDA, strings, hexdump, and a multi-cipher Python
brute-forcer.