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:
3. Extracting additional data
Extracting that data is as easy as selecting the right channel. Here’s an example for StegOnline
:
Highlighted Hex (Accurate)
data, including all trailing zeros of the additional hex sequence and a few extra hex characters, should be enough:
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
1
s are black pixels and0
s 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:
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}