QR-storm
QR-storm
Platform: hackerlab | Category: Misc | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2025-12-26 | Status: Solved Techniques: ansi_parsing, qr_decoding
Summary
Task: Decode 50 QR codes sent via network in ANSI graphics format with time limit. Solution: Parse ANSI escape sequences to pixel matrix, convert to image with PIL, decode with pyzbar, automate via socket.
QR-storm — hackerlab
Description
The task was to decode 50 QR codes sent by the server over a network connection in ANSI graphics format. Each round had a limited time (about 5 seconds).
Analysis
When connecting to the server at 192.169.2.100:10006 and sending the start command, the server begins sending QR codes rendered in the terminal using ANSI escape sequences.
Key characteristics:
- Black modules:
\x1b[42m \x1b[0m(in this case 42 is green background, but used as dark color). - White modules:
\x1b[40m \x1b[0m(40 is black background, but in the context of the task perceived as light/empty). - Number of rounds: 50.
- Time limit requires automation.
Solution
A Python script was written that:
- Connects to the server via sockets.
- Reads ANSI graphics and converts it to a pixel matrix.
- Uses the
PIL(Pillow) library to create an image from the matrix. - Uses
pyzbarfor QR code recognition. - Sends the decoded text back to the server.
python read-only • copy allowed#!/usr/bin/env python3 import socket import re from PIL import Image from pyzbar.pyzbar import decode import time def solve(): host = '192.169.2.100' port = 10006 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, port)) def recv_until(suffix): res = b'' while not res.endswith(suffix): chunk = s.recv(1) if not chunk: break res += chunk return res print(recv_until(b'>>> ').decode()) s.sendall(b'start\n') for i in range(50): data = b'' # Wait for QR code start while b'\x1b[4' not in data: chunk = s.recv(1024) if not chunk: break data += chunk # Read until input prompt while b'>>>' not in data: chunk = s.recv(1024) if not chunk: break data += chunk # Parse ANSI to matrix lines = data.decode(errors='ignore').split('\n') qr_matrix = [] for line in lines: if '\x1b[4' in line: row = [] # 42m - Black (or dark), 40m - White (or light) matches = re.findall(r'\x1b\[(42|40)m \x1b\[0m', line) if matches: for m in matches: if m == '42': row.append(0) # Black else: row.append(255) # White qr_matrix.append(row) if not qr_matrix: print(f"Round {i+1}: Failed to parse QR matrix") break # Create image height = len(qr_matrix) width = len(qr_matrix[0]) img = Image.new('L', (width, height)) for y in range(height): for x in range(width): img.putpixel((x, y), qr_matrix[y][x]) # Upscale for better recognition img = img.resize((width * 10, height * 10), Image.NEAREST) # Decode decoded_objs = decode(img) if not decoded_objs: # Try inversion if not recognized img = Image.eval(img, lambda x: 255 - x) decoded_objs = decode(img) if decoded_objs: answer = decoded_objs[0].data.decode() print(f"Round {i+1}: Decoded: {answer}") s.sendall(answer.encode() + b'\n') else: print(f"Round {i+1}: Failed to decode QR") img.save(f"failed_qr_{i}.png") break # Get flag time.sleep(1) remaining = s.recv(4096) print(remaining.decode(errors='ignore')) s.close() if __name__ == '__main__': solve()