Hackvent 2024: Day 01
A GIF palette challenge where randomizing the global color table turns the indexed image into a readable QR code.
TL;DR
The challenge provides a GIF showing a QR code holding the string Come and have a closer look:). The black and white pixels in the QR code point to more than two entries in the global color table, which enables hiding another QR code within the same pixels.
By replacing every palette entry with either black or white, the underlying index pattern becomes a high-contrast QR code. Scanning that QR reveals the flag.
Initial Analysis
The attachment is a GIF named qr_code.gif.
At first glance this looks like a normal QR code. Scanning it gives only:
1
Come and have a closer look:)
That is not the flag, but it is a useful hint. The QR code is asking us to look closer at the image representation rather than at the decoded message.
The file format matters here. This is a GIF, and GIF images can use indexed color. In an indexed image, each pixel does not store a full RGB value. Instead, each pixel stores an index into a palette. In GIF terms, that palette can be the global color table.
So two pixels that both look black are not necessarily the same byte in the image data. They can point to different palette entries that all happen to be dark. The same applies to pixels that look white.
The Trick
A normal QR code only needs two visual states: black and white. This challenge uses more than two palette entries to draw those states.
That gives the author an extra layer:
1
2
visible color -> makes the decoy QR readable
palette index -> stores the hidden QR pattern
The visible RGB colors produce the decoy message. The underlying palette indices carry another binary pattern. If we keep the pixel indices unchanged but replace the palette with a new black-and-white palette, we can reveal a different QR code from the same image data.
Solution
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
from PIL import Image
import random
# Load the QR code GIF
input_file = "qr_code.gif"
output_file = "qr_code_randomized.gif"
# Open the image
img = Image.open(input_file)
# Get the global color table
palette = img.getpalette()
# Randomize the palette
random_palette = palette[:]
for i in range(0, len(random_palette), 3):
bow = random.randint(0, 1) * 255
random_palette[i] = bow
random_palette[i+1] = bow
random_palette[i+2] = bow
# Apply the new palette
img.putpalette(random_palette)
# Save the modified GIF
img.save(output_file)
print(f"Randomized QR code saved to {output_file}")
The script is probabilistic, so a single run is not guaranteed to produce the cleanest possible QR code. If the output is not readable, rerunning it gives a different black-and-white assignment for the palette entries. After a few tries the correct GIF appears.

