Not here any more — A Payload No Scanner Can See
zsteg, Aletheia and five statistical detectors all called this file clean. The ZIP was sitting at offset 0 the whole time — hidden by splitting one bitstream across three different bitplanes.
One image of an Amazon Prime van. Flag format Hacktoria{The_Sign_You_Found}.
It took roughly two days, produced five wrong submissions, and at one point I had a working proof that the file contained no hidden data. That proof was correct. It was also useless, and understanding why is the actual content of this post.
The prompt
This pristine delivery vehicle was once the role model of its fleet; now you can see it parked at “Not here any more”, but there is something hidden for you to find.

Hints were added over the following two days:
| # | Hint |
|---|---|
| 1 | #000100 — later amended: “in RGB values it’s R0, G1, B0” |
| 2 | The hidden cargo will guide you — later: “the hidden cargo is a file” |
| 3 | 49 74 20 69 73 20 61 20 73 74 65 67... → “It is a steganography challenge. Oh and keep this tool open, you might need it ;)“ |
| 4 | To open and reveal the hidden cargo, look at the van and hint No.1 — later: “the file is password protected and the contents are encoded” |
| 5 | I hid it a bit deeper than standard scanners can reach |
The amendments matter. Everything before them sent the entire channel into the same dead end.
Part 1: The location (and the one-step solution I missed)
The phrase "Not here any more" is in quotation marks in the prompt. It is a place name.
I did not treat it as one. Instead I spent hours on:
Container forensics. Chunk enumeration, trailing data, ancillary chunks. Clean — IHDR, 302× IDAT, IEND, and the parser ends at byte 2,474,643, exactly the file size. Zero slack.
Reverse image search. This cannot work on Street View screenshots, and it’s worth knowing why: panorama tiles are not indexed by content. Reverse search matches landmarks, hotels, anything someone uploaded to a website. An anonymous house on a British cul-de-sac is structurally invisible to it. Google Lens returned 200 pictures of wooden windows, weighted to my own locale.
OpenStreetMap. Nominatim across GB/IE for the phrase, then an Overpass regex on addr:housename over all of Great Britain. Zero results.
The actual solution: type Not here any more into Google Maps. Direct hit. A user-generated POI in Northolt Village, London Borough of Ealing, category “training centre”, one review, one star.
User-generated Maps POIs are in neither the web index nor OSM. That is precisely why the search tools came back empty while the answer sat one input field away.
The Street View preview attached to that POI:
51.5444607, -0.3714258
pano: -ptXXrihRgTp3Z80pNI6Og
captured June 2024
Same van. Same scratches. Same dent above the wheel arch.

Dead end: Companies House
Before the hints were amended, “there is something hidden” read like “something else is still there.” So: 2 Church Road, Northolt UB5 5AJ is a registered office address. Two companies — one that moved out in September 2023, one still registered there.
Elegant chain. Completely wrong. Worth documenting because it looked right, and because the corrected hint text (“something hidden for you to find”) ruled it out retroactively.
Part 2: Proving a file is clean, incorrectly
Hint 3 said steganography. So I went after the pixels.
Manual extraction. All 8 bitplanes × 4 channels × row/column × MSB/LSB — 128 variants, full stream length, searching for Hacktoria, archive magics, long printable runs. Channel groups R, G, B, RG, RB, GB, RGB, BGR ranked by printable-ASCII ratio. Best candidate: 48%. That is noise.
zsteg. Full -a. Then -a -P (prime-indexed pixels — the van says PRIME, “pristine”, “role model of its fleet”, it fit beautifully). Then the isolated bitmask -b 000100 with and without prime, since -a only iterates b1..b8 and never tests an isolated mask. 451 prime variants. Nothing.
Other tools. Built stegano-cli from source — its unveil panics in the length-header decoder, and its encoder fingerprint doesn’t match (one IDAT chunk, zlib 78 01, filter 0; the challenge file has 302 chunks of 8192, zlib 78 5e, filter Sub). Python stegano with all eleven generators including eratosthenes: “Impossible to detect message.”
Statistical steganalysis. This is where I got confident:
| Test | Detects | Result |
|---|---|---|
| SPA (Aletheia) | LSB replacement | ”No hidden data found” |
| RS analysis | LSB replacement | divergence 0.0012 / 0.0025 / 0.0011 |
| Weighted Stego | embedding rate | β = 0.006 / 0.003 / 0.008 |
| Chi² (Westfeld) | sequential LSB fill | χ²/df = 3.21 / 3.00 / 3.70 |
| HCF centre of mass | LSB matching (±1) | ratio 0.951–0.981 |
Five independent detectors, three channels, all negative. Bit-0 compression ratios identical across R/G/B (0.9958 / 0.9959 / 0.9958). Per-block chi² localisation found no anomalous region.
I concluded the file was clean.
Why that conclusion was wrong
The payload is a 410-byte ZIP. That’s 3,280 bits distributed across 5.4 million LSBs — 0.06%.
None of those five tests can see that. They measure whether many LSBs deviate from natural image statistics. At 0.06% the signal is far below the noise floor of every one of them. My proof was mathematically sound and answered a question nobody had asked: “is this image saturated with embedded data?” No. “Does it contain a small file?” Yes.
I had actually noted this caveat mid-analysis and then drifted back to the strong claim. That’s the failure mode worth naming: a rigorous negative result on the wrong question feels exactly like a rigorous negative result.
Part 3: The extraction
#000100 has two readings.
Reading A (mine): a channel/bit specifier. R=00, G=01, B=00 → “green channel, bit 0.” This is how CyberChef’s Extract LSB is configured, so it feels native. It is a dead end.
Reading B (correct): a per-channel bit index, positionally.
#00 01 00
R G B
↓ ↓ ↓
0 1 0 ← bit index for that channel
Take bit 0 from red, bit 1 from green, bit 0 from blue. Three bits per pixel, row-major, packed MSB-first.
from PIL import Image
import numpy as np
a = np.array(Image.open('chall.png').convert('RGB'))
r = (a[:,:,0] ) & 1
g = (a[:,:,1] >> 1) & 1
b = (a[:,:,2] ) & 1
bits = np.stack([r, g, b], axis=-1).reshape(-1)
data = np.packbits(bits).tobytes()
print(data[:4].hex()) # 504b0304
print(data.find(b'PK\x05\x06')) # 388
PK\x03\x04 at offset 0. Not buried, not offset, not obfuscated. Offset zero, first four bytes.
Why no tool finds this
This is the genuinely interesting part, and it’s the thing the challenge author himself asked about afterwards.
It is not a row-vs-column problem. zsteg tests both, plus all channel orderings. The gap is that no mainstream tool supports a different bit index per channel:
zsteg -aiteratesb1..b8as one shared bit depth across all selected channels.b1,rgbmeans bit 0 from R and G and B. R0/G1/B0 is not expressible in its parameter space at all.- CyberChef’s Extract LSB has a single bit-index field for all selected channels.
- StegOnline and stegsolve display bitplanes individually but don’t combine them per-channel.
And each channel viewed alone looks like pure noise, because two-thirds of the bitstream is missing. That’s why every single-channel test — mine and everyone else’s — came back empty.
What a tool would need is -b R0,G1,B0. Nothing implements it. Five lines of numpy do.
The ZIP
something_else_is_here.txt 2772 bytes compress_type=99 (AES)
Password: amazon. From hint 4 — “look at the van.” The van says prime on the side, but the smile logo is Amazon’s. (prime does not work.)
import pyzipper
with pyzipper.AESZipFile('cargo.zip') as z:
z.setpassword(b'amazon')
payload = z.read('something_else_is_here.txt')
Part 4: Two encoding layers and a cipher
Contents:
00110001 00110010 00110010 00100000 ...
From Binary → 122 146 142 165 40 161 144 157 ... → octal.
From Octal →
Rfbu qdo befwo ylpn, yiny ev br lnd bswkxbod lkwro lve wfo apyjp. Zzgs zm dfk pao?
A polyalphabetic cipher. 63 letters.
Everything statistical fails here, and the failure is instructive: index of coincidence, chi-squared column solving, hill-climbing over key lengths 2–12, exhaustive search over all keys of length 1–4, Beaufort, variant Beaufort, autokey, and ~90 thematic keys. Nothing converges. 63 letters is simply not enough material.
Cribbing works. Structure first:
Zzgs zm dfk pao?— pattern 4-2-3-3, sentence-final question mark. Strong candidate:____ is the ___?yiny— same letter at positions 1 and 4. Fitsthat.- Word-length skeleton:
4 3 5 4 | 4 2 2 3 8 5 3 3 5 | 4 2 3 3
Which resolves to:
Take the first left, then go to the furthest point you can reach. What do you see?
Verify by deriving the keystream from key = cipher − plain (mod 26):
yfrqxwkwwoevnhkufbjlyhidsgzw
yfrqxwkwwoevnhkufbjlyhidsgzw
yfrqxwk
Exactly periodic at 28 — two complete blocks plus a partial repeat. A wrong plaintext does not produce a cleanly periodic keystream. That’s the proof, not the guess.
The period was the hint
I treated 28 as an arbitrary key length and moved on. It isn’t arbitrary. 28 = 4 × 7, and a period that factors into two small numbers is the signature of layered Vigenère.
The intended solve was two passes with two keys taken straight off the vehicle:
plain = vigenere_decode(vigenere_decode(cipher, "ford"), "transit")
ford (4) and transit (7), lcm = 28. Order doesn’t matter — the shifts add, and addition commutes. The combined keystream is exactly the one recovered above:
ford → f o r d f o r d f o r d ...
transit → t r a n s i t t r a n s ...
sum → yfrqxwkwwoevnhkufbjlyhidsgzw ✓ identical
The van is a Ford Transit. The make is on the rear doors, the model badge below the handle. Both words were visible in the challenge image the entire time.
So: not a design gap, just a layer none of us saw. The lesson is mechanical — when a derived keystream repeats at a composite period, factor it and ask which words of those lengths appear in the source material.
Part 5: Rebuilding the panorama
One suggestion in the channel was to diff the challenge image against the original Street View imagery to localise the manipulation. Somebody tried and gave up — “hard because of 360.”
It’s solvable, and the technique is reusable, so here it is.
Fetching tiles. The modern streetviewpixels-pa.googleapis.com endpoint returns 403 without a key. The legacy endpoint still serves:
https://geo0.ggpht.com/cbk?cb_client=maps_sv.tactile&authuser=0&hl=en
&panoid=<PANO_ID>&output=tile&x=<X>&y=<Y>&zoom=<Z>&nbt&fover=2
At zoom 4 that’s 128 tiles of 512×512 → an 8192×4096 equirectangular panorama.
Finding the view. The heading/pitch/fov in the Maps URL did not reproduce the challenge crop — the URL uses a different convention. Rather than reverse-engineering it, search for the parameters by cross-correlation against the challenge image:
heading 236.0°, pitch −12.0°, fov 57.0°
Reproject to 1852×978, then refine with ECC alignment: correlation 0.9977. Pixel-accurate reconstruction of the exact frame.
What the diff showed: nothing useful. Median per-block difference 7.19, and every outlier sits at the frame edges (alignment artefacts). This is expected — the challenge image is a browser screenshot of the panorama, resampled and re-encoded, while the tiles are JPEG. That difference is orders of magnitude larger than any LSB change. The method finds replaced regions; it cannot find bit manipulation.
Useful technique, wrong problem. Worth having in the toolbox anyway.
Part 6: The sign
The decrypted instruction, from the van’s position:
Take the first left, then go to the furthest point you can reach. What do you see?
Vicarage Close is the first left off Church Road, and it’s a cul-de-sac.

Click through to the last reachable panorama at the southern end:

Turn around, and there’s a sign on a fence:

Hacktoria{Private_Car_Park}
What I’d take from this
A negative result is only as good as the question it answers. Five detectors agreeing on “no substantial embedding” is not the same as “no embedding.” I knew the caveat, wrote it down once, and then let the strong version stand. When your evidence is statistical, state the detection threshold alongside the conclusion or the conclusion will outgrow it.
Ambiguous hints are worse than sparse hints. #000100 has two natural readings by people who already know the tooling. The wrong one leads to a plausible, testable, fully dead branch. The corrected hint — “in RGB values it’s R0, G1, B0” — turns the challenge into a five-minute exercise. It arrived on day three.
Tool coverage is not proof of absence. zsteg’s parameter space is large enough that “I ran -a” feels exhaustive. It isn’t. Knowing what a tool cannot express is worth more than knowing what it can.
Composite periods factor for a reason. A 28-character keystream isn’t a 28-character key — it’s two keys of length 4 and 7. I noted the period as “odd” and skipped past the one operation that would have cracked it open.
Try the obvious thing first. The place name was in quotation marks in the prompt. Google Maps. One field. Everything else in Part 1 was avoidable.
Sources
- Google Maps — the one-field solve: the user-generated “Not here any more” POI in Northolt, plus the attached Street View preview
- Google Street View — pano
-ptXXrihRgTp3Z80pNI6Og, June 2024, and the cul-de-sac walk to the sign - Google Lens — the reverse-image dead end: 200 wooden windows, zero panoramas
- OpenStreetMap via Nominatim and the Overpass API — the
addr:housenamesweep over Great Britain that returned zero, because Maps POIs aren’t in OSM - Companies House — the registered-office chain at 2 Church Road; elegant, wrong
- zsteg —
-a,-a -P,-b 000100; R0/G1/B0 is outside its parameter space - CyberChef — Extract LSB (single bit-index field), From Binary, From Octal
- StegOnline and Stegsolve — bitplane viewers that display planes but never combine them per-channel (Stegsolve link is the community mirror; caesum.com is offline)
- stegano-rs — the Rust
stegano-cliwhoseunveilpanics and whose encoder fingerprint ruled it out - Stegano (Python) — all eleven generators including
eratosthenes: “Impossible to detect message” - Aletheia — SPA and friends; its REFERENCES.md is the paper trail for SPA, RS and weighted stego
- Westfeld & Pfitzmann, “Attacks on Steganographic Systems” — the chi-squared test in the five-detector battery
- Pillow and NumPy — the five lines that do what no tool implements
- pyzipper — reading the AES-encrypted ZIP (
compress_type=99) - OpenCV
findTransformECC— the ECC alignment behind the 0.9977-correlation panorama reconstruction - Challenge: Hacktoria — “Not here any more”, flag format
Hacktoria{The_Sign_You_Found}