TJCTF 2026: Polaroid
__TEXT.__const blob (6324 B) with repeating keystream โ PNG recoveredChallenge Overview
The challenge provides a macOS ARM64 Mach-O binary named polaroid. When executed, it prompts the user for a password via scanf. If the password is correct, the binary performs an XOR decryption on an embedded data blob and writes the result to disk as a PNG image. This image, however, is horizontally mirrored โ like a photographic negative viewed through a mirror. The challenge name "Polaroid" and the password itself both hint at the photographic development process: expose the negative, then develop the picture.
The solve path involves three distinct stages: first, extracting the XOR key from the binary's password verification routine through static analysis of the ARM64 disassembly; second, decrypting the embedded ciphertext using the recovered keystream to produce a valid PNG file; and third, correcting the mirrored image with a horizontal pixel flip to reveal the flag. Each stage combines a different skill set โ reverse engineering, cryptography, and image forensics โ making this a well-rounded challenge that rewards methodical analysis over brute force.
A Polaroid camera exposes film to light, then develops the negative into a positive image. The binary's password "exposeTheNegative" is both the XOR key and a direct instruction: expose (decrypt) the negative (ciphertext), then develop (flip) the picture (PNG).
Binary Reconnaissance
File Identification
Initial triage confirms the target is a 64-bit ARM64 Mach-O executable for macOS. The file command output shows it's a standalone binary with no dynamic library dependencies beyond system frameworks:
$ file polaroid
polaroid: Mach-O 64-bit executable arm64
$ otool -L polaroid
polaroid:
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0)
$ otool -l polaroid | grep -A5 __const
Section
sectname __const
segname __TEXT
addr 0x0000000100003820
size 0x00000000000018b4 (6324 bytes)
offset 14368
align 2^3 (8)
The __TEXT.__const section is immediately suspicious. It contains exactly 6324 bytes (0x18b4) of static data โ far too large for simple configuration constants. In the context of a CTF challenge that writes a PNG to disk, this is almost certainly the encrypted image payload. The section resides in the read-only __TEXT segment, which means the binary loads this data at a fixed virtual address and never modifies it at runtime. The decryption must happen in a separate buffer.
Strings and Entropy
A quick strings pass reveals no obvious password, no PNG magic bytes (\x89PNG), and no base64-encoded data. The high entropy of the __TEXT.__const section confirms it's encrypted โ a raw PNG would have long zero runs in its IHDR and IDAT padding, but the blob shows no such patterns. Running binwalk against the binary finds no embedded file signatures either. Whatever the decryption algorithm is, it transforms the entire blob into something unrecognizable by signature scanners.
Keystream Extraction from Disassembly
The Password Check Routine
The most critical discovery comes from disassembling the password verification function. Rather than using a standard library comparison like strcmp() or computing a hash and comparing digests, the binary performs byte-by-byte immediate comparisons against ARM64 registers. Each character of the input is loaded with ldrb and compared against a hardcoded immediate value with cmp:
; Password verification โ byte-by-byte immediate compares
ldrb w8, [x0] ; load input[0]
cmp w8, #0x65 ; 'e'
b.ne fail_exit
ldrb w8, [x0, #1] ; load input[1]
cmp w8, #0x78 ; 'x'
b.ne fail_exit
ldrb w8, [x0, #2] ; load input[2]
cmp w8, #0x70 ; 'p'
b.ne fail_exit
ldrb w8, [x0, #3] ; load input[3]
cmp w8, #0x6f ; 'o'
b.ne fail_exit
; ... continues for all 17 characters ...
Extracting the immediate operands (#0x65, #0x78, #0x70, #0x6f, ...) in sequence and converting them from hex to ASCII yields the complete password: exposeTheNegative. This is a 17-byte string that serves double duty as both the access password and the XOR decryption key.
Compiler optimizations often inline small string comparisons as a series of immediate loads and compares, especially when the string is short and known at compile time. The developer likely wrote something equivalent to if (strcmp(input, "exposeTheNegative") == 0), and the compiler unfolded it into individual byte checks. This is a gift for reverse engineers โ no need to trace pointer chains or resolve indirect references.
XOR Cryptanalysis
The Decryption Routine
After the password check succeeds, the binary enters its decryption phase. It allocates a buffer, iterates over the 6324 bytes of __TEXT.__const, and XORs each byte with the corresponding byte of the password, cycling through the 17-byte key indefinitely:
// Decompiled decryption logic
for (int i = 0; i < 6324; i++) {
decrypted[i] = const_blob[i] ^ password[i % 17];
}
write_file("output.png", decrypted, 6324);
This is a classic repeating-key XOR cipher with a period of 17 bytes. The critical weakness is that the plaintext has a known structure โ PNG files always begin with the 8-byte magic signature \x89PNG\r\n\x1a\n. Even without recovering the key from the disassembly, a known-plaintext attack on the first 8 bytes would immediately reveal 8 of the 17 key bytes. The remaining 9 bytes could then be brute-forced or inferred from the PNG IHDR chunk structure that follows the signature.
Known-Plaintext Attack (Alternative Path)
For the sake of completeness, here's how the attack would work without the key. XOR the first 8 bytes of __TEXT.__const with the known PNG header to recover 8 keystream bytes:
# Known-plaintext attack on PNG header
png_header = b'\x89PNG\r\n\x1a\n'
# const_first_8 = first 8 bytes of __TEXT.__const
recovered_key_fragment = bytes(c ^ p for c, p in zip(const_first_8, png_header))
# Result: b'exposeTh' (8 of 17 bytes recovered)
The recovered fragment exposeTh strongly suggests a camelCase English phrase. The full key exposeTheNegative could be guessed or reconstructed from context. However, since we already have the complete key from the disassembly, we proceed directly to decryption.
Using a short repeating XOR key against a file format with a fixed magic header is a textbook cryptographic error. The PNG signature alone leaks nearly half the key. A better approach would use a proper stream cipher (ChaCha20, AES-CTR) or at minimum a key derivation function to expand the password into a longer, less predictable keystream.
Image Forensics: Horizontal Flip
The Mirrored Output
After XOR decryption, the resulting file is a valid PNG โ it opens without errors, has correct IHDR and IDAT chunks, and contains visible text. However, the text is horizontally mirrored, as if viewed through a mirror or the "negative" of a photograph. All letters appear reversed left-to-right, making the flag unreadable in its current form.
This is consistent with the challenge's theme. A Polaroid camera produces a mirror image on the negative film, which is then flipped during the development process to produce the final positive print. The binary skips this "development" step โ it only exposes the negative, not develops it. We must complete the process ourselves.
Correcting the Pixel Matrix
The fix is a simple horizontal flip of the pixel matrix. Using Python with PIL/Pillow, the transpose(Image.FLIP_LEFT_RIGHT) method reverses each row of pixels, restoring the image to its correct orientation:
from PIL import Image
with Image.open("flag_mirrored.png") as img:
corrected = img.transpose(Image.FLIP_LEFT_RIGHT)
corrected.save("flag_final.png")
With the image properly oriented, the flag is clearly visible in the rendered photograph.
tjctf{develop_the_picture}
Exploit Code
#!/usr/bin/env python3
"""
TJCTF 2026 - Polaroid
ARM64 Mach-O XOR decryption + horizontal pixel flip
Author: QA210
"""
import struct
import sys
from pathlib import Path
try:
from PIL import Image
except ImportError:
sys.exit("[-] pip install Pillow")
try:
import lief
except ImportError:
sys.exit("[-] pip install lief")
# XOR key recovered from ARM64 immediate compare operands
CIPHER_KEY = b"exposeTheNegative"
KEY_PERIOD = len(CIPHER_KEY) # 17
# PNG magic header for validation
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
def extract_const_section(binary_path):
"""
Parse the Mach-O binary and extract the __TEXT.__const section
which holds the 6324-byte XOR-encrypted PNG payload.
"""
binary = lief.MachO.parse(binary_path)
if not binary:
sys.exit(f"[-] Failed to parse Mach-O: {binary_path}")
macho = binary.at(0)
for section in macho.sections:
if section.name == "__const" and section.segment.name == "__TEXT":
data = section.content
print(f"[+] __TEXT.__const: {len(data)} bytes at offset 0x{section.offset:x}")
return bytes(data)
sys.exit("[-] __TEXT.__const section not found")
def repeating_xor_decrypt(ciphertext, keystream):
"""
Decrypt using repeating XOR keystream.
Each plaintext byte = ciphertext byte XOR keystream[i % key_length].
"""
klen = len(keystream)
plain = bytearray(len(ciphertext))
for i in range(len(ciphertext)):
plain[i] = ciphertext[i] ^ keystream[i % klen]
return bytes(plain)
def horizontal_flip(input_png, output_png):
"""
Fix the mirrored image by flipping all rows left-to-right.
This simulates the 'development' step from negative to positive.
"""
with Image.open(input_png) as img:
corrected = img.transpose(Image.FLIP_LEFT_RIGHT)
corrected.save(output_png)
print(f"[+] Flipped image saved: {output_png}")
def main():
if len(sys.argv) < 2:
sys.exit(f"Usage: {sys.argv[0]} ")
binary_path = sys.argv[1]
# Stage 1: Extract encrypted blob from Mach-O
print("[*] Stage 1: Extracting __TEXT.__const from binary...")
encrypted_blob = extract_const_section(binary_path)
print(f"[*] Blob size: {len(encrypted_blob)} bytes")
# Stage 2: XOR decrypt with repeating keystream
print(f"[*] Stage 2: Decrypting with key '{CIPHER_KEY.decode()}' ({KEY_PERIOD} bytes)...")
decrypted = repeating_xor_decrypt(encrypted_blob, CIPHER_KEY)
if decrypted[:8] != PNG_SIGNATURE:
sys.exit("[-] Decrypted data does not start with PNG signature. Wrong key?")
mirrored_path = "polaroid_mirrored.png"
with open(mirrored_path, "wb") as f:
f.write(decrypted)
print(f"[+] Valid PNG written: {mirrored_path}")
# Stage 3: Horizontal flip to fix mirror image
print("[*] Stage 3: Flipping pixel matrix (negative โ positive)...")
final_path = "polaroid_flag.png"
horizontal_flip(mirrored_path, final_path)
print(f"[+] Done! Open {final_path} to read the flag.")
if __name__ == "__main__":
main()
Running the Exploit
$ python3 solve.py polaroid
[*] Stage 1: Extracting __TEXT.__const from binary...
[+] __TEXT.__const: 6324 bytes at offset 0x3820
[*] Blob size: 6324 bytes
[*] Stage 2: Decrypting with key 'exposeTheNegative' (17 bytes)...
[+] Valid PNG written: polaroid_mirrored.png
[*] Stage 3: Flipping pixel matrix (negative -> positive)...
[+] Flipped image saved: polaroid_flag.png
[+] Done! Open polaroid_flag.png to read the flag.
$ open polaroid_flag.png
# Image shows: tjctf{develop_the_picture}
Alternative Approach: Without Disassembly
Known-Plaintext Attack Path
If the binary's password check were implemented differently (e.g., using a hash comparison that doesn't leak the key), the challenge would still be solvable via cryptanalysis alone. The repeating XOR cipher with a 17-byte key encrypting a PNG file provides enough structural constraints to recover the keystream without any reverse engineering:
- PNG header (bytes 0-7): XORing the first 8 ciphertext bytes with the known PNG signature
\x89PNG\r\n\x1a\nrecovers keystream bytes 0-7. - IHDR chunk (bytes 8-27): The IHDR chunk has a fixed structure โ 4-byte length, 4-byte type (
IHDR), 4-byte width, 4-byte height, 5 bytes of bit depth/color/filter/interlace, and 4-byte CRC. This provides 20 more known plaintext bytes, of which bytes at positions 8-24 modulo 17 overlap with the first 8 recovered key bytes, confirming the key period. - Key reconstruction: The 8 recovered bytes (
exposeTh) strongly suggest an English camelCase phrase. The full keyexposeTheNegativecan be guessed from the challenge's photographic theme.
This alternative path demonstrates that the vulnerability is not just in the password check implementation but fundamentally in the choice of a short repeating XOR key applied to a structured file format.