今年的 TPCTF 我随 0xFFF 参赛,并且拿下了两道 nanonymous 题目的一血,以下是题解。

nanonymous spam

本题给了一个匿名留言板网站,其中已有一堆 Spam 留言。在最底下有一个发送留言的部分,注意到网站已经给访问者指定了一个昵称。尝试使用不同的 IP 访问该网站,不难发现这个生成的昵称是根据 IP 地址生成的:

1
2
3
109.110.162.51 => PowSamPenWay
45.146.163.41 => FatUavRohHae
185.206.249.31 => LayHebLobLoa

猜测本题是需要探索出这个昵称的生成算法,然后将那些 Spam 留言的昵称反推回 IP 地址。但是这需要大量的 IP 地址和昵称的对应数据,显然我们很难找一堆 IP 来爆数据,所以我探究其是否可以伪造访问者的 IP 地址。

经过一番尝试,发现可以通过 X-Real-IP 这个 HTTP 头来伪造 IP 地址。并且也发现第一个 Spam 留言者昵称对应的 IP 地址正是 TPCT 这个 Flag 头的 ASCII 码,这证实了猜想:

1
2
3
4
5
6
7
8
9
10
11
import requests
from bs4 import BeautifulSoup

def get(ip: str) -> str:
response = requests.get("[REDACTED]", headers={ "X-Real-IP": ip })
soup = BeautifulSoup(response.text, "html.parser")
item = soup.select_one("body > div > div:nth-child(2) > div").text
return item.split(" ")[-1]

ip = "%d.%d.%d.%d" % tuple(map(ord, "TPCT"))
assert get(ip) == "VicCouNeaGas"

那么我们先从 0.0.0.0 开始递增看一看是否能找到一些规律:

1
2
3
4
5
6
7
f = open("result.txt", "at")
for i in range(0, 512):
ip = i
a, b, c, d = ip >> 24, (ip >> 16) & 0xff, (ip >> 8) & 0xff, ip & 0xff
ip_str = "%d.%d.%d.%d" % (a, b, c, d)
f.write(f"{str(i).zfill(3)} {get(ip_str)} {ip_str}\n")
f.close()

结果如下,注意到昵称的第 4-6 个字符每 103 个 IP 地址会有一个周期,进入下一个周期改变了第 1-3 个字符的内容,其余字符则保持不变:

1
2
3
4
5
6
7
8
9
10
11
12
000 WimNodSerPuc 0.0.0.0
001 WimTapSerPuc 0.0.0.1
002 WimLizSerPuc 0.0.0.2
...
102 WimGuoSerPuc 0.0.0.102
103 HetNodSerPuc 0.0.0.103
104 HetTapSerPuc 0.0.0.104
...
205 HetGuoSerPuc 0.0.0.205
206 FauNodSerPuc 0.0.0.206
207 FauTapSerPuc 0.0.0.207
...

那就继续探索下一个周期变化。这个周期很长,所以直到将范围拉到 520 之后才确定其周期长度为 513:

1
2
3
for i in range(0, 520):
ip = i * 103
# 其余代码同上
1
2
3
4
5
6
7
8
000 WimNodSerPuc 0.0.0.0
001 HetNodSerPuc 0.0.0.103
002 FauNodSerPuc 0.0.0.206
...
512 ButNodSerPuc 0.0.206.0
513 WimNodDeaPuc 0.0.206.103
514 HetNodDeaPuc 0.0.206.206
...

进一步探索下一个周期。下一个周期的长度为 313,但同时也注意到,在进入下一个周期后,四个部分的顺序也发生了变化:

1
2
3
for i in range(0, 320):
ip = i * 103 * 513
# 其余代码同上
1
2
3
4
5
6
7
8
000 WimNodSerPuc 0.0.0.0
001 WimNodDeaPuc 0.0.206.103
002 WimNodJacPuc 0.1.156.206
...
312 WimNodFomPuc 0.251.141.136
313 WimSerMazNod 0.252.91.239
314 WimDeaMazNod 0.253.42.86
...

先对之前三个周期的昵称部分进行了整理,发现它们互相没有交集,所以顺序的变化不影响对下一个周期的探索:

1
2
3
A  =  ['Nod', 'Tap', 'Liz', 'Mel', 'Fig', 'Rif', 'Rip', 'Pud', 'Foo', 'Haw', 'Wef', 'Kel', 'Gat', 'Hod', 'Mom', 'Lin', 'Fez', 'Rua', 'Fay', 'Pat', 'Ned', 'Taz', 'Sid', 'Mic', 'Nom', 'Hab', 'Rug', 'Men', 'Nok', 'Fun', 'Pox', 'Red', 'Jah', 'Tet', 'Hip', 'Tem', 'Bad', 'Mir', 'Taj', 'Maf', 'Rac', 'Zia', 'Hea', 'Fis', 'Dem', 'Bim', 'Gow', 'Hub', 'Job', 'Nex', 'Jas', 'Lie', 'Sim', 'Poc', 'Ran', 'Voa', 'Gig', 'Jes', 'Nie', 'Lal', 'Lek', 'Pen', 'Cos', 'Col', 'Nao', 'Mop', 'Bac', 'Cis', 'Mor', 'Vim', 'Ceo', 'Gic', 'Mii', 'Dep', 'Len', 'Few', 'Lob', 'Lea', 'Bec', 'Mui', 'Pec', 'Mab', 'Her', 'Tas', 'Tui', 'Kun', 'Vic', 'Too', 'Woe', 'Uav', 'Dam', 'Jin', 'Kaz', 'Yew', 'Cid', 'Jaw', 'Hay', 'Gib', 'Mis', 'Til', 'Six', 'Bot', 'Guo']
B = ['Wim', 'Het', 'Fau', 'Ria', 'Dio', 'God', 'Man', 'Lim', 'Fap', 'Bar', 'Sot', 'Uae', 'Faq', 'Gum', 'Doe', 'Kay', 'Vol', 'Bic', 'Ren', 'Sox', 'Ral', 'Pii', 'Fol', 'Noo', 'Wes', 'Law', 'Pic', 'Zig', 'Ric', 'Tad', 'Pav', 'Loo', 'Tea', 'Koh', 'Fia', 'Rep', 'Soa', 'Gog', 'Rim', 'Nec', 'Jun', 'Sus', 'Roh', 'Sac', 'Diy', 'Gin', 'Gul', 'Via', 'Tec', 'Mah', 'Rus', 'Cal', 'Wat', 'Mes', 'Pam', 'Sav', 'Luz', 'Lac', 'Jud', 'Lop', 'Tub', 'Lia', 'Kip', 'Nau', 'Loa', 'Roa', 'Dos', 'Nor', 'Jaz', 'Fim', 'Boo', 'Pad', 'Duo', 'Min', 'Vis', 'Hux', 'Cue', 'Soc', 'Caw', 'Rig', 'Wod', 'Pag', 'Tak', 'Cag', 'Coe', 'Lev', 'Ted', 'Vax', 'Peo', 'Uic', 'Cus', 'Huh', 'Rub', 'Gia', 'Raf', 'Bed', 'Pei', 'Sig', 'Pur', 'Qin', 'Dai', 'Deb', 'Pof', 'Neg', 'Tol', 'Lux', 'Jus', 'Uah', 'Que', 'Noe', 'Lov', 'Zee', 'Con', 'Fey', 'Soi', 'Tex', 'Pin', 'Kap', 'Sal', 'Luo', 'Tim', 'Mid', 'Daw', 'Had', 'Gam', 'Jul', 'Jie', 'Wol', 'Mon', 'Roc', 'Rel', 'Bas', 'Nou', 'Reo', 'Mar', 'Dao', 'Niu', 'Kev', 'Dee', 'Wip', 'Coc', 'Fes', 'Rat', 'Dig', 'Teu', 'Mob', 'Mae', 'Car', 'Tux', 'Dew', 'Xue', 'Poi', 'Sit', 'Xin', 'Per', 'Mos', 'Top', 'Gab', 'Yin', 'Loi', 'Jay', 'Moi', 'Yeo', 'Day', 'Dic', 'Haq', 'Dak', 'Mer', 'Wii', 'Pix', 'Fag', 'Dog', 'Por', 'Nib', 'Hog', 'Huw', 'Voc', 'Hob', 'Zep', 'Neo', 'Com', 'Seo', 'Cur', 'Mow', 'Reb', 'Jim', 'Noc', 'Big', 'Fin', 'Sek', 'Fav', 'Niv', 'Pom', 'Pes', 'Ker', 'Yao', 'Coq', 'Tif', 'Gem', 'Cel', 'Zit', 'Toc', 'Jet', 'Vow', 'Lon', 'Rev', 'Joi', 'Jem', 'Wad', 'Bom', 'Tar', 'Pua', 'Rao', 'Bio', 'For', 'Dec', 'Win', 'See', 'Pup', 'Mea', 'Fam', 'Muh', 'Doo', 'Moh', 'Sam', 'Maw', 'Tog', 'Moe', 'Tin', 'Hur', 'Won', 'Lox', 'Poa', 'Dun', 'Run', 'Bil', 'Vip', 'Viv', 'Del', 'Nae', 'Zip', 'Roo', 'Sum', 'Leh', 'Lam', 'Yoo', 'Yip', 'Tow', 'Pil', 'Nab', 'Goi', 'Gar', 'Qua', 'Cor', 'Hav', 'Let', 'Ree', 'Set', 'Lee', 'Cef', 'Jam', 'Fal', 'Daa', 'Put', 'Num', 'Vod', 'Tis', 'Cad', 'Mot', 'Rit', 'Lex', 'Nav', 'Sia', 'Lip', 'Nox', 'Raj', 'Pie', 'Hel', 'Bam', 'Fed', 'Los', 'Fax', 'Neh', 'Jag', 'Sec', 'Jap', 'Sun', 'Cea', 'Jug', 'Sis', 'Cut', 'Fit', 'Fox', 'Bum', 'Joh', 'Lag', 'Fic', 'Sae', 'Gaz', 'Yuh', 'Hee', 'Fae', 'Caf', 'Nag', 'Bay', 'Ray', 'Log', 'Dim', 'Bag', 'Gap', 'San', 'Sup', 'Kuo', 'Wav', 'Suh', 'Kal', 'Tom', 'Ret', 'Seb', 'Wil', 'Jen', 'Haz', 'Cum', 'Xiv', 'Pon', 'Cod', 'Kit', 'Biz', 'Gag', 'Fen', 'Leg', 'Uid', 'Bod', 'Peg', 'Fur', 'Pip', 'Vid', 'Ter', 'Mol', 'Yor', 'Tek', 'Koo', 'Sui', 'Gis', 'Cia', 'Jig', 'Nad', 'Sin', 'Wop', 'Hou', 'Xii', 'Mim', 'Naa', 'Nia', 'Fai', 'Cat', 'Mio', 'Vee', 'Sew', 'Pal', 'Bub', 'Lis', 'Cac', 'Bid', 'Pah', 'Dip', 'Goy', 'Rum', 'Hoc', 'Viz', 'Fog', 'Tax', 'Kin', 'Req', 'Kik', 'Coa', 'Meh', 'Mum', 'Lap', 'Mov', 'Pir', 'Bop', 'Der', 'Dag', 'Lei', 'Jit', 'Tod', 'Far', 'Tig', 'Tae', 'Ten', 'Toe', 'Sep', 'Mac', 'Hua', 'Vik', 'Piu', 'Rar', 'Hut', 'New', 'Pap', 'Hid', 'Xia', 'Hug', 'Rox', 'Rey', 'Meg', 'Zak', 'Uas', 'Dug', 'Bes', 'Ton', 'Lad', 'Hus', 'Lew', 'Jiu', 'Pub', 'Buy', 'Bet', 'Nog', 'Yak', 'Bau', 'Qol', 'Yet', 'Dor', 'Buh', 'Baz', 'Kat', 'Fei', 'Kon', 'Nuh', 'Noa', 'Cap', 'Cil', 'Tan', 'Jed', 'Dur', 'Bol', 'Sux', 'Gov', 'Dev', 'Teh', 'Bob', 'Bal', 'Pep', 'Hah', 'Res', 'Cai', 'Gas', 'Qiu', 'Wiz', 'Pis', 'Heh', 'Dil', 'Yer', 'Gon', 'Nis', 'Fiu', 'Ber', 'Gan', 'Bak', 'Fud', 'Cog', 'Zim', 'Doa', 'Bos', 'Hen', 'Hes', 'Dub', 'Web', 'Lol', 'Zoo', 'Vag', 'Lep', 'Vin', 'Cep', 'Sow', 'Naw', 'Mee', 'Vir', 'Jae', 'Lic', 'Gah', 'Wax', 'Zap', 'Bur', 'Civ', 'Tag', 'Led', 'Boe', 'Cin', 'You', 'Daf', 'Beg', 'Xan', 'Wix', 'Nun', 'Yap', 'Bai', 'Cox', 'Sur', 'Fet', 'Moj', 'Lau', 'Dis', 'Mat', 'Rid', 'Mal', 'Ris', 'Uis', 'Hib', 'Vie', 'But']
C = ['Ser', 'Dea', 'Jac', 'Way', 'Cio', 'Tie', 'Tun', 'Goa', 'Sap', 'Fan', 'Jor', 'Pit', 'Gor', 'Son', 'Mun', 'Dan', 'Veg', 'Wel', 'Sev', 'Jeb', 'Gio', 'Ceu', 'Bib', 'Cif', 'Bug', 'Zan', 'Mec', 'Rob', 'Lao', 'Hew', 'Quo', 'Hor', 'Foe', 'Mak', 'Hol', 'Fil', 'Cam', 'Nur', 'Vet', 'Yea', 'Yup', 'Lot', 'Jab', 'Goo', 'Soy', 'Pay', 'Hoe', 'Dud', 'Qos', 'Boa', 'Ceb', 'Lug', 'Nic', 'Rai', 'Nap', 'Sem', 'Rue', 'Bah', 'Sez', 'Jib', 'Ual', 'Mus', 'Cip', 'Cir', 'Yan', 'Div', 'Bor', 'War', 'Don', 'Tug', 'Tuk', 'Maj', 'Hae', 'Rui', 'Git', 'Gil', 'Lab', 'Med', 'Mag', 'Dui', 'Ruv', 'Raw', 'Sol', 'Foy', 'Sib', 'Sub', 'Moz', 'Ras', 'Mil', 'Rem', 'Nix', 'Dom', 'Ban', 'Zeb', 'Woo', 'Pus', 'Mau', 'Boi', 'Ped', 'Kee', 'Pop', 'Mix', 'Wai', 'Gun', 'Ley', 'Cee', 'Bok', 'Fao', 'Sul', 'Zac', 'Siu', 'Jan', 'Sai', 'Ged', 'Pau', 'Cop', 'Les', 'Suu', 'Dir', 'Var', 'Wap', 'Tai', 'Wah', 'Rei', 'Pas', 'Bat', 'Cas', 'Fad', 'Joe', 'Nir', 'Fem', 'Hai', 'Tal', 'Wea', 'Rok', 'Hoa', 'Goh', 'Hof', 'Nos', 'Roy', 'Nem', 'Bel', 'Yui', 'Wor', 'Neb', 'Tot', 'Luv', 'Yun', 'Lil', 'Doc', 'Lai', 'Hem', 'Kew', 'Lay', 'Nik', 'Gus', 'Hoh', 'Fix', 'Cup', 'Fer', 'Deo', 'Coy', 'Jer', 'Luc', 'Gif', 'Cou', 'Dob', 'Dow', 'Hum', 'Hom', 'Nan', 'Dot', 'Den', 'Yeh', 'Ces', 'Jak', 'Nei', 'Rag', 'Dar', 'Pun', 'Dex', 'Gee', 'Nes', 'Mit', 'Fos', 'Sed', 'Pac', 'Cic', 'Toi', 'Raz', 'Tok', 'Did', 'Rik', 'Hit', 'Kam', 'Hiv', 'Jut', 'Tee', 'Pod', 'Gir', 'Sax', 'Hat', 'Dab', 'Nai', 'Jez', 'Was', 'Bon', 'Kid', 'Him', 'Tia', 'Bin', 'Wep', 'Dup', 'Yue', 'Maa', 'Hao', 'Suv', 'Ken', 'Mod', 'Kan', 'Moc', 'Cow', 'Sex', 'Ben', 'Deg', 'Gaf', 'Yaw', 'Luk', 'Faa', 'Bow', 'Ror', 'Bee', 'Cob', 'Loy', 'Row', 'Det', 'Nut', 'Rah', 'Coi', 'Rap', 'Def', 'Hie', 'Tic', 'Wis', 'Mew', 'Dav', 'Sir', 'Zoe', 'Zin', 'Uac', 'Rab', 'Yen', 'Sip', 'Nip', 'Bir', 'Pak', 'Kar', 'Gen', 'Kea', 'Sor', 'Lod', 'Fas', 'Sif', 'Zag', 'Rea', 'Wed', 'Vex', 'Lem', 'Sob', 'Sue', 'Lar', 'Rav', 'Sou', 'Bev', 'Kek', 'Kol', 'Rae', 'Map', 'Dah', 'Pee', 'Tam', 'Loc', 'Boc', 'Coz', 'Ful', 'Paz', 'Hop', 'Bui', 'Ref', 'Coo', 'Rez', 'Seq', 'Lou', 'Hon', 'Leo', 'Bis', 'Dia', 'Hui', 'Mai', 'Pez', 'Boy', 'Rog', 'Dac', 'Tut', 'Rut', 'Cuz', 'Now', 'Nii', 'Yas', 'Doj', 'Saw', 'Bex', 'Fom']

那就继续:

1
2
3
4
5
6
i, ip = 0, 0
while ip < 256 ** 4:
a, b, c, d = ip >> 24, (ip >> 16) & 0xff, (ip >> 8) & 0xff, ip & 0xff
ip_str = "%d.%d.%d.%d" % (a, b, c, d)
f.write(f"{str(i).zfill(3)} {get(ip_str)} {ip_str}\n")
i, ip = i + 1, ip + 103 * 513 * 313

然后意识到这应该是最后一环了,只需要把这个周期的昵称部分整理出来就可以了:

1
2
3
4
5
6
7
8
9
10
D, exists = [], set(A + B + C)
i, ip = 0, 0
while ip < 256 ** 4:
a, b, c, d = ip >> 24, (ip >> 16) & 0xff, (ip >> 8) & 0xff, ip & 0xff
ip_str = "%d.%d.%d.%d" % (a, b, c, d)
nickname = get(ip_str)
parts = [nickname[j:j+3] for j in range(0, len(nickname), 3)]
D.extend([k for k in parts if k not in exists])
i, ip = i + 1, ip + 103 * 513 * 313
print(D)
1
D  =  ['Puc', 'Maz', 'Doh', 'Hun', 'Cud', 'Vit', 'Wer', 'Hag', 'Din', 'Feb', 'Gui', 'Rak', 'Vac', 'Kim', 'Pol', 'Som', 'Saa', 'Hac', 'Xie', 'Ses', 'Van', 'Nef', 'Mia', 'Tab', 'Pid', 'Ver', 'Cay', 'Jog', 'Jar', 'Lan', 'Hex', 'Soe', 'Lid', 'Fip', 'Wet', 'Ner', 'Dey', 'May', 'Dua', 'Dez', 'Gut', 'Sag', 'Kor', 'Yon', 'Haa', 'Par', 'Fat', 'Vel', 'Yum', 'Wac', 'Poe', 'Yes', 'Rex', 'Gop', 'Cit', 'Val', 'Xix', 'Bit', 'Mig', 'Mib', 'Gaa', 'Sat', 'Mex', 'Geo', 'Doi', 'Mou', 'Dol', 'Joy', 'Caa', 'Dix', 'Nat', 'Boj', 'Mad', 'Pew', 'Nev', 'Sas', 'Rin', 'Dal', 'Joo', 'Vii', 'Tid', 'Hap', 'Sea', 'Cae', 'Cab', 'Nea', 'Wan', 'Mem', 'Nam', 'Mao', 'Pov', 'Pio', 'Bey', 'Vas', 'Jee', 'Not', 'Lat', 'Sud', 'Bog', 'Hue', 'Rio', 'Got', 'Liu', 'Lax', 'Fec', 'Duc', 'Rec', 'Mas', 'Cig', 'Vox', 'Rov', 'Pow', 'Sil', 'Gac', 'Pet', 'Yay', 'Sad', 'Ram', 'Box', 'Wag', 'Nin', 'Lib', 'Tou', 'Dae', 'Tau', 'Teo', 'Sod', 'Hoy', 'Tip', 'Cer', 'Wee', 'Nov', 'Keg', 'Nit', 'Wok', 'Hin', 'Tue', 'Ron', 'Roi', 'Vos', 'Sao', 'Kia', 'Tix', 'Mip', 'Cub', 'Nah', 'Hot', 'Wic', 'Yar', 'Sic', 'Sar', 'Kok', 'Fee', 'Yuk', 'Hoo', 'Hei', 'Dap', 'Cen', 'Las', 'Guy', 'Jon', 'His', 'Moo', 'Roz', 'Fac', 'Fir', 'Ham', 'Rad', 'Foi', 'Sof', 'Poo', 'Toa', 'Kos', 'Sei', 'Dof', 'Get', 'Bap', 'Kes', 'Die', 'Dad', 'Pea', 'Nus', 'Tit', 'Ros', 'Nay', 'Moa', 'Zen', 'Mam', 'Heb', 'Fab', 'Rib', 'Cao', 'Hey', 'Wot', 'Soo', 'Kai', 'Cem', 'Rom', 'Uaw', 'Zed', 'Noi', 'Sab', 'Tes', 'Gob', 'Jax', 'Nob', 'Bao', 'Tos', 'Tor', 'Mep', 'Pan', 'Har', 'Guv', 'Foa', 'Nih', 'Cim', 'Pig', 'Jot', 'Sop', 'Duh', 'Jia', 'Nil', 'Fib', 'Kei', 'Gad', 'Toy', 'Pim', 'Gel', 'Cet', 'Hal', 'Wen', 'Yah', 'Nup', 'Jai', 'Paw', 'Pos', 'Qed', 'Tel', 'Gay', 'Liv', 'Bus', 'Fop', 'Pia', 'Miu', 'Ked', 'Fea', 'Fob', 'Sel', 'Miz', 'Lor', 'Tay', 'Pot', 'Tac', 'Wei', 'Mug', 'Dat', 'Wal', 'How', 'Yow', 'Pax']

那么就可以直接写解码将那些 Spam 留言者的昵称转换回 IP 地址,进一步解出 Flag 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
targets = ["VicCouNeaGas", "DemHohBojWod", "PowFitGuoRut", "VetTasBesDae", "FasLiuTasJoi", "DevRecWoeDia", "BogHubSorHad", "BagLibYupSix", "MowPetBecZan", "LonRecRipLuk", "KarYapTajGot", "TiaLiuFayDic", "VizDivCitBot", "LeaLatReaSac", "FasLiuVicToc", "KunSadMerMun", "LemLiuGuoReq"]

la, lb, lc, ld = len(A), len(B), len(C), len(D)
for i in targets:
for j in [i[k:k+3] for k in range(0, len(i), 3)]:
if j in A: a = A.index(j)
elif j in B: b = B.index(j)
elif j in C: c = C.index(j)
elif j in D: d = D.index(j)
else: assert False
ip = d * (la*lb*lc) + c * (la*lb) + b * la + a
ip_part = [ip >> 24 & 0xFF, ip >> 16 & 0xFF, ip >> 8 & 0xFF, ip & 0xFF]
print(''.join(map(chr, ip_part)), end='')
print()

nanonymous msg

本题完整的题目描述如下,没有附件也没有环境:

󠇖󠆖󠄟󠇖󠅗󠆫󠅯󠆧󠅮󠄮󠇦󠇛󠄣󠆤󠄧󠇛󠄪󠅣󠇬󠆦󠄪󠆧󠅮󠇛󠄧󠆪󠇛󠅯󠇮󠅮󠇬󠇬󠇛󠇦󠄢󠅮󠇛󠅧󠅢󠄮󠅯󠇛󠇯󠆧󠄪󠇦󠅮󠇮󠆦󠇛󠆤󠅧󠇛󠆧󠆤󠇮󠇦󠅮󠆧󠇛󠅣󠄪󠄪󠇛󠅧󠆧󠆤󠅪󠇛󠅦󠅮󠅧󠄯󠆤󠅣󠇛󠄯󠇦󠅧󠇛󠆥󠆤󠆥󠆥󠇛󠆮󠇮󠄮󠅢󠇬󠇪Work in progress…

然而事实上,这个题目描述是这样的,一段滥用 Unicode 变体选择符的文本:

1
\uDB40\uDDD6\uDB40\uDD96\uDB40\uDD1F\uDB40\uDDD6\uDB40\uDD57\uDB40\uDDAB\uDB40\uDD6F\uDB40\uDDA7\uDB40\uDD6E\uDB40\uDD2E\uDB40\uDDE6\uDB40\uDDDB\uDB40\uDD23\uDB40\uDDA4\uDB40\uDD27\uDB40\uDDDB\uDB40\uDD2A\uDB40\uDD63\uDB40\uDDEC\uDB40\uDDA6\uDB40\uDD2A\uDB40\uDDA7\uDB40\uDD6E\uDB40\uDDDB\uDB40\uDD27\uDB40\uDDAA\uDB40\uDDDB\uDB40\uDD6F\uDB40\uDDEE\uDB40\uDD6E\uDB40\uDDEC\uDB40\uDDEC\uDB40\uDDDB\uDB40\uDDE6\uDB40\uDD22\uDB40\uDD6E\uDB40\uDDDB\uDB40\uDD67\uDB40\uDD62\uDB40\uDD2E\uDB40\uDD6F\uDB40\uDDDB\uDB40\uDDEF\uDB40\uDDA7\uDB40\uDD2A\uDB40\uDDE6\uDB40\uDD6E\uDB40\uDDEE\uDB40\uDDA6\uDB40\uDDDB\uDB40\uDDA4\uDB40\uDD67\uDB40\uDDDB\uDB40\uDDA7\uDB40\uDDA4\uDB40\uDDEE\uDB40\uDDE6\uDB40\uDD6E\uDB40\uDDA7\uDB40\uDDDB\uDB40\uDD63\uDB40\uDD2A\uDB40\uDD2A\uDB40\uDDDB\uDB40\uDD67\uDB40\uDDA7\uDB40\uDDA4\uDB40\uDD6A\uDB40\uDDDB\uDB40\uDD66\uDB40\uDD6E\uDB40\uDD67\uDB40\uDD2F\uDB40\uDDA4\uDB40\uDD63\uDB40\uDDDB\uDB40\uDD2F\uDB40\uDDE6\uDB40\uDD67\uDB40\uDDDB\uDB40\uDDA5\uDB40\uDDA4\uDB40\uDDA5\uDB40\uDDA5\uDB40\uDDDB\uDB40\uDDAE\uDB40\uDDEE\uDB40\uDD2E\uDB40\uDD62\uDB40\uDDEC\uDB40\uDDEAWork in progress\u2026

恰巧之前看到过一个滥用 Unicode 变体选择符进行隐写的工具 emoji-encoder。先阅读一下它的代码,尝试理解其原理并且用 Python 复刻以下 Decoder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from typing import Optional

source = "󠇖󠆖󠄟󠇖󠅗󠆫󠅯󠆧󠅮󠄮󠇦󠇛󠄣󠆤󠄧󠇛󠄪󠅣󠇬󠆦󠄪󠆧󠅮󠇛󠄧󠆪󠇛󠅯󠇮󠅮󠇬󠇬󠇛󠇦󠄢󠅮󠇛󠅧󠅢󠄮󠅯󠇛󠇯󠆧󠄪󠇦󠅮󠇮󠆦󠇛󠆤󠅧󠇛󠆧󠆤󠇮󠇦󠅮󠆧󠇛󠅣󠄪󠄪󠇛󠅧󠆧󠆤󠅪󠇛󠅦󠅮󠅧󠄯󠆤󠅣󠇛󠄯󠇦󠅧󠇛󠆥󠆤󠆥󠆥󠇛󠆮󠇮󠄮󠅢󠇬󠇪Work in progress…"

VARIATION_SELECTOR_START = 0xfe00
VARIATION_SELECTOR_END = 0xfe0f
VARIATION_SELECTOR_SUPPLEMENT_START = 0xe0100
VARIATION_SELECTOR_SUPPLEMENT_END = 0xe01ef

def fromVariationSelector(codePoint: int) -> Optional[int]:
if VARIATION_SELECTOR_START <= codePoint <= VARIATION_SELECTOR_END:
return codePoint - VARIATION_SELECTOR_START
elif VARIATION_SELECTOR_SUPPLEMENT_START <= codePoint <= VARIATION_SELECTOR_SUPPLEMENT_END:
return codePoint - VARIATION_SELECTOR_SUPPLEMENT_START + 16
else:
return None

result = []
for char in source:
codePoint = ord(char)
byte = fromVariationSelector(codePoint)
if byte is None:
continue
result.append(byte)
print(result)

然后就得到了一串让人摸不着头脑的字节。假设这就是 Flag 经过一些转换得到的内容,既然已知 Flag 头是 TPCTF{,那么直接对比一下二进制吧:

1
2
3
4
compare = "TPCTF{"
for i in range(len(compare)):
print(bin(result[i])[2:].zfill(8), end=" ")
print(bin(ord(compare[i]))[2:].zfill(8))
1
2
3
4
5
6
11100110 01010100
10100110 01010000
00101111 01000011
11100110 01010100
01100111 01000110
10111011 01111011

不难注意到,这串字节的低第 1 4 7 8 位分别和明文的低第 2 1 3 5 位相同,貌似存在一个 bit mapping 的关系,但除此之外剩下的部分可能还涉及一些其他变换。经过一番尝试,最终写出了以下代码:

1
2
3
4
5
6
for byte in result:
a, b, c, d, e, f, g, h = map(int, bin(byte)[2:].zfill(8))
fin = [c^1, g, d, a, f^1, b, h, e]
res = int("".join(map(str, fin)), 2)
print(chr(res), end="")
print()