Martin Rozariyo
Back to writeups

L3m0nCTF2025-Writeups

Challenge Overview: Lost Signal

Forensics/LostSignal/Readme.md

Challenge Overview: Lost Signal

Category: Forensics
Event: L3m0nCTF 2025
Role: Challenge Author

🛠️ Author Note
This challenge was authored by me for L3m0nCTF 2025.
The following explanation describes the intended forensic analysis path.

image

Intended Analysis Path

The challenge was designed to test:

  • interpretation of a provided seed as an ordering mechanism
  • rejection of traditional steganography tools in favor of programmatic analysis
  • pixel-level manipulation rather than file-level extraction
  • reconstruction of shuffled data using deterministic randomness
  • multi-bitplane extraction guided by a controlled permutation

Without applying the seed correctly, all extracted data appears as noise.

Evidence Provided

File: lost_signal.tar.gz

Clue:

Seed = 739391

No other hints.

Analysis Phase 1 — Interpreting the Seed

The challenge explicitly provides a seed.

A seed is not encryption by itself — it is used to:

  • Initialize a pseudo-random number generator
  • Produce a deterministic order

So from the question alone, the solver can infer:

The hidden data depends on a specific order, reproducible using the seed.

Since:

  • The file is an image
  • Metadata, strings, binwalk, StegSolve show nothing

The only reasonable conclusion is:

The seed defines an order of pixels, not bytes or files.

Analysis Phase 2 — Pixel-Level Analysis

StegSolve fails because:

  • Any meaningful structure has been shuffled
  • No spatially coherent pattern exists

So you must:

  • Load the image programmatically
  • Treat it as a pixel array

Analysis Phase 3 — Isolating the Luminance Channel

RGB mixes color and brightness.

For hidden data, brightness is the most likely carrier because:

  • Small brightness changes are visually invisible
  • Color changes are more noticeable

So convert the image to YCbCr and extract Y (luminance).

Code:

python3 - << 'EOF'
from PIL import Image
import numpy as np

img = Image.open("challenge_color_random.png").convert("YCbCr")
Y, Cb, Cr = img.split()

Y_arr = np.array(Y, dtype=np.uint8)
np.save("Y.npy", Y_arr)

print("Y channel extracted:", Y_arr.shape)
EOF

Analysis Phase 4 — Reconstructing the Permutation

The seed must recreate the exact pixel order used during embedding.

Code:

python3 - << 'EOF'
import numpy as np

seed = 739391
Y = np.load("Y.npy")
h, w = Y.shape

rng = np.random.RandomState(seed)
indices = np.arange(h * w)
rng.shuffle(indices)

np.save("perm.npy", indices)
print("Permutation generated")
EOF

Now indices represents the correct pixel visit order.

Analysis Phase 5 — Controlled Bitplane Extraction

A simple LSB dump fails, which means:

  • More than one bit is used
  • Bits are interleaved

The correct approach is:

  • Extract two least significant bits
  • Alternate between them
  • Follow the shuffled pixel order

Bitplanes used:

[0,1]

Code:

python3 - << 'EOF'
import numpy as np

Y = np.load("Y.npy")
perm = np.load("perm.npy")
h, w = Y.shape

LSBS = [0, 1]
total_bits = h * w
qr_flat = np.zeros(total_bits, dtype=np.uint8)

for i in range(total_bits):
    pix_idx = perm[i // len(LSBS)]
    bitplane = LSBS[i % len(LSBS)]

    y = pix_idx // w
    x = pix_idx % w

    qr_flat[i] = (Y[y, x] >> bitplane) & 1

np.save("qr_flat.npy", qr_flat)
print("Bitstream recovered")
EOF

Without:

  • the seed
  • the correct order
  • both bitplanes

the output is pure noise.

Final Output

The recovered bitstream must be reshaped back into an image.

Code

python3 - << 'EOF'
from PIL import Image
import numpy as np

qr_flat = np.load("qr_flat.npy")
Y = np.load("Y.npy")
h, w = Y.shape

qr = qr_flat.reshape((h, w))

# invert for proper QR colors
qr_img = (255 * (1 - qr)).astype(np.uint8)

Image.fromarray(qr_img).save("solved_qr.png")
print("QR image saved as solved_qr.png")
EOF

The QR will be generated.

Scanning the reconstructed QR code reveals the final flag.

image

Flag : L3m0nCTF{1nv1s1bl3_b1tpl4n3_x0r_qr}