worked on the chall

This commit is contained in:
2026-01-08 09:42:09 +01:00
parent 6d8117c53c
commit ee95f4df4f
14 changed files with 313 additions and 232 deletions

4
.gitignore vendored
View File

@@ -1,3 +1,3 @@
.venv .venv/
__pycache__ __pycache__/
*.png *.png

View File

@@ -11,3 +11,12 @@ The mosaic system encodes emails as QR-Codes and then applies the ultra secure p
We found a QR-Code that was not disposed properly in a waste bin. We found a QR-Code that was not disposed properly in a waste bin.
Can you decipher the email? Can you decipher the email?
Hint: The mosaic algorithm adds decoy fake data to the QR-Code. It is mega secure after all! Hint: The mosaic algorithm adds decoy fake data to the QR-Code. It is mega secure after all!
### How to deploy
Install all python dependencies:
`pip install -r requirements.txt`
Install zbar:
`sudo apt-get install libzbar0` https://pypi.org/project/pyzbar/

69
create_qrcode.py Normal file
View File

@@ -0,0 +1,69 @@
import argparse
from typing import List
import qrcode
from PIL import Image
from pathlib import Path
import random
NUM_PIECES = 3
TILE = 78
def generate_qrcode(data: str) -> Image:
qr = qrcode.QRCode(version=25, box_size=2, error_correction=qrcode.ERROR_CORRECT_L, border=0)
qr.add_data(data)
return qr.make_image()
def cut_pieces(image: Image) -> List[Image]:
pieces = []
for x, y in [(x, y) for x in range(NUM_PIECES) for y in range(NUM_PIECES)]:
pieces.append(image.crop((x * TILE, y * TILE, x * TILE + TILE, y * TILE + TILE)))
return pieces
def scramble_qr_code(payloadList: List[Image], seed: str) -> Image:
height_piece, width_piece = payloadList[0].size
random.seed(seed)
scrambled_width = 3
random.shuffle(payloadList)
new_qr_code = Image.new("RGB", (width_piece * scrambled_width, height_piece * NUM_PIECES))
for x, y in [(x, y) for x in range(scrambled_width) for y in range(NUM_PIECES)]:
piece = payloadList[x + scrambled_width * y]
new_qr_code.paste(piece, (x * width_piece, y * height_piece))
return new_qr_code
def generate_scrambled_qrcode(seed: str, output_dir: Path, name: str):
with open("./secret_message.txt", "r") as secret:
message = secret.read()
qr_code = generate_qrcode(message + seed)
qr_pieces = cut_pieces(qr_code)
scrambled_qr_code = scramble_qr_code(qr_pieces, seed)
output_dir.mkdir(parents=True, exist_ok=True)
scrambled_qr_code.save(output_dir / name)
def main():
parser = argparse.ArgumentParser(description="Generates a scrambled QR code with optional decoy pieces.")
parser.add_argument("seed", type=str, help="The password to scramble the QR code.")
parser.add_argument("output_dir", type=Path, help="Directory where the output will be stored")
parser.add_argument("-o", "--output", type=str, default="secret_message.png", help="The name of the output image file.")
args = parser.parse_args()
generate_scrambled_qrcode(
seed=args.seed,
output_dir=args.output_dir,
name=args.output
)
if __name__ == "__main__":
main()

18
description.yml Normal file
View File

@@ -0,0 +1,18 @@
mosaic: # replace with actual short name
name: "mega obfuscated secure advanced information communication"
author: "cato447" # will be displayed on CTFd
# Estimated difficulty
difficulty: easy
description: "The TAs creating the endterm exam have suffered in the past from leaked drafts.
To protect themselves against further leaks they invented the *mega obfuscated secure advanced information communication* (mosaic) system.
The mosaic system encodes emails as QR-Codes and then applies the ultra secure protection algorithm.
We found a QR-Code that was not disposed properly in a waste bin.
Can you decipher the email?
Hint:
1. The mosaic algorithm adds decoy fake data to the QR-Code. It is mega secure after all!
2. Save yourself from the headache of searching for QR-Code libraries: https://github.com/NaturalHistoryMuseum/pyzbar"
category: misc
flag:
content: "h4tum{sm4ll_key_spac3s_are_deadly}"
dynamic: true
files: dynamic # dynamic requires a gen_files script

82
gen_files.py Normal file
View File

@@ -0,0 +1,82 @@
import argparse
import qrcode
from PIL import Image
from pathlib import Path
import random
NUM_PIECES = 3
TILE = 78
def generate_qrcode(data: str):
qr = qrcode.QRCode(version=25, box_size=2, error_correction=qrcode.ERROR_CORRECT_L, border=0)
qr.add_data(data)
return qr.make_image()
def cut_pieces(image):
pieces = []
for x, y in [(x, y) for x in range(NUM_PIECES) for y in range(NUM_PIECES)]:
pieces.append(image.crop((x * TILE, y * TILE, x * TILE + TILE, y * TILE + TILE)))
return pieces
def scramble_qr_code(payloadList, decoyList, seed):
height_piece, width_piece = payloadList[0].size
random.seed(seed)
scrambled_width = 3
if decoyList is not None:
payloadList += random.sample(decoyList, NUM_PIECES)
assert len(payloadList) == 12
scrambled_width = 4
random.shuffle(payloadList)
new_qr_code = Image.new("RGB", (width_piece * scrambled_width, height_piece * NUM_PIECES))
for x, y in [(x, y) for x in range(scrambled_width) for y in range(NUM_PIECES)]:
piece = payloadList[x + scrambled_width * y]
new_qr_code.paste(piece, (x * width_piece, y * height_piece))
return new_qr_code
def generate_scrambled_qrcode(seed, output_dir, decoy, name):
with open("res/secret_message.txt", "r") as secret:
message = secret.read()
qr_code = generate_qrcode(message + seed)
qr_pieces = cut_pieces(qr_code)
decoy_pieces = None
if decoy:
with open("res/decoy.txt", "r") as decoy_msg:
decoy = decoy_msg.read()
decoy_qr_code = generate_qrcode(decoy)
decoy_pieces = [piece for id, piece in enumerate(cut_pieces(decoy_qr_code)) if id not in [0, 2, 6]]
scrambled_qr_code = scramble_qr_code(qr_pieces, decoy_pieces, seed)
output_dir.mkdir(parents=True, exist_ok=True)
scrambled_qr_code.save(output_dir / name)
def main():
parser = argparse.ArgumentParser(description="Generates a scrambled QR code with optional decoy pieces.")
parser.add_argument("seed", type=str, help="The password to scramble the QR code.")
parser.add_argument("output_dir", type=Path, help="Directory where the output will be stored")
parser.add_argument("--without-decoy", action="store_false", help="Exclude decoy pieces in the scrambled QR code.")
parser.add_argument("-o", "--output", type=str, default="secret_message.png", help="The name of the output image file.")
args = parser.parse_args()
generate_scrambled_qrcode(
seed=args.seed,
output_dir=args.output_dir,
decoy=args.without_decoy,
name=args.output
)
if __name__ == "__main__":
main()

View File

@@ -1,74 +0,0 @@
import qrcode
from PIL import Image
import random
import sys
from templates import payload, decoy_payload
NUM_PIECES = 3
def generate_qrcode(data : str):
qr = qrcode.QRCode(version=25, box_size=2, error_correction=qrcode.ERROR_CORRECT_L, border=0)
qr.add_data(data)
return qr.make_image()
def cut_pieces(image):
# Get the size of the image
width, height = image.size
assert width == height
# Calculate the size of each grid cell
cell_width = width // NUM_PIECES
cell_height = height // NUM_PIECES
pieces = []
# Split the original image into 9 pieces
for i in range(NUM_PIECES**2):
# Calculate the coordinates of the current grid cell
x1 = (i % NUM_PIECES) * cell_width
y1 = (i // NUM_PIECES) * cell_height
x2 = x1 + cell_width
y2 = y1 + cell_height
# Crop the corresponding part of the original image
cropped_piece = image.crop((x1, y1, x2, y2))
pieces.append(cropped_piece)
return pieces
def scramble_qr_code(payloadList, decoyList, password):
height_piece, width_piece = payloadList[0].size
new_qr_code = Image.new("RGB", (width_piece * (NUM_PIECES+1), height_piece * NUM_PIECES))
random.seed(password)
payloadList += random.sample(decoyList, NUM_PIECES)
assert len(payloadList) == 12
random.shuffle(payloadList)
for x,y in [(x,y) for x in range(NUM_PIECES+1) for y in range(NUM_PIECES)]:
piece = payloadList[x + (NUM_PIECES+1) * y]
new_qr_code.paste(piece, (x * width_piece, y * height_piece))
return new_qr_code
def generate_scrambled_qrcode(passphrase):
qr_code = generate_qrcode(payload)
decoy_qr_code = generate_qrcode(decoy_payload)
qr_pieces = cut_pieces(qr_code)
decoy_pieces = [piece for id, piece in enumerate(cut_pieces(decoy_qr_code)) if id not in [0,2,6]]
scrambled_qr_code = scramble_qr_code(qr_pieces, decoy_pieces, passphrase)
scrambled_qr_code.save("scrambled_test_qr_code.png")
def main():
if len(sys.argv) != 2:
print("missing password")
sys.exit()
generate_scrambled_qrcode(sys.argv[1])
if __name__ == "__main__":
main()

25
healthcheck.py Normal file
View File

@@ -0,0 +1,25 @@
from solve import main
from gen_files import generate_scrambled_qrcode
import random
from pathlib import Path
import string
visible_chars = string.ascii_letters + string.digits
random_bytes = "".join(random.choice(visible_chars) for _ in range(10))
test_flag = f"h4tum{{healthy_healthy_healthchecks_{random_bytes}}}"
generate_scrambled_qrcode(
seed=test_flag,
output_dir=Path("tmp/health_check"),
decoy=True,
name=f"{random_bytes}.png"
)
output = main(f"tmp/health_check/{random_bytes}.png", False)
if output and test_flag in output:
print("Healthy!")
else:
print("Oh oh somethings wrong :()")

View File

@@ -1,51 +0,0 @@
from PIL import Image
import numpy as np
from numpy.lib.stride_tricks import sliding_window_view
def load_bw(path):
# Black=1, White=0
im = Image.open(path).convert("L")
bw = (np.array(im) < 128).astype(np.uint8)
return bw
def finder_template(box_size=2):
# 7x7 finder: outer black, inner white (5x5), center black (3x3)
F = np.array([
[1,1,1,1,1,1,1],
[1,0,0,0,0,0,1],
[1,0,1,1,1,0,1],
[1,0,1,1,1,0,1],
[1,0,1,1,1,0,1],
[1,0,0,0,0,0,1],
[1,1,1,1,1,1,1],
], dtype=np.uint8)
# upscale to pixels
return np.kron(F, np.ones((box_size, box_size), dtype=np.uint8))
def count_finders(img_path, box_size=2, max_mismatch=0):
bw = load_bw(img_path)
T = finder_template(box_size=box_size)
h, w = T.shape
# slide 14x14 window over the image
win = sliding_window_view(bw, (h, w))
# Hamming distance to template per window
mismatches = (win ^ T).sum(axis=(-2, -1))
hits = mismatches <= max_mismatch
# Optional: suppress overlapping duplicates by non-maximum suppression on exact matches
ys, xs = np.where(hits)
# Convert to center coordinates
centers = [(int(y + h/2), int(x + w/2)) for y, x in zip(ys, xs)]
# Greedy dedup within ~half a finder width
deduped = []
r = max(2, h//2)
for cy, cx in centers:
if all((abs(cy - y) > r) or (abs(cx - x) > r) for y, x in deduped):
deduped.append((cy, cx))
return len(deduped), deduped
if __name__ == "__main__":
n, centers = count_finders("./scrambled_test_qr_code.png", box_size=2, max_mismatch=0)
print("Finder count:", n, "centers:", centers, "Exactly three?", n == 3)

25
res/decoy.txt Normal file
View File

@@ -0,0 +1,25 @@
From: ops-team@sec.in.tum.de
To: exam_archive@sec.in.tum.de
Subject: [Draft Item — Secure Systems]
Restricted Draft — Not Finalized
Question:
Suppose a QR code is divided into equal parts and randomly
reordered before printing. Security is claimed because the
order cannot be recovered without the exact permutation.
(a) Explain why relying on this hidden ordering ensures
long-term secrecy.
(b) Would the presence of alignment markers or error
correction change this conclusion? Justify briefly.
Solution (Internal Notes Only):
(a) The scheme is secure since without the original order,
the data is mathematically unrecoverable; no structure
leaks to the adversary.
(b) Alignment markers and redundancy do not help attackers,
because the scrambling completely destroys any visible
pattern, leaving only random noise.
Verification Token: h4tum{fake_fake_fake_flag]

24
res/mosaic_secret_message Normal file
View File

@@ -0,0 +1,24 @@
From: hochspezialisiert@sec.in.tum.de
To: itsec_examdraft@sec.in.tum.de
Subject: [Draft Question — Cryptography]
Confidential (ONLY FOR AUTHORIZED PERSONNEL)
Question:
Consider a toy "QR-encryption" scheme where a message is encoded into
a QR code and then randomized by permuting its tiles with a secret seed.
(a) Under ideal assumptions, explain why this scheme could satisfy
Kerckhoffs principle.
(b) Now assume an attacker can test permutations against the QR format
(find patterns, error correction, alignment markers). Why does this
reduce the effective security of the scheme?
Solution (DO NOT DISTRIBUTE):
(a) If the algorithm is known and the seed remains secret, only the
seed determines security → aligns with Kerckhoffs principle.
(b) QR codes have strong structural redundancy (finder patterns,
error correction). This gives the attacker an oracle to prune wrong
seeds quickly → brute force feasible.
Verification Token:

4
res/secret_message.txt Normal file
View File

@@ -0,0 +1,4 @@
Wow you solved the challenge!
Meet us at our booth we really want to get in touch with you :D
Show us the flag:

View File

@@ -1,13 +1,13 @@
from logging import disable
from PIL import Image from PIL import Image
from pyzbar.pyzbar import decode from pyzbar.pyzbar import decode
import itertools import itertools
from math import perm
from tqdm import tqdm from tqdm import tqdm
import numpy as np import numpy as np
from multiprocessing import Pool
from math import perm
import sys import sys
NUM_PIECES_WIDTH = 4
NUM_PIECES_HEIGHT = 3
BOX_SIZE = 2 BOX_SIZE = 2
TILE = 78 TILE = 78
# 7x7 finder, upscaled to 14x14 (black=1, white=0) # 7x7 finder, upscaled to 14x14 (black=1, white=0)
@@ -40,23 +40,17 @@ def cut_pieces(image):
# Get the size of the image # Get the size of the image
width, height = image.size width, height = image.size
# Calculate the size of each grid cell if width == height:
cell_width = width // NUM_PIECES_WIDTH NUM_PIECES_WIDTH = 3
cell_height = height // NUM_PIECES_HEIGHT else:
NUM_PIECES_WIDTH = 4
NUM_PIECES_HEIGHT = 3
pieces = [] pieces = []
# Split the original image into 12 pieces for x, y in [(x, y) for x in range(NUM_PIECES_WIDTH) for y in range(NUM_PIECES_HEIGHT)]:
for i in range(NUM_PIECES_WIDTH * NUM_PIECES_HEIGHT): pieces.append(image.crop((x * TILE, y * TILE, x * TILE + TILE, y * TILE + TILE)))
# Calculate the coordinates of the current grid cell
x1 = (i % NUM_PIECES_WIDTH) * cell_width
y1 = (i // NUM_PIECES_WIDTH) * cell_height
x2 = x1 + cell_width
y2 = y1 + cell_height
# Crop the corresponding part of the original image
cropped_piece = image.crop((x1, y1, x2, y2))
pieces.append(cropped_piece)
return pieces return pieces
@@ -72,7 +66,6 @@ def finder_corner_exact(tile_bw):
'TL': tile_bw[0:H, 0:W ], 'TL': tile_bw[0:H, 0:W ],
'TR': tile_bw[0:H, TILE-W:TILE ], 'TR': tile_bw[0:H, TILE-W:TILE ],
'BL': tile_bw[TILE-H:TILE, 0:W ], 'BL': tile_bw[TILE-H:TILE, 0:W ],
'BR': tile_bw[TILE-H:TILE, TILE-W:TILE ],
} }
for corner, crop in crops.items(): for corner, crop in crops.items():
if np.array_equal(crop, TEMPLATE): if np.array_equal(crop, TEMPLATE):
@@ -90,11 +83,33 @@ def three_tiles_with_finders(tiles):
break break
return hits return hits
def solve_combo(combo):
"""
Function to be executed by each process in the pool.
"""
a, b, c, d, e, f = combo
# Assuming `new_image` is a base image you can copy or an in-memory image object.
# You may need to pass `new_image` to the function or have it loaded by each process.
local_image = new_image.copy() # Make a copy to avoid race conditions
local_image.paste(a, (78, 0))
local_image.paste(b, (0, 78))
local_image.paste(c, (78, 78))
local_image.paste(d, (156, 78))
local_image.paste(e, (78, 156))
local_image.paste(f, (156, 156))
if __name__ == "__main__": output = decodeQRCode(local_image)
img = Image.open("./scrambled_test_qr_code.png") if output:
return output
return None
def main(img_path="student_fair/secret_message.png", verbose=True):
img = Image.open(img_path)
global new_image
new_image = Image.new("RGB", (img.height, img.height)) new_image = Image.new("RGB", (img.height, img.height))
pieces = cut_pieces(img) pieces = cut_pieces(img)
corners = three_tiles_with_finders(pieces) corners = three_tiles_with_finders(pieces)
@@ -110,22 +125,27 @@ if __name__ == "__main__":
pieces = [piece for piece in pieces if piece not in [tile for tile, _ in corners]] pieces = [piece for piece in pieces if piece not in [tile for tile, _ in corners]]
assert len(pieces) == 9
new_image.show()
possible_combos = itertools.permutations(pieces, 6) possible_combos = itertools.permutations(pieces, 6)
num_possible_combos = perm(len(pieces), 6)
print(f"Testing {num_possible_combos} combinations")
for a,b,c,d,e,f in tqdm(possible_combos, total=num_possible_combos): result = None
new_image.paste(a, (78, 0))
new_image.paste(b, (0, 78))
new_image.paste(c, (78, 78))
new_image.paste(d, (156, 78))
new_image.paste(e, (78, 156))
new_image.paste(f, (156, 156))
output = decodeQRCode(new_image)
if output:
print(output)
print(decodeQRCode(img)) MAX_WORKER = 6
# Use `multiprocessing.Pool` with the number of available cores
with Pool(MAX_WORKER) as pool:
# `imap_unordered` returns results as they are completed.
# This is a lazy iterator, so it doesn't create all combos at once.
# `tqdm` can still be used to show progress.
for result in tqdm(pool.imap_unordered(solve_combo, possible_combos), total=perm(len(pieces), 6), disable=(not verbose)):
if result:
if verbose:
print(f"QR Code decoded: {result}")
pool.terminate() # Stop all worker processes
break
return result
if __name__ == "__main__":
main()

View File

@@ -1,55 +0,0 @@
flag = "h4tum{small_key_spaces_are_deadly}"
payload = f"""
From: hochspezialisiert@sec.in.tum.de
To: itsec_examdraft@sec.in.tum.de
Subject: [Draft Question — Cryptography]
Confidential (ONLY FOR AUTHORIZED PERSONNEL)
Question:
Consider a toy "QR-encryption" scheme where a message is encoded into
a QR code and then randomized by permuting its tiles with a secret seed.
(a) Under ideal assumptions, explain why this scheme could satisfy
Kerckhoffs principle.
(b) Now assume an attacker can test permutations against the QR format
(find patterns, error correction, alignment markers). Why does this
reduce the effective security of the scheme?
Solution (DO NOT DISTRIBUTE):
(a) If the algorithm is known and the seed remains secret, only the
seed determines security → aligns with Kerckhoffs principle.
(b) QR codes have strong structural redundancy (finder patterns,
error correction). This gives the attacker an oracle to prune wrong
seeds quickly → brute force feasible.
Verification Token: {flag}"""
decoy_payload = """
From: ops-team@sec.in.tum.de
To: exam_archive@sec.in.tum.de
Subject: [Draft Item — Secure Systems]
Restricted Draft — Not Finalized
Question:
Suppose a QR code is divided into equal parts and randomly
reordered before printing. Security is claimed because the
order cannot be recovered without the exact permutation.
(a) Explain why relying on this hidden ordering ensures
long-term secrecy.
(b) Would the presence of alignment markers or error
correction change this conclusion? Justify briefly.
Solution (Internal Notes Only):
(a) The scheme is secure since without the original order,
the data is mathematically unrecoverable; no structure
leaks to the adversary.
(b) Alignment markers and redundancy do not help attackers,
because the scrambling completely destroys any visible
pattern, leaving only random noise.
Verification Token: h4tum{fake_fake_fake_flag]"""

View File

@@ -1,15 +0,0 @@
from locate_finders import count_finders
from generate_qrcode import generate_scrambled_qrcode
import random
from PIL import Image
for i in range(1000):
print(f"\rTesting {i}", end="")
generate_scrambled_qrcode(random.randbytes(32))
finders, _ = count_finders("./scrambled_test_qr_code.png")
if finders != 3:
print("found anomaly")
img = Image.open("./scrambled_test_qr_code.png")
img.show()