Post

scriptCTF 2025 - Forensics - Off By One

scriptCTF 2025 - Forensics - Off By One

Forensics - Off By One

46 solves out of 1190 teams.

Embargoed after the event.

1. Initial analysis

Scanning the QR code in the provided hidden.png gets us Rick-rolled.

2. Deeper visual analysis

For this step, different tools could be used:

Inspecting individual colour channels of hidden.png reveals additional data included at the top of the QR code in Blue 0 channel: Blue 0

3. Extracting additional data

Extracting that data is as easy as selecting the right channel. Here’s an example for StegOnline: Extracting Blue 0

Highlighted Hex (Accurate) data, including all trailing zeros of the additional hex sequence and a few extra hex characters, should be enough:

Hex data

4. Constructing new QR code

A Python script is needed to build the new QR code leading to the flag. The script:

  • Takes the extracted hex data and converts it to a binary string
  • Trims trailing binary characters until the total length of the binary string is a perfect square (as QR codes are square)
  • Creates a black-and-white image where 1s are black pixels and 0s are white pixels of the QR code
  • Scales up and displays the new QR code leading to the flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from PIL import Image
import math

# Extracted hex data with trailing zeros and some f-characters:
hex_data = "000000000000000fe423f8416cd042e9c0ba175ae5d0ba28ae841519043faaafe00010000fbedd507ce16a80a9763e120e6a20ffd7e6842356b025f148610054e20a38ffa800734683fb6ab610425100bad1fb85d4638c2eb1d5210519710feaa058000000000000007fff"

# Convert hex string to binary string:
data = bytes.fromhex(hex_data)
bin_str = ''.join(format(byte, '08b') for byte in data)

# Trim trailing binary characters by 1 until perfect square:
while True:
    length = len(bin_str)
    size = int(math.isqrt(length))
    if size * size == length:
        break
    bin_str = bin_str[:-1]
print(f"Final length: {len(bin_str)}, Square size: {size}x{size}")

# Build QR code, 1 is black and 0 is white:
img = Image.new("1", (size, size))
for i, bit in enumerate(bin_str):
    x = i % size
    y = i // size
    img.putpixel((x, y), 0 if bit == "1" else 1)

# Scale up for readability
img = img.resize((size*10, size*10), Image.NEAREST)
img.save("qr_flag.png")
img.show()

This results in the following image:

New QR

5. Getting the flag

Uploading the image to an online service or scanning it with an app gives us the following flag:

1
scriptCTF{qrqrqrc0d3s}
This post is licensed under CC BY 4.0 by the author.