packet.bspak — Three Channels, Two Decoys, One Stereogram
Breaking a layered OSINT packet: binary-ASCII JSON, a QR code that isn't a QR code, and a Vocaroo spectrogram that hands you a Google Maps pin.
The brief
Interception units have pulled a high-value encrypted ledger packet from an automated cartel communication relay (PRX-EU-09). The syndicate uses layered obfuscation across visual, textual, and acoustic channels to coordinate their smuggling routes without raising red flags.
Break through the cartel’s cryptographic layers, trace the shipment from its origin point, verify the destination air corridor, and pinpoint the final ground drop zone.
The challenge — an OSINT Industries exercise, “Underground Shipment” — hands you a single 5,579-byte file, packet.bspak. Flag format: OSINT{origin-airport_target-airport,location_name}, all lowercase, spaces replaced with underscores, passive OSINT only.
That phrase “visual, textual, and acoustic” is not scene-setting. It is a checklist. Three channels, three objectives, three flag fields — one each. Once you notice the symmetry, it tells you when you are done and, more importantly, when you are not.
TL;DR
| Channel | Artifact | Technique | Objective | Value |
|---|---|---|---|---|
| Textual | packet.bspak | Binary ASCII → JSON | Origin point | haab |
| Visual | Imgur PNG | Autostereogram disparity map | Destination air corridor | eddf |
| Acoustic | Vocaroo MP3 | Spectrogram text | Ground drop zone | marley_spoon_warehouse |
Final flag: OSINT{haab_eddf,marley_spoon_warehouse}
Layer 0 — the container
file reports ASCII text with very long lines. A hexdump shows why:
00000000: 3031 3131 3130 3131 2030 3030 3031 3031 01111011 0000101
00000010: 3020 3030 3130 3030 3030 2030 3031 3030 0 00100000 00100
Space-separated 8-bit binary. 01111011 is {. It is JSON:
data = open("packet.bspak").read().split()
print("".join(chr(int(b, 2)) for b in data))
{
"tracker_session": { "uuid": "7f8b2c1a-...", "gateway": "PRX-EU-09", "status": "ACTIVE" },
"telemetry": { "packet_id": 884712, "feed": "AIS_VHF_PRIMARY", "freq_mhz": 162.025 },
"logistics_manifest": {
"departure_vector": "vzthe.pbz/n/0bFIqpX",
"origin_iata": "ADD",
"audio_archive": "15srKdWXE3HV",
"audio_host": "vocaroo",
"cargo_id": "CM-9942-X",
"seal_status": "INTACT",
"temp_c": 4.2
},
"diagnostics": { "firmware": "2.14.9", "cipher": "AES-256", "auth": "ACK" }
}
Three things matter: origin_iata (the textual answer, and a trap — see the ending), departure_vector (visual), audio_archive (acoustic). Everything else is flavour. AIS_VHF_PRIMARY at 162.025 MHz is a maritime AIS channel dropped into an air-freight manifest purely to waste your time.
Layer 1 — textual
departure_vector is ROT13:
import codecs
codecs.encode("vzthe.pbz/n/0bFIqpX", "rot13") # -> imgur.com/a/0oSVdcK
The album is private, so scraping is blocked, but Imgur’s public post API answers anonymously:
curl -s "https://api.imgur.com/post/v1/albums/0oSVdcK?client_id=546c25a59c58ad7&include=media"
title: "Nothing to see here"
image: "secretive qr.png" (1600x1200 PNG, 50,099 bytes)
description: "Only those with the eyes for it can read this"
Note both strings. They are honest.
Layer 2 — the visual channel, and how I got it wrong
The trap
Boost the contrast and you get something that looks exactly like a dense QR code: three finder patterns, a sea of modules. It decodes with nothing. Not zxing-cpp, not OpenCV, not after thresholding, rescaling, or grid-resampling.
Pixel analysis explains why. The image holds only eight distinct grey values, and all but eight pixels sit in four of them:
| Value | Count | What it is |
|---|---|---|
0 / 255 | 66,924 / 97,344 | The three finder patterns and their separators |
8 / 245 | 878,984 / 876,732 | Everything else — the low-contrast “data” |
Label the pure-black regions and you get exactly six components — three 182×182 px finder outlines and three 78×78 px cores. 182 px is 7 modules at 26 px each, textbook. And the black pixel count is suspiciously exact: a QR finder is 33 black modules, so 3 × 33 × 26² = 66,924, matching to the pixel.
So the finders are real. The geometry is not:
TL finder left edge: x = 60 TR finder right edge: x = 1539 -> QR width 1480 px
TL finder top edge: y = 60 BL finder bottom: y = 1139 -> QR height 1080 px
1480 / 26 = 56.92 modules 1080 / 26 = 41.54 modules
Neither is an integer, and they disagree with each other. A real QR is square with an integer module count. Uniform scaling can’t produce this — if the image were stretched, the finders would be stretched too, and they are perfectly square. The finder patterns are decoration pasted onto a noise field.

The finders are pixel-perfect, but the spans they define are 56.92 × 41.54 modules — no integer, no QR. Three real finder patterns pasted onto a noise field.
My false negative
I then tested whether the “noise” was really noise, and concluded it was:
reg = binary[300:900, 300:1300]
for lag in range(1, 20):
print(lag, (reg[:, :-lag] == reg[:, lag:]).mean())
# lag 1: 0.733 lag 2: 0.532 lag 3..19: ~0.500
Flat at 0.5 — indistinguishable from random. A local-density map at five window sizes showed no hidden picture, a bitstream scan in three reading orders found no file magic, and the PNG had only IHDR/IDAT/IEND. I wrote the image off as a decoy.
That call was wrong, and the reason is instructive: I only swept lags 1–19.
The actual mechanism
What forced the re-examination was not new evidence about the image — it was the scenario text. Three channels, three flag fields. The visual channel had produced no answer, so by the challenge’s own accounting it could not be a decoy. Something was still in there.
“Only those with the eyes for it can read this” plus a field of random dots is a random-dot autostereogram — a Magic Eye. Those work by repeating a pattern horizontally with a period of roughly 100–150 px, modulating the period to encode depth. My autocorrelation sweep stopped at 19, about six times too short to see it:
for lag in range(20, 400):
...
# same central window: peak at lag 130 (0.702), echo at the doubled lag (256: 0.614)
# full frame: peak at lag 122 — the period varies across the image,
# and that modulation IS the depth signal
There it is. Recover the depth by finding, per pixel, the horizontal shift that best matches the local neighbourhood:
import numpy as np
from PIL import Image
from scipy.ndimage import uniform_filter
a = np.array(Image.open("qr.png").convert("L"))
b = (a < 128).astype(np.float32)
H, W = b.shape
best = np.full((H, W), -1.0, np.float32)
bestd = np.zeros((H, W), np.float32)
for d in range(106, 140): # around the 122 px base period
m = (b[:, :-d] == b[:, d:]).astype(np.float32)
s = uniform_filter(m, size=(9, 9)) # smooth the match score
cur = np.full((H, W), -1.0, np.float32)
cur[:, :W - d] = s
upd = cur > best
best[upd], bestd[upd] = cur[upd], d
depth = ((bestd - bestd.min()) / np.ptp(bestd) * 255).astype(np.uint8)
Image.fromarray(depth).save("depth.png")
The disparity map renders two lines of text floating above a stepped-pyramid background:

The disparity map, histogram-equalized: 50.0476 / 8.5607 floating over the stepped pyramid. A coordinate pair was hiding in what autocorrelation at lag ≤19 called “random”.
Reverse-geocoded:
curl "https://nominatim.openstreetmap.org/reverse?lat=50.0476&lon=8.5607&format=jsonv2"
# -> Airportring, Flughafen, Süd, Frankfurt am Main, Hessen, 60549
Inside Frankfurt Airport, roughly 1.1 km north of the aerodrome centroid, by Terminal 1 and the Lufthansa base. An Overpass query at 200 m confirms Flughafen Frankfurt am Main | aerodrome | Fraport AG. That is the destination air corridor.
Layer 3 — acoustic
https://vocaroo.com/15srKdWXE3HV, 12.69 s, mono, 320 kbps. The media file lives at a predictable host:
curl -e "https://vocaroo.com/15srKdWXE3HV" \
"https://media1.vocaroo.com/mp3/15srKdWXE3HV" -o voc.mp3
Whisper transcribes it as [Music] — a second decoy. The payload is drawn into the spectrogram:
ffmpeg -i voc.mp3 -lavfi showspectrumpic=s=1200x600:legend=1 spec.png

The payload sits in the 4.4–8.8 kHz band: vKnHvCjAe6eTLYTz6. The music underneath is real audio — Whisper hears [Music] and nothing else.
Seventeen mixed-case alphanumerics. That length is a fingerprint. Vocaroo IDs are 12, Imgur albums 7, Pastebin 8, YouTube 11 — but maps.app.goo.gl short links are 17, and this is a logistics scenario. Probing a dozen hosts, everything 404s or returns an SPA shell except:
curl -sI "https://maps.app.goo.gl/vKnHvCjAe6eTLYTz6"
# 302 -> .../place/Marley+Spoon+Warehouse/@52.6033231,13.3375846,504m/...
# !3d52.6037205!4d13.3397674 !16s/g/11g01wyppn
The listing:
Marley Spoon Warehouse (category: Warehouse)
Wallenroder Strasse 7-9, BOS Gewerbehof Halle C, Ebene C1
13435 Berlin, Germany
52.6037205, 13.3397674 CID 0x2b9ba9b5e271f55f
A detail worth noticing: the redirect carries !15s decoding to the base64 string warehouse germany. That is the search context the author used when they shared the pin — a small confirmation you have landed on the intended place and not a neighbour.
Assembling the flag — the last trap
Three verified values: Addis Ababa Bole, Frankfurt Airport, Marley Spoon Warehouse. A coherent route — air freight Addis Ababa → Frankfurt, then overland to a Berlin industrial estate, which is why the drop zone and the arrival airport are in different cities.
The obvious submission fails:
OSINT{add_fra,marley_spoon_warehouse} rejected
So do add_ber and add_txl (attempts made before the stereogram cracked, when Berlin looked like the only candidate airport), and every casing and separator permutation.
The answer is ICAO, not IATA:
OSINT{haab_eddf,marley_spoon_warehouse}
HAAB = Addis Ababa Bole. EDDF = Frankfurt. The flag format says airport, never IATA — but the manifest hands you a field literally named origin_iata containing ADD, which establishes IATA as the apparent convention for the whole flag. Accepting that framing cost seven submissions. The handout taught me a convention it never actually committed to.
Catalogue of decoys
The challenge is built almost entirely out of plausible wrong turns:
- The QR code. Genuine finder patterns, pixel-exact, on a field that is not a QR. Burns hours in QR decoders.
- The speech track. Real audio under the spectrogram text, transcribing to nothing.
origin_iata. Correct airport, wrong coding system.AIS_VHF_PRIMARY/162.025. A maritime frequency in an air-freight manifest.cipher: AES-256,uuid,packet_id,cargo_id. No crypto anywhere in the challenge.- Album title “Nothing to see here.” Simultaneously a taunt and, for the QR layer specifically, the literal truth.
What I would do differently
- Sweep autocorrelation to several hundred pixels before calling image data random. Stopping at lag 19 produced a confident, wrong “this is noise” verdict. A peak anywhere near 80–150 px means autostereogram, and the disparity map is the message.
- Treat an explicit channel enumeration as a completeness checklist. “Visual, textual, and acoustic” was the strongest evidence in the whole challenge, and it was in the first paragraph. When the visual channel had yielded nothing, that was proof of an unsolved layer — not proof of a decoy.
- When a field says “airport”, try ICAO before IATA. Especially when the handout volunteers an IATA code; that generosity is the misdirection.
- Statistical absence of evidence is weak evidence of absence. Every negative result I had about the image was correct as measured and useless as interpreted.
Full solver
#!/usr/bin/env python3
"""packet.bspak — full chain."""
import codecs, json, subprocess
import numpy as np
from PIL import Image
from scipy.ndimage import uniform_filter
# --- textual -------------------------------------------------------------
raw = open("packet.bspak").read().split()
doc = json.loads("".join(chr(int(b, 2)) for b in raw))
man = doc["logistics_manifest"]
print("origin IATA :", man["origin_iata"]) # ADD -> HAAB
print("imgur :", codecs.encode(man["departure_vector"], "rot13"))
# --- visual: autostereogram ---------------------------------------------
a = np.array(Image.open("qr.png").convert("L"))
b = (a < 128).astype(np.float32)
H, W = b.shape
period = max(range(20, 400), key=lambda d: (b[:, :-d] == b[:, d:]).mean())
print("stereogram period:", period, "px") # 122
best = np.full((H, W), -1.0, np.float32)
bestd = np.zeros((H, W), np.float32)
for d in range(period - 16, period + 18):
s = uniform_filter((b[:, :-d] == b[:, d:]).astype(np.float32), size=(9, 9))
cur = np.full((H, W), -1.0, np.float32); cur[:, :W - d] = s
upd = cur > best; best[upd], bestd[upd] = cur[upd], d
Image.fromarray(((bestd - bestd.min()) / np.ptp(bestd) * 255).astype(np.uint8)) \
.save("depth.png") # 50.0476 / 8.5607 -> EDDF
# --- acoustic: spectrogram ----------------------------------------------
subprocess.run(["ffmpeg", "-y", "-i", "voc.mp3", "-lavfi",
"showspectrumpic=s=1200x600:legend=1", "spec.png"])
# reads: vKnHvCjAe6eTLYTz6 -> https://maps.app.goo.gl/vKnHvCjAe6eTLYTz6
print("FLAG: OSINT{haab_eddf,marley_spoon_warehouse}")
Sources
- Challenge: OSINT Industries — “Underground Shipment” exercise
- Imgur post API (anonymous album metadata): api.imgur.com/post/v1/albums/0oSVdcK
- The stereogram image: i.imgur.com/bUvzuk0.png
- The audio: vocaroo.com/15srKdWXE3HV (direct media: media1.vocaroo.com/mp3/15srKdWXE3HV)
- Reverse geocoding: Nominatim
/reverseat 50.0476, 8.5607 - The drop zone: maps.app.goo.gl/vKnHvCjAe6eTLYTz6
- ICAO codes: HAAB — Addis Ababa Bole · EDDF — Frankfurt
Everything above is passive: local file analysis, anonymous public API reads, HTTP redirects, and OpenStreetMap reverse geocoding. No accounts, no authentication, no contact with any person or place.