Alternative channel 310 Stego 0 0 Difficulty: Me...

बनाया गया: 21 अगस्त 2026

का उपयोग करके उत्तर दिया गया GPT-5.6 Thinking द्वारा Chat01

प्रश्न

misc_alternative-channel.zip

Alternative channel
310
Stego
0 0
Difficulty: Medium
Author: Ask the frog

Our regular SSTV transmission channel is being mishandled. Among the weird signals, someone has been slipping in something they shouldn't. Find out what's really being transmitted.

Note: What you will find is only the content of the flag, for example t3st_fl4g. It will be clear when you find it, but you must wrap it in brunner{} before submitting, for example brunner{t3st_fl4g}.

tìm flag rồi viết write up

Python

python
from PIL import Image import numpy as np, os, json, math, collections, statistics path="/mnt/data/misc_alternative-channel.zip" # confirm path os.path.exists(path), os.path.getsize(path) (True, 10317)

Python

python
from PIL import Image img=Image.open("/mnt/data/altchan/misc_alternative-channel/alternative_channel.png").convert("RGB") a=np.array(img) a.shape, a.dtype, a.min(), a.max() ((256, 320, 3), dtype('uint8'), np.uint8(0), np.uint8(255))

Python

python
# channel correlations and difference stats for i,name in enumerate("RGB"): vals=a[:,:,i].ravel() print(name, vals.mean(), vals.std(), len(np.unique(vals))) print("corr", np.corrcoef([a[:,:,i].ravel() for i in range(3)])) for i in range(3): for j in range(i+1,3): d=a[:,:,i].astype(int)-a[:,:,j].astype(int) print(i,j,"neq",np.count_nonzero(d), "maxabs",np.max(np.abs(d)), "meanabs",np.mean(np.abs(d)), "unique",len(np.unique(d)))

Python

python
central=a[52:209,:,:] for i in range(3): for j in range(i+1,3): d=central[:,:,i].astype(int)-central[:,:,j].astype(int) print(i,j,"neq",np.count_nonzero(d), "meanabs",np.mean(np.abs(d)), "max",np.max(np.abs(d)))

Python

python
# coordinates in central where channels differ mask=(central[:,:,0]!=central[:,:,1]) | (central[:,:,1]!=central[:,:,2]) ys,xs=np.where(mask) len(xs), (ys.min(),ys.max(),xs.min(),xs.max()), list(zip(ys[:20]+52,xs[:20],central[ys[:20],xs[:20]].tolist())) (320, (np.int64(156), np.int64(156), np.int64(0), np.int64(319)), [(np.int64(208), np.int64(0), [8, 10, 24]), (np.int64(208), np.int64(1), [8, 10, 24]), (np.int64(208), np.int64(2), [8, 10, 24]), (np.int64(208), np.int64(3), [8, 10, 24]), (np.int64(208), np.int64(4), [8, 10, 24]), (np.int64(208), np.int64(5), [8, 10, 24]), (np.int64(208), np.int64(6), [8, 10, 24]), (np.int64(208), np.int64(7), [8, 10, 24]), (np.int64(208), np.int64(8), [8, 10, 24]), (np.int64(208), np.int64(9), [8, 10, 24]), (np.int64(208), np.int64(10), [8, 10, 24]), (np.int64(208), np.int64(11), [8, 10, 24]), (np.int64(208), np.int64(12), [8, 10, 24]), (np.int64(208), np.int64(13), [8, 10, 24]), (np.int64(208), np.int64(14), [8, 10, 24]), (np.int64(208), np.int64(15), [8, 10, 24]), (np.int64(208), np.int64(16), [8, 10, 24]), (np.int64(208), np.int64(17), [8, 10, 24]), (np.int64(208), np.int64(18), [8, 10, 24]), (np.int64(208), np.int64(19), [8, 10, 24])])

Python

python
gray=a[:,:,0].astype(float) for y in range(45,215): row=gray[y]-gray[y].mean() spec=np.abs(np.fft.rfft(row)) spec[0]=0 k=np.argmax(spec) if y%5==0 or (50<=y<=70): print(y,k,spec[k],gray[y].min(),gray[y].max(),len(np.unique(gray[y])))

Python

python
import struct, zlib, binascii, os, io, itertools, collections, math png_path="/mnt/data/altchan/misc_alternative-channel/alternative_channel.png" data=open(png_path,'rb').read() pos=8 chunks=[] while pos<len(data): ln=struct.unpack(">I",data[pos:pos+4])[0] typ=data[pos+4:pos+8].decode('latin1') chunks.append((typ,ln,pos)) pos += 12+ln chunks, pos, len(data) ([('IHDR', 13, 8), ('IDAT', 10297, 33), ('IEND', 0, 10342)], 10354, 10354)

Python

python
# hash rows and find consecutive identical runs import hashlib hashes=[hashlib.md5(a[y].tobytes()).hexdigest() for y in range(a.shape[0])] runs=[] s=0 for y in range(1,len(hashes)+1): if y==len(hashes) or hashes[y]!=hashes[s]: runs.append((s,y-1,y-s,hashes[s])) s=y runs[:80], len(runs) ([(0, 10, 11, '71d3698647f05ff392866d24ad423d73'), (11, 11, 1, '3f4c0b5f56396f99adada156255c0555'), (12, 12, 1, '107c7873901dee93a7453acdc9937587'), (13, 13, 1, '07df54211eaae1c846ad1069697d88e2'), (14, 14, 1, 'c213c5e6d2acc8859e3d49b24f909d27'), (15, 15, 1, 'bfc538fbf7c04e1728144a236ecce41b'), (16, 16, 1, '4a1bc6ea83a68431616ac10238763d86'), (17, 17, 1, '701918715bb4f81c44e8e0ccd0cffbc6'), (18, 18, 1, '1d16c4031be58730c74f58347ef99237'), (19, 19, 1, 'bce0100f77b3b9592510ca7046693ff0'), (20, 20, 1, '8c24d841bc76b1d52f6e7c4b4a38d06a'), (21, 21, 1, '37d975e35b23210e81532756c0d289af'), (22, 22, 1, 'eefeb068b2a9f70f528c9221debf2888'), (23, 23, 1, '3897d23fa3ce112166fec0a5d950c157'), (24, 24, 1, 'c80e236fb73d952b2b04f70b456c3263'), (25, 25, 1, '5ce9a53dd13d75a21b51d0e2528fc4d4'), (26, 26, 1, 'ed37c7694a6fa1bf57943d47f533f676'), (27, 27, 1, '705d81563dc6216e6dbf8272d99c6670'), (28, 28, 1, '1f761a66fe47931532010019817ccca0'), (29, 37, 9, '71d3698647f05ff392866d24ad423d73'), (38, 38, 1, 'b074b615e265825b4de0b20964f08cf0'), (39, 39, 1, 'c9f530a9abcbbf31d8285d28c2b9aeab'), (40, 40, 1, '59ed807353852faaa01fe071fdce62e1'), (41, 41, 1, '881e5c44b9aa820b550a0741cedfc4b9'), (42, 42, 1, 'af933805f8ffb83d0ec6f55cc0e1bd52'), (43, 43, 1, '46a972b3037dc16e616c8f67cef85ecc'), (44, 44, 1, 'b44cbf7549495c5e1528ad896dbf6b39'), (45, 45, 1, 'bcc6134ea80801835894907c9ce38623'), (46, 46, 1, '1c8063864d79d29813c86a0f2cc44c09'), (47, 47, 1, '8b3df273ce4bc274af01972227af22e3'), (48, 50, 3, 'e2b1e53f26985bc0bc2a99c7d107a1d1'), (51, 51, 1, '001ab8988b108627601af2530fa79000'), (52, 52, 1, 'd049c4c266558e43288c74ca059b5076'), (53, 53, 1, '9a1fcc1b8b41e9f5d1746b05045512d6'), (54, 54, 1, '9a26fb087e0d355198edf8dc6cc56d3e'), (55, 59, 5, 'ded3432b71f209ad77908ba56c6429e8'), (60, 63, 4, '4f04af9d3d5b50e1706d2c436f79dd5f'), (64, 64, 1, 'e2b1e53f26985bc0bc2a99c7d107a1d1'), (65, 77, 13, '557606247aeae91b2113d416d9332aab'), (78, 79, 2, 'e2b1e53f26985bc0bc2a99c7d107a1d1'), (80, 83, 4, '59261ee76feea68f594e18e870b48376'), (84, 88, 5, 'e2b1e53f26985bc0bc2a99c7d107a1d1'), (89, 89, 1, '6549c7fa18babc0a1473cc90d3e6401e'), (90, 90, 1, 'd049c4c266558e43288c74ca059b5076'), (91, 92, 2, '9a1fcc1b8b41e9f5d1746b05045512d6'), (93, 97, 5, 'ded3432b71f209ad77908ba56c6429e8'), (98, 101, 4, '4f04af9d3d5b50e1706d2c436f79dd5f'), (102, 105, 4, 'e2b1e53f26985bc0bc2a99c7d107a1d1'), (106, 109, 4, '59261ee76feea68f594e18e870b48376'), (110, 110, 1, 'eeb52547ecdd7829dd417a94e8e005df'), (111, 111, 1, 'ed76b3c5b9726fb3fbdc36b9311427fc'), (112, 112, 1, '691cc95d657b4905b8dcf93873d121b6'), (113, 113, 1, 'a79bb815c7e3e94a64f08d1eb459819e'), (114, 114, 1, '43b99d904cd7150a14e028710edcb01c'), (115, 115, 1, '7699548397745eac9686f5c07491003d'), (116, 116, 1, 'cd15afebeb42955134721b5ea5f2f9a7'), (117, 117, 1, 'a2c46059a2731eeb1ea3430af96108ea'), (118, 118, 1, '3ea79ddf8593442064a091fafdc042cc'), (119, 119, 1, '24a7abc90ff8d2a6eab277e9f449389b'), (120, 120, 1, '6fcb6d668fca8bf44d52916e23c0079f'), (121, 121, 1, 'e2b1e53f26985bc0bc2a99c7d107a1d1'), (122, 122, 1, '9aec9472f6895ff9f2968ffedc44cbde'), (123, 123, 1, '3ae922520910599ffa65aae0a5660eef'), (124, 124, 1, '769be30d6c9579b265a4334408f94642'), (125, 125, 1, '21748b8e76ed349ac0881b71bfc1fafc'), (126, 128, 3, '92a2cc7011c779fbf54f34b38391b557'), (129, 129, 1, '057ca17929c4035249dc6711e75f81bc'), (130, 130, 1, 'e1a667e58dea25337aeaed1ddd5700a2'), (131, 131, 1, 'ded3432b71f209ad77908ba56c6429e8'), (132, 132, 1, '99a646196cea9c82d13c5336a057f95c'), (133, 133, 1, 'cf12901ebf1558f513940931ceeeb296'), (134, 134, 1, '65f38efec32603272873ef1792e506d0'), (135, 135, 1, 'b6c7d2203e71f91b11ea78e955c75d23'), (136, 137, 2, 'e2b1e53f26985bc0bc2a99c7d107a1d1'), (138, 149, 12, '557606247aeae91b2113d416d9332aab'), (150, 150, 1, 'e2b1e53f26985bc0bc2a99c7d107a1d1'), (151, 151, 1, '098205469e1108e018c7775c27c51728'), (152, 152, 1, '0e0f9516a2a6d6146c8eeb0c96be1ff3'), (153, 153, 1, 'a109a37c3a9f92c8e1f446f31835ac8a'), (154, 154, 1, '399a5a8f06e95c3fcf53f0347639de04')], 150)

Python

python
runs[80:140] [(155, 155, 1, '7677b2b6622101f6b12fb3b391ec99f7'), (156, 156, 1, '96fce4850e75174654bf9a4df151c6e8'), (157, 157, 1, 'c9e1c2bc9ee1bc24107655207ac3d1ca'), (158, 158, 1, '20656b0ac4cb81a4da636ce49fb8579f'), (159, 159, 1, 'a6a75bd0847ac5ed5354850ebae79369'), (160, 160, 1, 'fb248f0eafc3a57c1d9ea3f3f650a30e'), (161, 161, 1, '46e9c7a25f044917b6e06db72d1af6f1'), (162, 162, 1, '81feedba4ff2c1c0943490f597f3347a'), (163, 163, 1, '3ce55e63ddf94a726aa73b4f98caeeee'), (164, 165, 2, 'e2b1e53f26985bc0bc2a99c7d107a1d1'), (166, 166, 1, 'e64ba7bf4af38e3d313958dfce961d2c'), (167, 167, 1, 'd2d6f1c8c02df40a74599cbb9cc21f40'), (168, 168, 1, 'a109a37c3a9f92c8e1f446f31835ac8a'), (169, 169, 1, '91d341f968484ff2a1939ddd57413b54'), (170, 170, 1, '7677b2b6622101f6b12fb3b391ec99f7'), (171, 171, 1, '96fce4850e75174654bf9a4df151c6e8'), (172, 172, 1, 'c9e1c2bc9ee1bc24107655207ac3d1ca'), (173, 173, 1, '20656b0ac4cb81a4da636ce49fb8579f'), (174, 174, 1, '5aa8b686f0fb643bc40d0bc49defe325'), (175, 175, 1, 'c379699ae57e79875db05987d086a6f5'), (176, 176, 1, '46e9c7a25f044917b6e06db72d1af6f1'), (177, 177, 1, '81feedba4ff2c1c0943490f597f3347a'), (178, 178, 1, '403910b2622bf67449e2cbf21b3107ea'), (179, 179, 1, 'e2b1e53f26985bc0bc2a99c7d107a1d1'), (180, 181, 2, 'bdd0348e46fa073e24d6283ec779ef61'), (182, 182, 1, '2b13877d9b8c9e1c33856de3d1f0be30'), (183, 183, 1, '230a3f955f1bc911a1b8b79b1c6919f8'), (184, 184, 1, 'eaa0151016d2cbc4aace713cc9141ed0'), (185, 186, 2, '6c14539c1c574ec991e5462c8311f98f'), (187, 187, 1, '2f3684a0cdb9757ff303b3ba7448baf6'), (188, 190, 3, '97960c1bc51bcaab1ba59962b0e3d6a9'), (191, 191, 1, 'e2b1e53f26985bc0bc2a99c7d107a1d1'), (192, 192, 1, 'da649e1df2db36021e9ca31dc8199f36'), (193, 193, 1, 'eca21adb529123e983f27418ffb64937'), (194, 194, 1, '1f88b84b55490f7bd99be0bf275d8b7c'), (195, 195, 1, 'cc72ea4015ddc35754af50a4c0a2354a'), (196, 196, 1, '551dff0095db78139e8cf4e7eaac8d63'), (197, 197, 1, 'd415e9a19182b616cfc1e2d1663de6b9'), (198, 198, 1, 'faa86ec0de05b42c751095113145f99e'), (199, 199, 1, 'fbb32935232f70fa304cfc79b7e4ac52'), (200, 200, 1, '813305ba3099e1dfc22326d3bc16ee9d'), (201, 201, 1, '76e2ad4792d52e2f9cb250b6838e0aee'), (202, 202, 1, 'bd13abd02e43de87e418bfa861c04115'), (203, 203, 1, '924fda80c0948bd9ac23bccf7a102741'), (204, 204, 1, '3d97b57f8c1b730a50d9eda1fe1e258a'), (205, 205, 1, '286539bb24273f8cab853a98d6c82d00'), (206, 206, 1, 'bdd0348e46fa073e24d6283ec779ef61'), (207, 207, 1, '04997cd63b22f71c07ab41218c6019ac'), (208, 211, 4, '8b3df273ce4bc274af01972227af22e3'), (212, 223, 12, '25de8268ebc07a6da5c436d74452cefb'), (224, 224, 1, '2ddfea9ae7d7bdae28b29bbd37419afc'), (225, 225, 1, '166cdde2745d913270c31a69d4353eb7'), (226, 226, 1, '9d54150d26af6a4c5ea511df3460a392'), (227, 227, 1, 'bbdff3c7ef6308048f3e76b3709d0794'), (228, 228, 1, 'f7c8ae617b5eb12f4ea72d2fe4174e07'), (229, 229, 1, '9d069ff6016df705ee4f33870fcfac18'), (230, 230, 1, 'ad765d88b84832f5f8bba6ae20d823fa'), (231, 231, 1, '38d5bbe36eeca1d7db06f23170ff71ac'), (232, 232, 1, '13624b93ed2868a5213c3058c273d76f'), (233, 244, 12, '25de8268ebc07a6da5c436d74452cefb')]

Python

python
import numpy as np, scipy.io.wavfile as wavfile, matplotlib.pyplot as plt, os, math sig = a[51:208,:,0].astype(np.float32) # center around row or global 128? flat = (sig.ravel()-128)/128 wavfile.write('/mnt/data/central48k.wav',48000,(flat*30000).astype(np.int16)) len(flat), len(flat)/48000 (50240, 1.0466666666666666)

Python

python
# print stats rows 51-207 stats=[] for y in range(51,208): row=a[y,:,0].astype(float) spec=np.abs(np.fft.rfft(row-row.mean())); spec[0]=0 k=int(np.argmax(spec)) stats.append((y,row.mean(),row.std(),row.min(),row.max(),len(np.unique(row)),k)) stats[:20], stats[-10:] ([(51, np.float64(154.809375), np.float64(54.843053681477066), np.float64(85.0), np.float64(219.0), 21, 5), (52, np.float64(155.609375), np.float64(55.50754937041785), np.float64(85.0), np.float64(219.0), 20, 5), (53, np.float64(157.09375), np.float64(56.886487507469646), np.float64(85.0), np.float64(222.0), 20, 5), (54, np.float64(157.70625), np.float64(57.5294051849791), np.float64(85.0), np.float64(224.0), 21, 5), (55, np.float64(156.31875), np.float64(41.934006467752404), np.float64(85.0), np.float64(224.0), 52, 2), (56, np.float64(156.31875), np.float64(41.934006467752404), np.float64(85.0), np.float64(224.0), 52, 2), (57, np.float64(156.31875), np.float64(41.934006467752404), np.float64(85.0), np.float64(224.0), 52, 2), (58, np.float64(156.31875), np.float64(41.934006467752404), np.float64(85.0), np.float64(224.0), 52, 2), (59, np.float64(156.31875), np.float64(41.934006467752404), np.float64(85.0), np.float64(224.0), 52, 2), (60, np.float64(97.378125), np.float64(7.695219066691668), np.float64(85.0), np.float64(109.0), 10, 11), (61, np.float64(97.378125), np.float64(7.695219066691668), np.float64(85.0), np.float64(109.0), 10, 11), (62, np.float64(97.378125), np.float64(7.695219066691668), np.float64(85.0), np.float64(109.0), 10, 11), (63, np.float64(97.378125), np.float64(7.695219066691668), np.float64(85.0), np.float64(109.0), 10, 11), (64, np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), 1, 0), (65, np.float64(46.54375), np.float64(5.611201826480669), np.float64(38.0), np.float64(55.0), 7, 15), (66, np.float64(46.54375), np.float64(5.611201826480669), np.float64(38.0), np.float64(55.0), 7, 15), (67, np.float64(46.54375), np.float64(5.611201826480669), np.float64(38.0), np.float64(55.0), 7, 15), (68, np.float64(46.54375), np.float64(5.611201826480669), np.float64(38.0), np.float64(55.0), 7, 15), (69, np.float64(46.54375), np.float64(5.611201826480669), np.float64(38.0), np.float64(55.0), 7, 15), (70, np.float64(46.54375), np.float64(5.611201826480669), np.float64(38.0), np.float64(55.0), 7, 15)], [(198, np.float64(116.478125), np.float64(17.84322760837778), np.float64(85.0), np.float64(145.0), 23, 5), (199, np.float64(104.2125), np.float64(11.879019056723497), np.float64(85.0), np.float64(123.0), 15, 7), (200, np.float64(107.44375), np.float64(13.588481737762317), np.float64(85.0), np.float64(129.0), 17, 6), (201, np.float64(119.8375), np.float64(20.906454595411436), np.float64(85.0), np.float64(153.0), 26, 4), (202, np.float64(133.81875), np.float64(26.701678756915264), np.float64(88.0), np.float64(175.0), 33, 3), (203, np.float64(151.984375), np.float64(23.352524078980736), np.float64(109.0), np.float64(189.0), 30, 3), (204, np.float64(160.3), np.float64(17.808460068181077), np.float64(129.0), np.float64(189.0), 23, 5), (205, np.float64(169.034375), np.float64(12.422839585190458), np.float64(148.0), np.float64(189.0), 16, 7), (206, np.float64(178.015625), np.float64(7.012694265357289), np.float64(167.0), np.float64(189.0), 9, 12), (207, np.float64(189.0), np.float64(0.0), np.float64(189.0), np.float64(189.0), 1, 0)])

Python

python
arr=a def bits_to_bytes(bits, msb_first=True, offset=0): bits=np.asarray(bits,dtype=np.uint8)[offset:] n=(len(bits)//8)*8 bits=bits[:n].reshape(-1,8) if msb_first: weights=(1<<np.arange(7,-1,-1)) else: weights=(1<<np.arange(8)) return bytes((bits*weights).sum(axis=1).astype(np.uint8).tolist()) def printable_runs(b,minlen=6): out=[] start=None for i,x in enumerate(b): ok=32<=x<127 if ok and start is None: start=i if (not ok or i==len(b)-1) and start is not None: end=i if not ok else i+1 if end-start>=minlen: out.append((start,b[start:end])) start=None return out results=[] orders=[('row',arr),('col',arr.transpose(1,0,2))] for oname,ar in orders: flat=ar.reshape(-1,3) for chans in [(0,), (1,), (2,), (0,1,2),(2,1,0),(0,2,1),(1,0,2),(1,2,0),(2,0,1)]: vals=flat[:,chans].reshape(-1) for bit in range(8): bits=(vals>>bit)&1 for msb in [True,False]: for off in range(8): b=bits_to_bytes(bits,msb,off) runs=printable_runs(b,8) for st,s in runs[:50]: # score if relevant chars text=s.decode('ascii','ignore') if any(k in text.lower() for k in ['flag','brunner','sstv','channel','frog','_']) or len(text)>=20: results.append((oname,chans,bit,msb,off,st,text[:200])) len(results) 0

Python

python
from PIL import Image pairs=[(0,1,'R-G'),(1,2,'G-B'),(0,2,'R-B')] for i,j,name in pairs: d=a[:,:,i].astype(np.int16)-a[:,:,j].astype(np.int16) # symmetric stretch mn,mx=d.min(),d.max() out=((d-mn)*255/(mx-mn)).astype(np.uint8) Image.fromarray(out).save(f'/mnt/data/{name}.png') print(name,mn,mx, np.count_nonzero(d))

Python

python
regions={'central':a[51:208], 'signal':a[51:207], 'all':a} def search_region(ar): outs=[] variants=[('row',ar),('col',ar.transpose(1,0,2))] for oname,x in variants: flatpix=x.reshape(-1,3) for chans in [(0,), (1,), (2,), (0,1,2),(2,1,0)]: vals=flatpix[:,chans].reshape(-1) # take bit planes, or multiple LSBs interleaved per value streams=[] for bit in range(8): streams.append((f'bit{bit}',((vals>>bit)&1).astype(np.uint8))) for nbits in range(1,5): # zsteg style per sample bits lsb-> or msb within sample bmat=np.stack([((vals>>b)&1) for b in range(nbits)],axis=1) streams.append((f'lsb{nbits}-lowfirst',bmat.ravel())) streams.append((f'lsb{nbits}-highfirst',bmat[:,::-1].ravel())) for sname,bits in streams: for msb in [True,False]: for off in range(8): bb=bits_to_bytes(bits,msb,off) # detect likely ascii sequences 6+ runs=printable_runs(bb,6) for st,s in runs: txt=s.decode('ascii','ignore') # score broad if len(txt)>=12 or any(k in txt.lower() for k in ['flag','sstv','frog','channel','brunner','_']): outs.append((oname,chans,sname,msb,off,st,txt[:300])) return outs for rn,reg in regions.items(): outs=search_region(reg) print(rn,len(outs),outs[:20])

Python

python
# all-black rows central black=[y for y in range(51,208) if np.all(a[y,:,0]==0)] black [64, 78, 79, 84, 85, 86, 87, 88, 102, 103, 104, 105, 121, 136, 137, 150, 164, 165, 179, 191]

Python

python
blocks=[] start=None for y in range(51,208): nz=not np.all(a[y,:,0]==0) if nz and start is None: start=y if (not nz or y==207) and start is not None: end=y-1 if not nz else y blocks.append((start,end,end-start+1)) start=None blocks [(51, 63, 13), (65, 77, 13), (80, 83, 4), (89, 101, 13), (106, 120, 15), (122, 135, 14), (138, 149, 12), (151, 163, 13), (166, 178, 13), (180, 190, 11), (192, 207, 16)]

Python

python
# list y, dominant fft bin, mean, std, maybe first few unique row hashes labels for y in range(51,208): row=a[y,:,0].astype(float) if np.all(row==0): print(f"{y:3d}: BLACK") continue x=row-row.mean() sp=np.abs(np.fft.rfft(x)) sp[0]=0 top=np.argsort(sp)[-5:][::-1] print(f"{y:3d}: k={top[0]:3d} mean={row.mean():6.1f} std={row.std():5.1f} top={top[:3].tolist()}")

Python

python
pairs=[((51,63),(89,101)),((65,77),(138,149)),((151,163),(166,178))] for (s1,e1),(s2,e2) in pairs: n=min(e1-s1+1,e2-s2+1) d=a[s1:s1+n,:,0].astype(int)-a[s2:s2+n,:,0].astype(int) print((s1,s2),"n",n,"neq",np.count_nonzero(d),"max",np.max(np.abs(d)),"rows neq",[np.count_nonzero(d[i]) for i in range(n)])

Python

python
for y in [52,55,60,65,80,90,110,123,151,166,180,192,200]: row=a[y,:,0].astype(float); x=row-row.mean() sp=np.abs(np.fft.rfft(x)); sp[0]=0 tops=np.argsort(sp)[-15:][::-1] print("y",y, [(int(k), round(float(sp[k]),1)) for k in tops[:10]])

Python

python
# parse IDAT payload and filter types import struct,zlib,collections data=open(png_path,'rb').read() pos=8 idat=b'' while pos<len(data): ln=struct.unpack(">I",data[pos:pos+4])[0] typ=data[pos+4:pos+8] payload=data[pos+8:pos+8+ln] if typ==b'IDAT': idat+=payload pos+=12+ln raw=zlib.decompress(idat) len(raw), (320*3+1)*256 (246016, 246016)

Python

python
rowlen=320*3+1 filters=[raw[y*rowlen] for y in range(256)] collections.Counter(filters), filters[:80] (Counter({2: 121, 1: 63, 4: 49, 0: 23}), [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 2, 4, 2, 2, 2, 4, 4, 2, 2, 2, 2, 2, 2, 4, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 2, 2, 2, 4, 2, 4, 4, 1, 0, 0, 0, 1, 4, 4, 1, 1, 2, 2, 2, 2, 1, 2, 2, 2, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0])

Python

python
rowlen=320*3+1 filters=[raw[y*rowlen] for y in range(256)] collections.Counter(filters), filters[:80] (Counter({2: 121, 1: 63, 4: 49, 0: 23}), [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 2, 4, 2, 2, 2, 4, 4, 2, 2, 2, 2, 2, 2, 4, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 2, 2, 2, 4, 2, 4, 4, 1, 0, 0, 0, 1, 4, 4, 1, 1, 2, 2, 2, 2, 1, 2, 2, 2, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0])

Python

python
import itertools, string, math, re types=sorted(set(filters)) cands=[] for perm in itertools.permutations(range(4)): mp=dict(zip(types,perm)) vals=[mp[x] for x in filters] # choices: per symbol bits big/little, stream pack offsets for symbit_order in [0,1]: bits=[] for v in vals: pair=[(v>>1)&1, v&1] if symbit_order==0 else [v&1,(v>>1)&1] bits += pair for revstream in [False, True]: bs=bits[::-1] if revstream else bits for off in range(8): for byte_msb in [True,False]: b=bits_to_bytes(bs,byte_msb,off) # score printable ratio & known patterns pr=sum(32<=x<127 for x in b)/max(1,len(b)) text=''.join(chr(x) if 32<=x<127 else '.' for x in b) if pr>0.65 or re.search(r'[A-Za-z0-9_]{8,}',text): cands.append((pr,mp,symbit_order,revstream,off,byte_msb,text)) len(cands) 160

Python

python
for x in sorted(cands, reverse=True, key=lambda z:z[0])[:30]: print(x[0],x[1],x[2:6],x[6])

Python

python
for y in [55,60,65,80,110,123,151,192]: print("\ny",y) print(a[y,:80,0].tolist())

Python

python
vals=np.unique(a[51:208,:,0]) vals.tolist(), len(vals) ([0, 38, 41, 44, 46, 49, 52, 55, 82, 85, 88, 90, 93, 96, 98, 101, 104, 107, 109, 112, 115, 118, 120, 123, 126, 129, 131, 134, 137, 140, 142, 145, 148, 151, 153, 156, 159, 161, 164, 167, 170, 172, 175, 178, 181, 183, 186, 189, 192, 194, 197, 200, 203, 205, 208, 211, 214, 216, 219, 222, 224, 227, 230, 233], 64)

Python

python
central_g=a[51:208,:,0] uniq=sorted(np.unique(central_g).tolist()) rank={v:i for i,v in enumerate(uniq)} idx=np.vectorize(rank.get)(central_g).astype(np.uint8) print(uniq[:10],uniq[-10:],len(uniq)) # pack 6-bit symbols row-major -> bytes (4 sextets -> 3 bytes) bits=[] for v in idx.ravel(): bits += [(v>>b)&1 for b in range(5,-1,-1)] b=bits_to_bytes(bits,True,0) print(len(b), printable_runs(b,8)[:20]) # base64 chars from ranks alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" s=''.join(alphabet[v] for v in idx.ravel()) print(s[:500])

Python

python
for y in [55,60,65,80,110,123,151,192]: r=idx[y-51] print("y",y,"r first 80:",r[:80].tolist())

Python

python
ds=idx[:,::3] # 157 x 107 ds.shape (157, 107)

Python

python
def pack6(vals, sextet_msb=True, bitstream_msb=True, offset_bits=0): bits=[] for v in vals: rng=range(5,-1,-1) if sextet_msb else range(6) bits.extend((int(v)>>b)&1 for b in rng) return bits_to_bytes(bits, bitstream_msb, offset_bits) variants=[] for ordername,mat in [('row',ds),('col',ds.T),('rowflipx',ds[:,::-1]),('rowflipy',ds[::-1]),('both',ds[::-1,::-1])]: vals=mat.ravel() for sm in [True,False]: for bm in [True,False]: for off in range(8): bb=pack6(vals,sm,bm,off) runs=printable_runs(bb,8) if runs: variants.append((ordername,sm,bm,off,runs[:10])) len(variants) 86

Python

python
for v in variants[:50]: print(v)

Python

python
def min_period(seq): seq=list(map(int,seq)) n=len(seq) for p in range(1,n+1): if all(seq[i]==seq[i%p] for i in range(n)): return p # allow prefix of repeating period since width not multiple for p in range(1,n): if all(seq[i]==seq[i%p] for i in range(n)): return p return n # better find p that matches all positions i>=p def period(seq): seq=np.asarray(seq) for p in range(1,len(seq)): if np.all(seq[p:]==seq[:-p]): return p return len(seq) for y in range(51,208): r=ds[y-51] if np.all(r==0): continue p=period(r) if y in [51,52,53,54,55,60,65,80,89,90,110,123,151,166,180,192,200,207]: print(y,p,r[:min(p,40)].tolist(), "minmax",r.min(),r.max())

Python

python
import base64, binascii, re, numpy as np, math, statistics alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" reg=idx # 157 x320 ranks seqs={} seqs['min']=[int(r.min()) for r in reg] seqs['max']=[int(r.max()) for r in reg] seqs['first']=[int(r[0]) for r in reg] seqs['last']=[int(r[-1]) for r in reg] seqs['mean_round']=[int(round(r.mean()))%64 for r in reg] seqs['mid']=[int(round((r.min()+r.max())/2))%64 for r in reg] seqs['range']=[int(r.max()-r.min())%64 for r in reg] seqs['uniq']=[len(np.unique(r))%64 for r in reg] # period on downsampled seqs['period']=[period(ds[i])%64 for i in range(ds.shape[0])] # first differences count/wrap count seqs['jumps']=[int(np.sum(np.abs(np.diff(ds[i].astype(int)))>1))%64 for i in range(ds.shape[0])] def try_b64chars(vals): s=''.join(alphabet[v%64] for v in vals) outs=[] for rev in [False,True]: ss=s[::-1] if rev else s for off in range(4): t=ss[off:] t=t[:len(t)//4*4] try: b=base64.b64decode(t,validate=False) except: continue runs=printable_runs(b,4) pr=sum(32<=x<127 for x in b)/max(1,len(b)) outs.append((rev,off,pr,runs[:10],b[:100])) return s,outs for name,vals in seqs.items(): s,outs=try_b64chars(vals) best=max(outs,key=lambda x:x[2]) if outs else None print("\n",name, s) print("best",best[:4] if best else None, "prefix", best[4] if best else None)

Python

python
pseq=seqs['period'] print(pseq)

Python

python
for y,(mn,mx,pr) in enumerate(zip(seqs['min'],seqs['max'],seqs['period']),start=51): rng=mx-mn print(f"{y:3d} min={mn:2d} max={mx:2d} rng={rng:2d} per={pr:2d}")

Python

python
res=np.zeros_like(ds,dtype=int) ps=[] for i,row in enumerate(ds): p=period(row) ps.append(p) carrier=np.array([p-1-(j%p) for j in range(row.size)]) res[i]=row.astype(int)-carrier print("res range",res.min(),res.max(),"unique",sorted(np.unique(res).tolist())) # save normalized and per-bit lo,hi=res.min(),res.max() out=((res-lo)*255/(hi-lo)).astype(np.uint8) Image.fromarray(out).save('/mnt/data/residual.png') for b in range(6): Image.fromarray((((res>>b)&1)*255).astype(np.uint8)).save(f'/mnt/data/res_b{b}.png')

Python

python
for i in range(res.shape[0]): y=i+51 u=np.unique(res[i]) if len(u)<=8: print(y,len(u),u.tolist())

Python

python
def run_count(seq): return 1+int(np.sum(np.asarray(seq)[1:]!=np.asarray(seq)[:-1])) runc=[run_count(r) for r in res] print(runc) print(max(runc))

Python

python
offset0=[int(res[i,0])%64 for i in range(res.shape[0])] print(offset0) s=''.join(alphabet[v] for v in offset0) print(s) # try pack sextets directly to bytes and base64-decode string variants for rev in [False,True]: vals=offset0[::-1] if rev else offset0 for sextet_msb in [True,False]: for byte_msb in [True,False]: for off in range(8): bb=pack6(vals,sextet_msb,byte_msb,off) runs=printable_runs(bb,4) if runs: maxrun=max(runs,key=lambda x:len(x[1])) if len(maxrun[1])>=6: print("pack",rev,sextet_msb,byte_msb,off,maxrun) ss=''.join(alphabet[v] for v in vals) for o in range(4): t=ss[o:]; t=t[:len(t)//4*4] try: bb=base64.b64decode(t) except: continue runs=printable_runs(bb,4) if runs: print("b64",rev,o,runs[:5])

Python

python
# exhaustive bit packing residual def search_bitstreams(matrix): outs=[] mats=[('row',matrix),('col',matrix.T),('flipx',matrix[:,::-1]),('flipy',matrix[::-1]),('both',matrix[::-1,::-1])] for name,m in mats: vals=m.ravel().astype(np.uint8) streams=[] for bit in range(6): streams.append((f'b{bit}', (vals>>bit)&1)) for n in range(1,7): bm=np.stack([((vals>>b)&1) for b in range(n)],axis=1) streams.append((f'lsb{n}lo',bm.ravel())) streams.append((f'lsb{n}hi',bm[:,::-1].ravel())) for sname,bits in streams: for rev in [False,True]: x=bits[::-1] if rev else bits for off in range(8): for msb in [True,False]: bb=bits_to_bytes(x,msb,off) # scan printable runs >= 6 and flags for st,sr in printable_runs(bb,6): txt=sr.decode('ascii','ignore') if len(txt)>=12 or '_' in txt or 'flag' in txt.lower() or 'sstv' in txt.lower(): outs.append((name,sname,rev,off,msb,st,txt[:200])) return outs outs=search_bitstreams(res) len(outs) 10554

Python

python
# rank candidate texts by allowed flag-like char ratio and run length sc=[] for rec in outs: txt=rec[-1] allowed=sum(c.isalnum() or c in "_{}-" for c in txt)/len(txt) sc.append((allowed,len(txt),rec)) for score,l,rec in sorted(sc, reverse=True)[:50]: print(score,l,rec)

विचार

Recovered hidden text through image channel and glyph analysis

Worked for 18m 2s

उत्तर

Flag tìm được là:

brunner{1_l1k3_sstv}

:::writing{variant="document" id="58321"}

Alternative channel — Write-up

Challenge

  • Category: Stego
  • Difficulty: Medium
  • Hint chính: SSTV transmission, “alternative channel”
  • File: alternative_channel.png

Mục tiêu là tìm phần nội dung flag ẩn trong tín hiệu rồi bọc bằng brunner{}.

1. Quan sát ban đầu

Giải nén file:

bash
unzip misc_alternative-channel.zip

Bên trong chỉ có một ảnh PNG kích thước 320×256.

Nhìn bằng mắt thường, phần giữa ảnh chứa nhiều sọc nhiễu giống tín hiệu bị hỏng:

text
BRUNNER CORP SECURE SSTV UPLINK ... !! TRANSMISSION DEGRADED !!

Không có metadata hay chunk PNG lạ đáng chú ý, nên khả năng cao dữ liệu nằm trực tiếp trong pixel.

2. Phân tích vùng “signal”

Vùng tín hiệu nằm khoảng:

python
signal = img[51:208, :, 0]

Điểm đáng chú ý đầu tiên: vùng này hoàn toàn grayscale, tức:

text
R = G = B

Và chỉ sử dụng đúng 64 mức xám khác nhau.

64 = 2^6, khá đáng ngờ đối với một bài stego.

Ta có thể sắp xếp 64 mức xám rồi ánh xạ chúng thành các giá trị:

text
0, 1, 2, ..., 63

Ngoài ra, mỗi giá trị trên trục ngang được lặp lại 3 pixel liên tiếp, vì vậy chỉ cần lấy:

python
ranks = ranks[:, ::3]

3. Phát hiện cấu trúc của tín hiệu

Sau khi đổi mức xám thành rank 0..63 và bỏ các pixel lặp, từng scanline có cấu trúc rất đặc biệt.

Ví dụ một hàng:

text
58 57 56 55 54 53 52 51 50 49 48 18 17 16 15 14 13 12 11 10 9 58 57 56 ...

Một hàng khác:

text
7 6 5 4 3 2 1 7 6 5 4 3 2 1 ...

Nghĩa là phần “nhiễu” thực chất được tạo bởi các đoạn đếm giảm tuần hoàn.

Nếu chỉ nhìn thứ tự pixel thì trông giống tín hiệu SSTV hỏng, nhưng có một cách nhìn khác: bỏ qua thứ tự xuất hiện và chỉ quan tâm scanline đó đã sử dụng những mức xám nào.

Đây chính là “alternative channel”.

4. Tái dựng kênh ẩn

Với mỗi hàng của tín hiệu:

  1. Lấy tất cả giá trị rank khác nhau xuất hiện trong hàng.
  2. Tạo một bitmap mới cao 63 pixel.
  3. Mỗi scanline cũ trở thành một cột của bitmap mới.
  4. Nếu rank v xuất hiện, tô pixel tương ứng.
  5. Các hàng hoàn toàn đen của ảnh gốc được coi như khoảng cách giữa các ký tự.

Code cốt lõi:

python
recovered = np.full((63, ranks.shape[0]), 255, dtype=np.uint8) for x, row in enumerate(ranks): if np.all(row == 0): continue for value in np.unique(row): if 1 <= value <= 63: recovered[63 - int(value), x] = 0

Ảnh sau khi tái dựng bị nén theo chiều ngang, nên kéo rộng khoảng 4 lần:

python
out = Image.fromarray(recovered) out = out.resize( (out.width * 4, out.height), Image.Resampling.NEAREST )

Lúc này dòng chữ hiện ra rất rõ:

text
1_l1k3_sstv

Có thể đọc thành leetspeak:

text
I LIKE SSTV

5. Solver hoàn chỉnh

python
from PIL import Image import numpy as np img = np.array(Image.open("alternative_channel.png").convert("RGB")) # Vùng transmission signal = img[51:208, :, 0] # Ánh xạ 64 mức xám -> 0..63 levels = np.unique(signal) rank_lut = {int(v): i for i, v in enumerate(levels)} ranks = np.vectorize(rank_lut.get)(signal) # Mỗi sample bị kéo dài 3 pixel ranks = ranks[:, ::3] # Recover alternative channel recovered = np.full((63, ranks.shape[0]), 255, dtype=np.uint8) for x, row in enumerate(ranks): # separator if np.all(row == 0): continue for value in np.unique(row): if 1 <= value <= 63: recovered[63 - int(value), x] = 0 out = Image.fromarray(recovered) # Sửa aspect ratio để đọc chữ out = out.resize( (out.width * 4, out.height), Image.Resampling.NEAREST ) out.save("recovered.png")

Flag

Nội dung thu được:

text
1_l1k3_sstv

Theo yêu cầu đề bài cần bọc bằng brunner{}:

text
brunner{1_l1k3_sstv}

:::

Mình đã lưu luôn ảnh đã recoversolver Python.

इस प्रश्नोत्तर को साझा करें