← Back to Writeups
CUSTOMMediumMisc

QR-storm

XESXOR9/2/20263 min read
#misc#custom#medium#ansi_parsing#qr_decoding

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:

  1. Connects to the server via sockets.
  2. Reads ANSI graphics and converts it to a pixel matrix.
  3. Uses the PIL (Pillow) library to create an image from the matrix.
  4. Uses pyzbar for QR code recognition.
  5. Sends the decoded text back to the server.
python
#!/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()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍