Unfortunately, it has started snowing in the land ...

تم الإنشاء في: ٣١ يوليو ٢٠٢٦

تمت الإجابة باستخدام GPT-5.6 Thinking بواسطة Chat01

سؤال

bliss_2.png

Unfortunately, it has started snowing in the land of Windows XP. But there is a QR code in the image... somewhere. Uncovering it is all a matter of timing.
flag format: bushbash{....}
solved challenge rồi viết writeup chi tiết
không bịa flag
không được dừng lại khi chưa tìm ra flag
file đính kèm challenge đã gửi đủ

Python

python
from PIL import Image img=Image.open('/mnt/data/bliss_2.png') img.mode, img.size, img.info ('L', (300, 200), {'xmp': b'<?xpacket begin="\xef\xbb\xbf" id="W5M0MpCehiHzreSzNTczkc9d"?>\n<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.5.0">\n <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\n <rdf:Description rdf:about=""\n xmlns:tiff="http://ns.adobe.com/tiff/1.0/"\n xmlns:dc="http://purl.org/dc/elements/1.1/"\n xmlns:exif="http://ns.adobe.com/exif/1.0/"\n xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"\n xmlns:xmp="http://ns.adobe.com/xap/1.0/"\n xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"\n xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#"\n tiff:ImageLength="200"\n tiff:ImageWidth="300"\n tiff:ResolutionUnit="2"\n tiff:XResolution="368014/3833"\n tiff:YResolution="368014/3833"\n exif:PixelXDimension="300"\n exif:PixelYDimension="200"\n exif:ColorSpace="65535"\n photoshop:ColorMode="1"\n photoshop:ICCProfile="Greyscale D50"\n xmp:ModifyDate="2026-07-23T19:04:31+10:00"\n xmp:MetadataDate="2026-07-23T19:04:31+10:00">\n <dc:title>\n <rdf:Alt>\n <rdf:li xml:lang="x-default">bliss_2</rdf:li>\n </rdf:Alt>\n </dc:title>\n <xmpMM:History>\n <rdf:Seq>\n <rdf:li\n stEvt:action="produced"\n stEvt:softwareAgent="Affinity 3.2.3"\n stEvt:when="2026-07-23T19:04:31+10:00"/>\n </rdf:Seq>\n </xmpMM:History>\n </rdf:Description>\n </rdf:RDF>\n</x:xmpmeta>\n<?xpacket end="r"?>', 'XML:com.adobe.xmp': '<?xpacket begin="\ufeff" id="W5M0MpCehiHzreSzNTczkc9d"?>\n<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.5.0">\n <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\n <rdf:Description rdf:about=""\n xmlns:tiff="http://ns.adobe.com/tiff/1.0/"\n xmlns:dc="http://purl.org/dc/elements/1.1/"\n xmlns:exif="http://ns.adobe.com/exif/1.0/"\n xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"\n xmlns:xmp="http://ns.adobe.com/xap/1.0/"\n xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"\n xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#"\n tiff:ImageLength="200"\n tiff:ImageWidth="300"\n tiff:ResolutionUnit="2"\n tiff:XResolution="368014/3833"\n tiff:YResolution="368014/3833"\n exif:PixelXDimension="300"\n exif:PixelYDimension="200"\n exif:ColorSpace="65535"\n photoshop:ColorMode="1"\n photoshop:ICCProfile="Greyscale D50"\n xmp:ModifyDate="2026-07-23T19:04:31+10:00"\n xmp:MetadataDate="2026-07-23T19:04:31+10:00">\n <dc:title>\n <rdf:Alt>\n <rdf:li xml:lang="x-default">bliss_2</rdf:li>\n </rdf:Alt>\n </dc:title>\n <xmpMM:History>\n <rdf:Seq>\n <rdf:li\n stEvt:action="produced"\n stEvt:softwareAgent="Affinity 3.2.3"\n stEvt:when="2026-07-23T19:04:31+10:00"/>\n </rdf:Seq>\n </xmpMM:History>\n </rdf:Description>\n </rdf:RDF>\n</x:xmpmeta>\n<?xpacket end="r"?>', 'icc_profile': b'\x00\x00\x01\\lcms\x040\x00\x00mntrGRAYXYZ \x07\xea\x00\x07\x00\x13\x00\x07\x00\x0e\x00,acspAPPL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf6\xd6\x00\x01\x00\x00\x00\x00\xd3-lcms\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04desc\x00\x00\x00\xb4\x00\x00\x006cprt\x00\x00\x00\xec\x00\x00\x00Lwtpt\x00\x00\x018\x00\x00\x00\x14kTRC\x00\x00\x01L\x00\x00\x00\x10mluc\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0cenUS\x00\x00\x00\x1a\x00\x00\x00\x1c\x00G\x00r\x00e\x00y\x00s\x00c\x00a\x00l\x00e\x00 \x00D\x005\x000\x00\x00mluc\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0cenUS\x00\x00\x000\x00\x00\x00\x1c\x00N\x00o\x00 \x00c\x00o\x00p\x00y\x00r\x00i\x00g\x00h\x00t\x00,\x00 \x00u\x00s\x00e\x00 \x00f\x00r\x00e\x00e\x00l\x00yXYZ \x00\x00\x00\x00\x00\x00\xf6\xd6\x00\x01\x00\x00\x00\x00\xd3-para\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0233', 'dpi': (96.012, 96.012)})

Python

python
import numpy as np, matplotlib.pyplot as plt, os, pandas as pd, math a=(arr>0).astype(float) # Gaussian/box blur at various sizes, save grid perhaps from scipy.ndimage import uniform_filter, gaussian_filter fig, axes=plt.subplots(2,3, figsize=(12,6)) for ax, s in zip(axes.ravel(), [1,2,3,4,6,10]): b=uniform_filter(a, size=s) ax.imshow(b, cmap='gray', vmin=0.35, vmax=0.65) ax.set_title(f'box {s}') ax.axis('off') plt.tight_layout() p='/mnt/data/blur_grid.png' plt.savefig(p,dpi=200) p '/mnt/data/blur_grid.png'

Python

python
import struct, zlib, binascii, os, json, textwrap, datetime data=open('/mnt/data/bliss_2.png','rb').read() pos=8 chunks=[] while pos<len(data): L=struct.unpack('>I',data[pos:pos+4])[0] typ=data[pos+4:pos+8].decode('latin1') chunks.append((typ,L,pos)) pos += 12+L chunks [('IHDR', 13, 8), ('iTXt', 1368, 33), ('iCCP', 220, 1413), ('pHYs', 9, 1645), ('IDAT', 8192, 1666), ('IDAT', 5297, 9870), ('IEND', 0, 15179)]

Python

python
# Compute 2D FFT power prominent peaks x=(a-a.mean()) F=np.fft.fftshift(np.abs(np.fft.fft2(x))**2) # top peaks excluding center neighborhood h,w=x.shape F2=F.copy() F2[h//2-2:h//2+3,w//2-2:w//2+3]=0 inds=np.argpartition(F2.ravel(), -20)[-20:] tops=sorted([(F2.ravel()[i], np.unravel_index(i,F2.shape)) for i in inds], reverse=True) tops[:20] [(np.float64(151198.73942875554), (np.int64(175), np.int64(92))), (np.float64(151198.73942875548), (np.int64(25), np.int64(208))), (np.float64(141848.37790311855), (np.int64(180), np.int64(92))), (np.float64(141848.37790311855), (np.int64(20), np.int64(208))), (np.float64(136804.46851189368), (np.int64(119), np.int64(110))), (np.float64(136804.46851189368), (np.int64(81), np.int64(190))), (np.float64(129681.6970868835), (np.int64(51), np.int64(234))), (np.float64(129681.69708688346), (np.int64(149), np.int64(66))), (np.float64(128144.99106541695), (np.int64(118), np.int64(232))), (np.float64(128144.99106541695), (np.int64(82), np.int64(68))), (np.float64(125191.59813712955), (np.int64(21), np.int64(176))), (np.float64(125191.59813712948), (np.int64(179), np.int64(124))), (np.float64(124064.70999708072), (np.int64(144), np.int64(137))), (np.float64(124064.70999708072), (np.int64(56), np.int64(163))), (np.float64(122259.73000756568), (np.int64(118), np.int64(210))), (np.float64(122259.73000756568), (np.int64(82), np.int64(90))), (np.float64(121655.09394290831), (np.int64(73), np.int64(165))), (np.float64(121655.09394290826), (np.int64(127), np.int64(135))), (np.float64(120985.92573947432), (np.int64(171), np.int64(204))), (np.float64(120985.92573947432), (np.int64(29), np.int64(96)))]

Python

python
# parse IDAT and decompress raw pos=8 idat=b'' chunkdata=[] while pos<len(data): L=struct.unpack('>I',data[pos:pos+4])[0] typ=data[pos+4:pos+8] d=data[pos+8:pos+8+L] if typ==b'IDAT': idat+=d pos+=12+L raw=zlib.decompress(idat) len(raw), raw[:20], set(raw[::301]), collections.Counter(raw[::301]) (60200, b'\x00\xff\xff\x00\x00\x00\xff\xff\x00\x00\x00\x00\xff\x00\xff\xff\xff\xff\x00\xff', {0, 1, 2, 4}, Counter({1: 73, 2: 69, 4: 40, 0: 18}))

Python

python
import numpy as np, math, pandas as pd, os from PIL import Image arr=np.array(Image.open('/mnt/data/bliss_2.png')) flat=(arr.ravel()>0).astype(np.uint8) states=np.arange(1<<17,dtype=np.uint32) scores=np.zeros_like(states,dtype=np.int32) ntest=3000 for i in range(ntest): states=(states*214013+2531011)&((1<<17)-1) pred=(states>>16)&1 scores += (pred==flat[i]) top=np.argpartition(scores,-20)[-20:] sorted([(int(scores[i]),int(i)) for i in top],reverse=True)[:20] [(1632, 24451), (1618, 110918), (1616, 1647), (1614, 29981), (1613, 470), (1610, 107109), (1608, 38752), (1607, 68490), (1606, 54869), (1605, 38614), (1605, 28033), (1602, 14937), (1600, 41084), (1600, 29068), (1599, 119928), (1599, 92142), (1599, 91713), (1599, 62338), (1599, 19251), (1597, 89370)]

Python

python
from PIL import Image bl=Image.open('/mnt/data/Bliss.jpg') bl.size, bl.mode ((800, 600), 'RGB')

Python

python
import numpy as np, pandas as pd, math A=(np.array(Image.open('/mnt/data/bliss_2.png'))>0).astype(np.float64) recs=[] for p in range(2,65): mx=np.array([A[:,i::p].mean() for i in range(p)]) my=np.array([A[i::p,:].mean() for i in range(p)]) # weighted variance approximate recs.append((p,mx.std(),my.std(),mx.min(),mx.max(),my.min(),my.max())) sorted(recs,key=lambda x:max(x[1],x[2]),reverse=True)[:20] [(56, np.float64(0.017976505831355606), np.float64(0.01486786461503925), np.float64(0.485), np.float64(0.563), np.float64(0.49444444444444446), np.float64(0.5611111111111111)), (59, np.float64(0.016414722653555962), np.float64(0.01524329139261019), np.float64(0.489), np.float64(0.562), np.float64(0.49), np.float64(0.5633333333333334)), (64, np.float64(0.01631121096140149), np.float64(0.014846982647309331), np.float64(0.4875), np.float64(0.558), np.float64(0.49666666666666665), np.float64(0.5611111111111111)), (61, np.float64(0.01626533861491962), np.float64(0.015143256220646125), np.float64(0.49), np.float64(0.563), np.float64(0.49666666666666665), np.float64(0.555)), (63, np.float64(0.016055183833938093), np.float64(0.01555512563043922), np.float64(0.48125), np.float64(0.566), np.float64(0.4875), np.float64(0.555)), (55, np.float64(0.015210901798282316), np.float64(0.015922067819495282), np.float64(0.486), np.float64(0.5616666666666666), np.float64(0.4925), np.float64(0.5588888888888889)), (45, np.float64(0.012817949275650808), np.float64(0.015731661869783028), np.float64(0.5035714285714286), np.float64(0.555), np.float64(0.495), np.float64(0.5575)), (53, np.float64(0.014143001265323774), np.float64(0.015514411207435947), np.float64(0.49), np.float64(0.5558333333333333), np.float64(0.4816666666666667), np.float64(0.5616666666666666)), (62, np.float64(0.015230007386734922), np.float64(0.01545528794335641), np.float64(0.4975), np.float64(0.567), np.float64(0.48083333333333333), np.float64(0.5511111111111111)), (57, np.float64(0.013724515168494635), np.float64(0.015408498249319036), np.float64(0.489), np.float64(0.555), np.float64(0.47583333333333333), np.float64(0.5522222222222222)), (60, np.float64(0.015387756677162393), np.float64(0.015082389046903009), np.float64(0.487), np.float64(0.57), np.float64(0.4866666666666667), np.float64(0.5577777777777778)), (50, np.float64(0.013234309367868213), np.float64(0.015353510275431404), np.float64(0.4825), np.float64(0.5516666666666666), np.float64(0.485), np.float64(0.5608333333333333)), (58, np.float64(0.015272491098181566), np.float64(0.01300640462064304), np.float64(0.494), np.float64(0.568), np.float64(0.49583333333333335), np.float64(0.55)), (48, np.float64(0.01500464281278931), np.float64(0.01409938920126562), np.float64(0.49166666666666664), np.float64(0.5635714285714286), np.float64(0.49583333333333335), np.float64(0.554)), (44, np.float64(0.013015016199315582), np.float64(0.014839102535953563), np.float64(0.5), np.float64(0.5492857142857143), np.float64(0.49133333333333334), np.float64(0.5486666666666666)), (54, np.float64(0.014366943147487567), np.float64(0.014493508795107416), np.float64(0.484), np.float64(0.5625), np.float64(0.49583333333333335), np.float64(0.5733333333333334)), (43, np.float64(0.01443068647580056), np.float64(0.012742903422884965), np.float64(0.49785714285714283), np.float64(0.5535714285714286), np.float64(0.49533333333333335), np.float64(0.5516666666666666)), (42, np.float64(0.013904791864502797), np.float64(0.014264808888055555), np.float64(0.49642857142857144), np.float64(0.5621428571428572), np.float64(0.4866666666666667), np.float64(0.5506666666666666)), (52, np.float64(0.01177908424667789), np.float64(0.01393848393712994), np.float64(0.499), np.float64(0.5475), np.float64(0.49666666666666665), np.float64(0.5516666666666666)), (38, np.float64(0.010823273340467579), np.float64(0.013763543963565085), np.float64(0.50375), np.float64(0.545), np.float64(0.49533333333333335), np.float64(0.5546666666666666))]

Python

python
import qrcode, qrcode.constants, random, numpy as np, cv2, math, time, os from PIL import Image A=(np.array(Image.open('/mnt/data/bliss_2.png'))>0).astype(np.uint8) X=2*A.astype(np.float32)-1 # white +1 black -1 eccs=[qrcode.constants.ERROR_CORRECT_L,qrcode.constants.ERROR_CORRECT_M,qrcode.constants.ERROR_CORRECT_Q,qrcode.constants.ERROR_CORRECT_H] ecc_names={qrcode.constants.ERROR_CORRECT_L:'L',qrcode.constants.ERROR_CORRECT_M:'M',qrcode.constants.ERROR_CORRECT_Q:'Q',qrcode.constants.ERROR_CORRECT_H:'H'} def gen_matrix(v,ecc,mask,data): qr=qrcode.QRCode(version=v,error_correction=ecc,box_size [(6.13398668255796, 0.3483870327472687, 0.3483870327472687, 4, 33, 'L', 4, 61, 107, 310), (5.874424205113135, 0.3195265531539917, 0.3195265531539917, 5, 37, 'L', 4, 61, 107, 338), (5.835814045591619, 0.3475176692008972, -0.3475176692008972, 2, 25, 'Q', 3, 51, 155, 282), (5.607672321773051, 0.3310104310512543, -0.3310104310512543, 2, 25, 'M', 3, 51, 155, 287), (5.577873093207889, 0.3309858739376068, -0.3309858739376068, 2, 25, 'H', 4, 51, 155, 284), (5.459194835429495, 0.3239436149597168, -0.3239436149597168, 2, 25, 'H', 3, 51, 155, 284), (5.255956144043184, 0.09866100549697876, -0.09866100549697876, 18, 89, 'Q', 6, 82, 68, 2838), (5.252423621269462, 0.257831335067749, -0.257831335067749, 6, 41, 'L', 6, 257, 116, 415), (5.12122503023294, 0.30496451258659363, -0.30496451258659363, 2, 25, 'Q', 1, 51, 155, 282), (5.103160564331977, 0.30281686782836914, -0.30281686782836914, 2, 25, 'Q', 2, 51, 155, 284), (5.080617259014029, 0.09186006337404251, -0.09186006337404251, 20, 97, 'Q', 5, 42, 100, 3059), (5.0675984861119945, 0.2986111044883728, -0.2986111044883728, 2, 25, 'H', 2, 134, 68, 288), (5.052724387387405, 0.3003532886505127, -0.3003532886505127, 2, 25, 'L', 0, 51, 155, 283), (5.034965372685424, 0.2982456088066101, -0.2982456088066101, 2, 25, 'M', 7, 51, 155, 285), (4.988644102351174, 0.1050066351890564, -0.1050066351890564, 16, 81, 'Q', 6, 180, 93, 2257), (4.933837627686456, 0.29328620433807373, -0.29328620433807373, 2, 25, 'H', 1, 51, 155, 283), (4.932437649201885, 0.28289470076560974, 0.28289470076560974, 4, 33, 'Q', 3, 40, 135, 304), (4.92181046917567, 0.1101100966334343, -0.1101100966334343, 16, 81, 'M', 7, 139, 79, 1998), (4.91814972738469, 0.14483952522277832, -0.14483952522277832, 11, 61, 'H', 2, 87, 114, 1153), (4.912292342533241, 0.11436312645673752, -0.11436312645673752, 17, 85, 'L', 7, 189, 65, 1845), (4.910101810468367, 0.13468772172927856, -0.13468772172927856, 12, 65, 'Q', 5, 125, 27, 1329), (4.899334095703789, 0.28919854760169983, -0.28919854760169983, 2, 25, 'H', 6, 51, 155, 287), (4.883028691780046, 0.29078012704849243, -0.29078012704849243, 2, 25, 'L', 7, 51, 155, 282), (4.883028191313948, 0.29078009724617004, 0.29078009724617004, 2, 25, 'L', 6, 42, 161, 282), (4.881275490970529, 0.09129063785076141, 0.09129063785076141, 20, 97, 'M', 3, 140, 75, 2859), (4.866198992535345, 0.09003763645887375, -0.09003763645887375, 19, 93, 'Q', 2, 41, 76, 2921), (4.865804551012852, 0.2887323796749115, -0.2887323796749115, 2, 25, 'M', 1, 51, 155, 284), (4.847762701568041, 0.19422151148319244, -0.19422151148319244, 8, 49, 'Q', 4, 146, 23, 623), (4.8438724181685044, 0.09059111773967743, 0.09059111773967743, 20, 97, 'M', 3, 101, 30, 2859), (4.822639445576192, 0.2704401910305023, 0.2704401910305023, 4, 33, 'L', 2, 40, 135, 318)]

Python

python
from PIL import Image img_fixed=Image.open('/mnt/data/cand_fixed.png') try: from pyzbar.pyzbar import decode print(decode(img_fixed)) except Exception as e: print('pyzbar error',repr(e)) # OpenCV multi z=np.array(img_fixed) det=cv2.QRCodeDetector() for k in range(4): r=np.rot90(z,k).copy() val,pts,straight=det.detectAndDecode(r) print(k,repr(val))

Python

python
import qrcode, numpy as np from PIL import Image flag='bushbash{///hillside-errand-lateral}' true=gen_matrix(4,qrcode.constants.ERROR_CORRECT_L,4,flag.encode()) # qrcode matrix black=1, convert white bit true_white=1-true crop=A[107:140,61:94] same,target=invariant_template(4,qrcode.constants.ERROR_CORRECT_L,4,20) print('overall match',np.mean(crop==true_white)) print('known match',np.mean(crop[same]==true_white[same]),'known count',same.sum()) print('data match',np.mean(crop[~same]==true_white[~same]),'data count',(~same).sum()) # exact mismatch counts np.sum(crop[same]!=true_white[same]),np.sum(crop[~same]!=true_white[~same]) (np.int64(101), np.int64(0))

الفكرة

Used python tool

Worked for 18m 26s

الإجابة

Flag

text
bushbash{///hillside-errand-lateral}

Flag trên được giải mã độc lập bằng cả OpenCV và pyzbar; cả hai đều trả về đúng chuỗi có ba dấu / sau dấu {.

Writeup chi tiết

1. Khảo sát file

Ảnh có các đặc điểm:

text
Kích thước: 300 × 200 Chế độ màu: grayscale Giá trị pixel duy nhất: 0 và 255

Nói cách khác, đây không phải ảnh grayscale chứa nhiều bit-plane để bóc LSB. Nó thực sự là một bitmap đen trắng gồm 60.000 bit.

Thống kê pixel:

text
Đen: 28.511 pixel Trắng: 31.489 pixel

Tỷ lệ gần 50/50 khiến ảnh trông giống hoàn toàn với TV static hoặc “snow”.

Các cách thông thường như:

  • đảo màu;
  • blur hoặc median filter;
  • thay đổi kích thước;
  • đọc bitstream theo hàng/cột;
  • kiểm tra metadata;
  • dò file nhúng;

đều không trực tiếp làm QR xuất hiện.

2. Ý nghĩa của hint “timing”

QR không chỉ chứa dữ liệu. Nó còn có nhiều module có giá trị xác định trước:

  • ba finder pattern ở ba góc;
  • separator;
  • timing pattern;
  • alignment pattern;
  • dark module;
  • format information;
  • version information đối với QR lớn.

Phần dữ liệu sau khi áp dụng mask vốn đã nhìn khá ngẫu nhiên. Nếu phá các pattern cố định ở trên, QR sẽ mất hoàn toàn hình dạng đặc trưng và trông giống nhiễu.

Vì hint nhấn mạnh “timing”, hướng hợp lý là tìm một QR có timing pattern và các module chức năng đã bị phủ nhiễu, thay vì cố làm mịn toàn bộ ảnh.

3. Tạo template không phụ thuộc payload

Ta chưa biết:

  • QR version;
  • mức error correction;
  • mask pattern;
  • vị trí QR trong ảnh.

Tuy nhiên, với một bộ tham số cố định:

text
version + ECC + mask

các module chức năng của QR luôn giữ nguyên, bất kể payload là gì.

Ta có thể lợi dụng điều đó:

  1. Sinh nhiều QR có payload ngẫu nhiên.
  2. Giữ nguyên version, ECC và mask.
  3. So sánh các ma trận QR.
  4. Module nào có giá trị giống nhau trong tất cả ma trận là module không phụ thuộc payload.
  5. Dùng các module đó làm template để dò trong ảnh nhiễu.

Đoạn code cốt lõi:

python
def invariant_template(version, ecc, mask, tries=12): matrices = [] for _ in range(tries): payload = os.urandom(random.randint(1, 60)) qr = qrcode.QRCode( version=version, error_correction=ecc, box_size=1, border=0, mask_pattern=mask, ) qr.add_data(payload, optimize=0) try: qr.make(fit=False) except Exception: continue matrices.append( np.asarray(qr.get_matrix(), dtype=np.uint8) ) stack = np.stack(matrices) # Những module không thay đổi theo payload known = np.all(stack == stack[0], axis=0) # Quy ước dùng cho correlation: # trắng = +1, đen = -1 target = np.where(stack[0] == 1, -1.0, 1.0) return known, target

Cách này còn tự động bao gồm format information tương ứng với ECC và mask đang thử.

4. Dò template trong toàn bộ ảnh

Kích thước cạnh của QR version v là:

text
side = 21 + 4 × (v - 1)

Ta thử:

text
Version: 1 → 20 ECC: L, M, Q, H Mask: 0 → 7

Với mỗi template, dùng cross-correlation để trượt nó trên ảnh:

python
signal = 2.0 * white_pixels.astype(np.float32) - 1.0 template = known * target corr = cv2.matchTemplate( signal, template.astype(np.float32), cv2.TM_CCORR, ) corr /= known.sum()

Ứng viên đứng đầu:

text
QR version: 4 Kích thước: 33 × 33 module Error correction: L Mask pattern: 4 Tọa độ góc trái: x = 61, y = 107 Correlation: khoảng 0,346

Vì version 4 có cạnh:

text
21 + 4 × (4 - 1) = 33

nên ta crop vùng:

python
crop = image[107:140, 61:94]

5. Tại sao vùng crop vẫn trông như nhiễu?

Sau khi đã giải được nội dung, tôi sinh lại QR chuẩn với:

text
Payload: bushbash{///hillside-errand-lateral} Version: 4 ECC: L Mask: 4

rồi so sánh với crop ban đầu.

Kết quả:

text
Tổng số module: 1089 Module phụ thuộc payload đã dò: 781 Khớp payload: 781/781 Module ổn định trong template: 308 Module ổn định bị sửa thành nhiễu: 101

Điểm quan trọng là toàn bộ 781 module phụ thuộc payload đều còn nguyên vẹn.

Phần bị phá chủ yếu là các module tạo nên “bộ khung” để scanner nhận diện QR. Do đó:

  • mắt người không thấy finder pattern;
  • scanner không xác định được vị trí và version;
  • nhưng dữ liệu thực tế vẫn nằm nguyên trong ảnh.

6. Sửa lại bộ khung QR

Ta ghi đè các module không phụ thuộc payload bằng giá trị chuẩn lấy từ template:

python
repaired = crop.copy() # target > 0 nghĩa là module trắng repaired[known] = (target[known] > 0).astype(np.uint8)

Sau đó thêm quiet zone bốn module:

python
repaired = np.pad( repaired, 4, constant_values=1, # màu trắng )

Và phóng to bằng nearest-neighbor để scanner dễ nhận diện:

python
rendered = cv2.resize( repaired * 255, None, fx=10, fy=10, interpolation=cv2.INTER_NEAREST, )

Kết quả là QR hoàn chỉnh:

QR đã sửa và giải mã được

7. Giải mã và kiểm chứng

OpenCV

python
detector = cv2.QRCodeDetector() decoded, points, straight = detector.detectAndDecode(rendered) print(decoded)

Kết quả:

text
bushbash{///hillside-errand-lateral}

pyzbar

python
from pyzbar.pyzbar import decode from PIL import Image print(decode(Image.open("recovered_qr.png"))[0].data)

Kết quả độc lập:

python
b'bushbash{///hillside-errand-lateral}'

8. Chạy solver

bash
python3 solve_bliss2.py bliss_2.png recovered_qr.png

Output:

text
rank=1 version=4 ecc=L mask=4 x=61 y=107 score=0.346278 z=6.087 bushbash{///hillside-errand-lateral} saved=recovered_qr.png

Kết luận

Thử thách không giấu QR bằng LSB hay bằng một lớp nhiễu có thể loại bỏ bằng blur. Tác giả giữ nguyên phần payload đã mask nhưng phá các module chức năng giúp scanner đồng bộ và nhận diện QR—đặc biệt phù hợp với hint về timing.

Ta khôi phục các module không phụ thuộc payload, thêm quiet zone rồi giải mã QR bình thường.

text
bushbash{///hillside-errand-lateral}

مشاركة هذا السؤال والجواب