SIGINT PUZZLE
Challenge Description
SIGINT PUZZLE PACKAGE - OPERATION NIGHTJAR. Three declassified document images are included. Each document image contains embedded key material.
The archive contains four files:
SIGINT_PUZZLE/
freq_logs.txt - Package header and analyst notes
SIGINT_LOG_1983_09_01.png - Frequency monitoring log [Document 1]
CALLSIGN_REGISTRY_PARTIAL.png - Callsign registry extract [Document 2]
ASSET_LOCATION_BRIEF_0912.png - Asset location brief [Document 3]
Reading freq_logs.txt
The package header is explicit about the structure:
ANALYST NOTES:
Each document image contains embedded key material related to the
encrypted burst transmissions captured on 2023-09-01. The key material
is fragmented across documents [1], [2], and [3] in that order.
Fragments must be extracted from the images and assembled in document
order to recover the decryption key. Standard signal processing
techniques apply.
And R. Okafor's note:
"I don't know how they keep getting this stuff in here. Looked at the images at every zoom level before I gave up and actually ran a script on them. Do not make my mistake."
The hint is direct: visual inspection won't work. You need a script.
Identifying the Technique: LSB Steganography
The images are PNGs of typewritten documents on aged paper, lots of nearly-uniform pixel values with small noise variations. This is classic LSB steganography territory. The noise in the background makes single-bit changes completely invisible to the eye.
LSB (Least Significant Bit) steganography hides data in the lowest bit of each pixel's color channel. A pixel with R value 11001011 becomes 11001010 or 11001011 depending on the hidden bit. The visual difference is a change of 1 in 255, imperceptible.
Tools that find this automatically:
# zsteg (ruby gem)
zsteg SIGINT_LOG_1983_09_01.png
# reports: b1,r,lsb,xy ... text: "REVBR..."
# stegsolve (Java)
# Open image → Analyse → Data Extract → R bit 0 → Extract
Or implement extraction directly.
Extraction
The embedding scheme: LSBs of the red channel, big-endian, prefixed with a 2-byte big-endian length field.
import struct
from PIL import Image
def lsb_extract(path: str) -> str:
img = Image.open(path).convert('RGB')
pixels = list(img.getdata())
# Read LSB of R channel for each pixel
bits = [(px[0] & 1) for px in pixels]
# First 16 bits = message length in bytes
length = 0
for b in bits[:16]:
length = (length << 1) | b
# Read length * 8 bits
msg_bits = bits[16:16 + length * 8]
# Convert bits to bytes
msg_bytes = bytearray()
for i in range(0, len(msg_bits), 8):
byte = 0
for b in msg_bits[i:i+8]:
byte = (byte << 1) | b
msg_bytes.append(byte)
return msg_bytes.decode('ascii')
Assembling the Flag
import base64
raw1 = lsb_extract('SIGINT_PUZZLE/SIGINT_LOG_1983_09_01.png')
raw2 = lsb_extract('SIGINT_PUZZLE/CALLSIGN_REGISTRY_PARTIAL.png')
raw3 = lsb_extract('SIGINT_PUZZLE/ASSET_LOCATION_BRIEF_0912.png')
print(raw1) # k1:REVBRFJPUHtzaWdpbnRf
print(raw2) # k2:b3NpbnRfYW5kX2FfY3Jp
print(raw3) # k3:c2lzX29mX2ZhaXRofQ==
# Strip the k{n}: prefix before concatenating
p1 = raw1.split(':', 1)[1]
p2 = raw2.split(':', 1)[1]
p3 = raw3.split(':', 1)[1]
flag = base64.b64decode(p1 + p2 + p3).decode()
print(flag) # DEADROP{sigint_osint_and_a_crisis_of_faith}
Document order [1], [2], [3] as stated in freq_logs.txt is the correct concatenation order.
The Lore
The three documents tell a coherent SIGINT story once you read them:
- Document 1 captures encrypted radio bursts from NW-ARRAY-7, recovers partial coordinates (48.1N, 17.1E) and notes operator callsign WX-RAVEN
- Document 2 shows a callsign registry: WX-RAVEN is PRIORITY/ACTIVE, identity redacted, handler is KOVACS
- Document 3 gives full coordinates (48.1486N, 17.1077E, Bratislava), confirms NIGHTINGALE asset and 72-hour extraction window
The "key fragments" framing is in-universe justification for why each image hides part of the flag, they're supposedly distributed key material for decrypting the radio bursts.
Key Takeaways
1. LSB steganography is invisible to the eye. A 1-bit change in an 8-bit value is a 0.4% brightness difference. Against noisy paper texture it is completely imperceptible. The only way to find it is to look at the bit plane directly.
2. Read the documentation. freq_logs.txt tells you the order and through Okafor's note, the method (don't look, run a script). This is a forensics challenge that rewards careful reading as much as technical skill.
3. zsteg is your friend. For any PNG forensics challenge, zsteg is the first tool to run. It tests every combination of bit plane, channel, and byte order and reports anything that looks like text. It would have found all three fragments in under a second.
4. Length-prefixed payloads are robust. The 2-byte length prefix means the extractor knows exactly how many bits to read and doesn't have to guess at null terminators or padding. If you're implementing your own LSB scheme, prefix your payload with its length.
Flag
DEADROP{sigint_osint_and_a_crisis_of_faith}