一年前,我还只是SUCTF的参赛者,谁又能想到一年后的今天,我已经成为了SUCTF的共建者,很荣幸能作为SU的一员为这次SUCTF2026出题。

image-20260316114154842

因为是第一次给XCTF分站赛出题,所以其实在出题的时候真的很慌,可以说是绞尽脑汁了。早在今年年初甚至是去年底,我们就已经确定今年2-3月份要办SUCTF了,所以那时候开始我也就尝试构思该出些什么题(虽然说并没有投入实操吧)。因为那段时间我发现作为出题人,其实是可以在不影响题目效果的情况下,夹带一些私货的,所以我也就在想能怎么夹带些私货好)当时想的就是看看能不能加入一些我刀剑的元素,而正好我们的题目都是以SU打头的,跟SAO的开头一拍即合,所以最终的目标就是去构建了一个缩写是SAO的题目名,这也是我的题目SU_Artifact_Online的由来,另外我在别的地方也夹带了刀剑的内容,比如pow的ui能看到link start,flag的内容是捏他了刀剑的台词什么的

至于题目本身,其实当初构思了很多题目包括但不限于流量,磁盘,社工,甚至是ARG等等,最后都因为各种原因没去实现(其实是我太菜了没有这种技术力)。最后还是回归到了一个我早就构思过的题目废案中(原本应该出现在羊城杯2025的,但是当时没实现)——本质上是一个魔方翻转+命令构造,命令构造的部分其实是参考了damctf2025的一道misc题 breach,跟我这题命令构造的部分几乎是一模一样的一比一还原,我在它的基础上,融入了魔方翻转的部分,构造了一个六面的5x5魔方,并对其进行随机的打乱,因此需要将其翻转还原到一个可以构造所需命令的状态,才能进行命令的构造并执行。除此之外,还加上了一层单表替换的密文考点,将字母及部分符号映射成了卢恩文字,实际上只需要进行一些词频分析统计就能还原大部分符号映射表(这部分映射足够解出题目),而且在测题过程中只需要不到10min的时间ai就能成功映射出附件的明文。

最后还是给各位师傅磕一个,实力有限只能做出这种垃圾题了,也没什么含金量,纯当玩了,要是给大家带来了不好的体验请见谅😭

(夹带个私货,我的首页换了张超帅的背景图,记得看看嘿嘿~)

SU_Signin

1if you want it then you have to take it.  
2
3https://ctftime.org/team/29641

签到题,描述是鬼泣的名台词,直接给出了SU的ctftime链接,在里面有一个flag,对其做一个简单的字符去重即可 SUCTF{W3lc0me_2_SUC7F2026!!!!}

image-20260317214043831

SU_Artifact_Online

1By chance, you discovered a mysterious machine along with a strange file inscribed with cryptic runes. Can you decipher the runes and uncover the truth hidden within the machine?  
2
3It is recommended to connect to the target machine using stty raw -echo; ncat --ssl xxxxxxxxx xxxx; stty sane for the best experience. 

首先通过题目信息说提到的,nc进靶机后可以发现需要进行一个简单的pow验证,这里简单让ai写写脚本都能跑了,将爆破得到的S值提交即可进入正式的题目

image-20260316124840738

通过验证后,显示的ui如下,可以看到它展示了两个面,分别是正前面和右侧面,并且5x5的格子里各有一个符文,不确定的情况下,我们需要对这个玩意进行一些操作。可以留意到下方有两个模式选择,分别是翻转和激活

image-20260316125021434

选择翻转,可以进入新的界面,可以看到上面提示了通过以下命令来进行一些翻转操作,并可以通过加上`符号来执行逆操作

1R1-R5 / C1-C5 / F1-F5

其中R对应的是正前面的行,1-5则分别对应第一行到第五行,正向操作是进行顺时针翻转,也就是right面的行会朝着front面移动,逆操作则相反

C对应的是正前面的列,1-5则分别对应第一列到第五列,正向操作是进行顺时针翻转,也就是front面的列会向下翻转移动,逆操作则相反

F对应的是则是右侧面的列,1-5则分别对应第一列到第五列,正向操作是进行顺时针翻转,也就是right面的列会向下翻转移动,逆操作则相反

image-20260316125029790

而选择激活,则会将魔方面锁定在正前面,以正前面作为激活面,进行横纵字符选择,从横向移动开始确认字符,确认后移动方向会转换成纵向,随后循环往复这个过程,在这个过程中需要选择合适的字符来构造对应的linux命令

image-20260316125038009

在确定魔方的一些操作后,就是要确定符文代表的字符了,题目中提供了一个附件,里面是一长串经过符文映射后的密文,其实通过简单的字符统计就可以发现里面只有大概31种,并且有明显的分行,大抵能推断出是由英文字符+一些符号构造出来的文段经由映射变成的密文

1ᛗᚹᚱᛨᛚᛈᛇᚠᛒᛒᚺᚱᚨᛨᛇᛉᛗᚹᚱᛒᛨᛟᚠᛖᛨᚠᛨᛇᚠᛈᛨᛗᛟᚱᛈᛗᛣᚬᚬᚲᚺᛜᚱᛨᛣᚱᚠᛒᛖᛨᛉᛃᚨᛥᛨᛈᛉᛨᛗᚠᛃᛃᚱᛒᛨᛗᚹᚠᛈᛨᚺᛨᚠᛇᛥᛨᚦᚹᚺᛃᚨᚺᛖᚹᛨᚲᚱᚠᛗᛚᛒᚱᛖᛨᚠᛈᚨᛨᚠᛨᛗᛉᛚᚦᚹᛣᛨᛗᚱᛇᛋᚱᛒᛧᛨᚺᛨᚨᚺᚨᛈᚯᛗᛨᛃᚺᛁᚱᛨᚹᚺᛖᛨᛃᛉᛉᛁᛖᚬᚬᚬᚺᛨᛈᚱᛜᚱᛒᛨᚹᚠᚨᚬᚬᚬᚢᛚᛗᛨᚹᚱᛨᛟᚠᛖᛨᚠᛨᛃᚠᚨᛨᚺᛨᛟᚠᛖᛨᚹᚱᛒᚱᛨᛗᛉᛨᛒᚱᚦᛒᛚᚺᛗᛥᛨᚹᚱᛨᛟᚠᛖᛨᛇᛣᛨᚢᛉᛣᛧᛨᚺᛨᚷᚠᛜᚱᛨᚹᚺᛇᛨᛇᛣᛨᚢᚱᛖᛗᛨᚢᚠᛒᛁᚱᚱᛋᚯᛖᛨᛖᛇᚺᛃᚱᛧ
2ᛇᚠᛣᚢᚱᛨᚺᚯᛇᛨᛗᛉᛉᛨᚦᛒᚺᛗᚺᚦᚠᛃᛧᛨᚹᚱᛨᛟᚠᛖᛈᚯᛗᛨᛖᛟᚺᛖᚹᛦᛨᚹᚺᛖᛨᛈᚺᚦᛁᛈᚠᛇᚱᛨᚦᚠᛇᚱᛨᚲᛒᛉᛇᛨᛟᚹᚠᛗᛨᚹᚱᛨᚠᛃᛟᚠᛣᛖᛨᛖᚠᚺᚨᛨᛟᚹᚱᛈᛨᛖᛉᛇᚱᛨᛈᛉᛖᛣᛨᛗᛣᛋᚱᛨᚠᛖᛁᚱᚨᛨᚹᚺᛇᛨᚹᚺᛖᛨᛃᚺᛈᚱ:ᛨᚭᚺᚯᛇᛨᚠᛈᛨᛚᛈᛇᚠᛒᛒᚺᚱᚨᛨᛇᛉᛗᚹᚱᛒᛧᚭᛨᚺᚲᛨᚹᚱᛨᚲᚱᛃᛗᛨᛃᚱᛖᛖᛨᛗᚹᚠᛈᛨᛇᛚᛒᚨᚱᛒᛉᛚᛖᛨᚹᚱᛨᛟᛉᛚᛃᚨᛨᚠᚨᚨ:ᛨᚭᚠᛗᛨᚲᛉᛚᛒᛨᚦᚱᛈᛗᛖᛨᚠᛨᛟᛉᛒᚨᛧᛨᚺᛨᛟᛒᚺᛗᚱᛨᚦᛉᛈᚲᚱᛖᛖᚺᛉᛈᛨᛖᛗᛉᛒᚺᚱᛖᛧᚭ
3ᚺᚲᛨᚹᚱᛨᚲᚱᛃᛗᛨᛈᚠᛖᛗᛣᛥᛨᚹᚱᛨᛟᛉᛚᛃᚨᛨᛟᚠᚺᛗᛨᚲᛉᛒᛨᛖᛉᛇᚱᚢᛉᚨᛣᛨᛗᛉᛨᛇᚠᛁᚱᛨᛖᛉᛇᚱᛗᚹᚺᛈᚷᛨᛉᚲᛨᚺᛗᛧᛨᚹᚱᛨᚹᚠᚨᛨᚠᛨᛃᚱᛗᚹᚠᛃᛨᛖᛗᛣᛃᚱᛨᛉᚲᛨᚺᛈᚲᚺᚷᚹᛗᚺᛈᚷᛥᛨᛃᚺᛁᚱᛨᚠᛨᚲᚱᛇᚠᛃᚱᛨᚦᛉᛋᚬᚬᚬᛒᚱᚠᛖᛉᛈᛨᚺᛨᛟᚠᛈᛗᚱᚨᛨᚹᚺᛇᛧᛨᛈᛉᛗᛨᛗᚹᚱᛨᛉᛈᛃᛣᛨᛉᛈᚱᛧ
4ᚹᚱᛨᚹᚠᚨᛨᚠᛨᛃᛉᚠᚨᛨᛉᛈᛥᛨᚠᛈᚨᛨᚹᚺᛖᛨᚲᚠᚦᚱᛨᛖᚹᛉᛟᚱᚨᛨᛗᚹᚠᛗᛨᚹᚱᛨᚨᚱᛖᛋᚺᛖᚱᚨᛨᛋᚱᛉᛋᛃᚱᛨᛇᛉᛒᚱᛨᛗᚹᚠᛈᛨᛚᛖᛚᚠᛃᛧᛨᛖᚺᛃᚱᛈᛗᛃᛣᛨᚺᛨᛋᛉᛚᛒᚱᚨᛨᚠᛨᚨᛉᛚᚢᛃᚱᛨᛖᚹᛉᛗᛨᛉᚲᛨᛉᛃᚨᛨᛚᛈᚨᚱᛒᛟᚱᚠᛒᛨᚠᛈᚨᛨᛃᚱᚲᛗᛨᛗᚹᚱᛨᚢᛉᛗᛗᛃᚱᛧᛨᚹᚱᛨᚨᛒᚠᛈᛁᛨᚺᛗᛥᛨᛋᛉᛚᛒᚱᚨᛨᚠᛈᛉᛗᚹᚱᛒᛧ
5ᚺᛨᛟᚺᛋᚱᚨᛨᛗᚹᚱᛨᚢᚠᛒᛨᛗᛉᛋᛧᛨᚭᚹᛉᛟᚯᛖᛨᛗᚹᚱᛨᚯᛚᛈᛇᚠᛒᛒᚺᚱᚨᛨᛇᛉᛗᚹᚱᛒᚯᛨᛒᚠᚦᛁᚱᛗᛩᚭ
6ᚹᚺᛖᛨᚲᚺᛈᚷᚱᛒᛖᛨᛗᚺᚷᚹᛗᚱᛈᚱᚨᛨᛉᛈᛨᛗᚹᚱᛨᚷᛃᚠᛖᛖᛨᚠᛈᚨᛨᚹᚱᛨᛖᚱᚱᛇᚱᚨᛨᚠᚢᛉᛚᛗᛨᛗᛉᛨᛗᚹᛒᛉᛟᛨᚺᛗᛨᚠᛗᛨᛇᚱᛦᛨᚺᛨᚲᚱᛃᛗᛨᚲᛉᛒᛨᛗᚹᚱᛨᛖᚠᛋᛨᛚᛈᚨᚱᛒᛨᛗᚹᚱᛨᚢᚠᛒᛧᛨᚺᛈᛨᛗᚱᛇᛋᛉᛒᚠᛃᛨᛇᚠᛈᚺᛋᛚᛃᚠᛗᚺᛉᛈᛨᛣᛉᛚᛨᛗᛒᛣᛨᛗᛉᛨᚲᚺᚷᛚᛒᚱᛨᚱᛜᚱᛒᛣᛗᚹᚺᛈᚷᛥᛨᚢᛚᛗᛨᛗᚹᚱᛒᚱᛨᚠᛒᚱᛨᛖᛉᛨᛇᚠᛈᛣᛨᚲᚠᚦᛗᛉᛒᛖᛨᛗᚹᚠᛗᛨᛣᛉᛚᛨᛈᚱᛜᚱᛒᛨᛗᚠᛁᚱᛨᛈᚱᚱᚨᛃᚱᛖᛖᛨᛒᚺᛖᛁᛖᛧ

对其进行词频统计,可以发现 ᛨ 出现最多,大概率就是空格

image-20260316132757065

将 ᛨ 替换为空格后,用脚本替换一下字母和符号

 1symbols = "abcdefghijklmnopqrstuvwxyz,?;\"\'-!"  # 这里只需要写入26个字符加任意符号即可
 2
 3with open("text", "r", encoding="utf-8") as f:
 4    text = f.read()
 5
 6unique_chars = []
 7for ch in text:
 8    if ch in [" ", ".",'\n']:
 9        continue
10    if ch not in unique_chars:
11        unique_chars.append(ch)
12
13if len(unique_chars) > len(symbols):
14    raise ValueError("字符种类超过可映射数量")
15
16mapping = {ch: symbols[i] for i, ch in enumerate(unique_chars)}
17
18result = []
19for ch in text:
20    if ch == "\n":
21        result.append("\n")
22    elif ch == " ":
23        result.append(" ")
24    elif ch == ".":
25        result.append(".")
26    else:
27        result.append(mapping[ch])
28
29output = "".join(result)
30
31print(output)

然后得到了

1abc defghhicj fkabch lgm g fge alceanoopiqc ncghm krjs ek agrrch abge i gfs tbirjimb pcgadhcm gej g akdtbn acfuchv i jijewa rixc bim rkkxmoooi ecqch bgjoooyda bc lgm g rgj i lgm bchc ak hcthdias bc lgm fn yknv i zgqc bif fn ycma yghxccuwm mfircv
2fgnyc iwf akk thiaitgrv bc lgmewa mlimb, bim eitxegfc tgfc phkf lbga bc grlgnm mgij lbce mkfc ekmn anuc gmxcj bif bim riec? ;iwf ge defghhicj fkabchv; ip bc pcra rcmm abge fdhjchkdm bc lkdrj gjj? ;ga pkdh tceam g lkhjv i lhiac tkepcmmike makhicmv;
3ip bc pcra egmans bc lkdrj lgia pkh mkfcykjn ak fgxc mkfcabiez kp iav bc bgj g rcabgr manrc kp iepizbaiezs rixc g pcfgrc tkuooohcgmke i lgeacj bifv eka abc kern kecv
4bc bgj g rkgj kes gej bim pgtc mbklcj abga bc jcmuimcj uckurc fkhc abge dmdgrv mircearn i ukdhcj g jkdyrc mbka kp krj dejchlcgh gej rcpa abc ykaarcv bc jhgex ias ukdhcj gekabchv
5i liucj abc ygh akuv ;bklwm abc wdefghhicj fkabchw hgtxca";
6bim piezchm aizbacecj ke abc zrgmm gej bc mccfcj gykda ak abhkl ia ga fc, i pcra pkh abc mgu dejch abc yghv ie acfukhgr fgeiudrgaike nkd ahn ak pizdhc cqchnabiezs yda abchc ghc mk fgen pgtakhm abga nkd ecqch agxc eccjrcmm himxmv

然后拿到quipqiup里进行词频分析,则可以还原出大概得明文是什么,通过社工或者问ai就能锁定明文是来自Robert A. Heinlein的短篇小说《All You Zombies》

image-20260316133246865

找到原文对应的部分,然后将他跟密文一一对应,即可还原映射表,简单拿脚本还原一下

 1from collections import OrderedDict
 2
 3def build_mapping(plain, cipher):
 4    plain = plain.replace("\n", "")
 5    cipher = cipher.replace("\n", "")
 6
 7    if len(plain) != len(cipher):
 8        raise ValueError(f"长度不一致: plain={len(plain)} cipher={len(cipher)}")
 9
10    mapping = OrderedDict()
11
12    for p, c in zip(plain, cipher):
13        p = p.lower()
14
15        if p not in mapping:
16            mapping[p] = c
17        else:
18            if mapping[p] != c:
19                raise ValueError(f"映射冲突: '{p}' -> '{mapping[p]}' 与 '{c}'")
20
21    return mapping
22
23
24def print_mapping(mapping):
25    print("{")
26    for k, v in mapping.items():
27        if k == "'":
28            k = "\\'"
29        print(f"    '{k}':'{v}',")
30    print("}")
31
32
33if __name__ == "__main__":
34    plaintext = """The Unmarried Mother was a man twenty--five years old, no taller than I am, childish features and a touchy temper. I didn't like his looks---I never had---but he was a lad I was here to recruit, he was my boy. I gave him my best barkeep's smile.
35Maybe I'm too critical. He wasn't swish; his nickname came from what he always said when some nosy type asked him his line: "I'm an unmarried mother." If he felt less than murderous he would add: "at four cents a word. I write confession stories."
36If he felt nasty, he would wait for somebody to make something of it. He had a lethal style of infighting, like a female cop---reason I wanted him. Not the only one.
37He had a load on, and his face showed that he despised people more than usual. Silently I poured a double shot of Old Underwear and left the bottle. He drank it, poured another.
38I wiped the bar top. "How's the 'Unmarried Mother' racket?"
39
40His fingers tightened on the glass and he seemed about to throw it at me; I felt for the sap under the bar. In temporal manipulation you try to figure everything, but there are so many factors that you never take needless risks."""
41    ciphertext = """ᛗᚹᚱᛨᛚᛈᛇᚠᛒᛒᚺᚱᚨᛨᛇᛉᛗᚹᚱᛒᛨᛟᚠᛖᛨᚠᛨᛇᚠᛈᛨᛗᛟᚱᛈᛗᛣᚬᚬᚲᚺᛜᚱᛨᛣᚱᚠᛒᛖᛨᛉᛃᚨᛥᛨᛈᛉᛨᛗᚠᛃᛃᚱᛒᛨᛗᚹᚠᛈᛨᚺᛨᚠᛇᛥᛨᚦᚹᚺᛃᚨᚺᛖᚹᛨᚲᚱᚠᛗᛚᛒᚱᛖᛨᚠᛈᚨᛨᚠᛨᛗᛉᛚᚦᚹᛣᛨᛗᚱᛇᛋᚱᛒᛧᛨᚺᛨᚨᚺᚨᛈᚯᛗᛨᛃᚺᛁᚱᛨᚹᚺᛖᛨᛃᛉᛉᛁᛖᚬᚬᚬᚺᛨᛈᚱᛜᚱᛒᛨᚹᚠᚨᚬᚬᚬᚢᛚᛗᛨᚹᚱᛨᛟᚠᛖᛨᚠᛨᛃᚠᚨᛨᚺᛨᛟᚠᛖᛨᚹᚱᛒᚱᛨᛗᛉᛨᛒᚱᚦᛒᛚᚺᛗᛥᛨᚹᚱᛨᛟᚠᛖᛨᛇᛣᛨᚢᛉᛣᛧᛨᚺᛨᚷᚠᛜᚱᛨᚹᚺᛇᛨᛇᛣᛨᚢᚱᛖᛗᛨᚢᚠᛒᛁᚱᚱᛋᚯᛖᛨᛖᛇᚺᛃᚱᛧ
42ᛇᚠᛣᚢᚱᛨᚺᚯᛇᛨᛗᛉᛉᛨᚦᛒᚺᛗᚺᚦᚠᛃᛧᛨᚹᚱᛨᛟᚠᛖᛈᚯᛗᛨᛖᛟᚺᛖᚹᛦᛨᚹᚺᛖᛨᛈᚺᚦᛁᛈᚠᛇᚱᛨᚦᚠᛇᚱᛨᚲᛒᛉᛇᛨᛟᚹᚠᛗᛨᚹᚱᛨᚠᛃᛟᚠᛣᛖᛨᛖᚠᚺᚨᛨᛟᚹᚱᛈᛨᛖᛉᛇᚱᛨᛈᛉᛖᛣᛨᛗᛣᛋᚱᛨᚠᛖᛁᚱᚨᛨᚹᚺᛇᛨᚹᚺᛖᛨᛃᚺᛈᚱ:ᛨᚭᚺᚯᛇᛨᚠᛈᛨᛚᛈᛇᚠᛒᛒᚺᚱᚨᛨᛇᛉᛗᚹᚱᛒᛧᚭᛨᚺᚲᛨᚹᚱᛨᚲᚱᛃᛗᛨᛃᚱᛖᛖᛨᛗᚹᚠᛈᛨᛇᛚᛒᚨᚱᛒᛉᛚᛖᛨᚹᚱᛨᛟᛉᛚᛃᚨᛨᚠᚨᚨ:ᛨᚭᚠᛗᛨᚲᛉᛚᛒᛨᚦᚱᛈᛗᛖᛨᚠᛨᛟᛉᛒᚨᛧᛨᚺᛨᛟᛒᚺᛗᚱᛨᚦᛉᛈᚲᚱᛖᛖᚺᛉᛈᛨᛖᛗᛉᛒᚺᚱᛖᛧᚭ
43ᚺᚲᛨᚹᚱᛨᚲᚱᛃᛗᛨᛈᚠᛖᛗᛣᛥᛨᚹᚱᛨᛟᛉᛚᛃᚨᛨᛟᚠᚺᛗᛨᚲᛉᛒᛨᛖᛉᛇᚱᚢᛉᚨᛣᛨᛗᛉᛨᛇᚠᛁᚱᛨᛖᛉᛇᚱᛗᚹᚺᛈᚷᛨᛉᚲᛨᚺᛗᛧᛨᚹᚱᛨᚹᚠᚨᛨᚠᛨᛃᚱᛗᚹᚠᛃᛨᛖᛗᛣᛃᚱᛨᛉᚲᛨᚺᛈᚲᚺᚷᚹᛗᚺᛈᚷᛥᛨᛃᚺᛁᚱᛨᚠᛨᚲᚱᛇᚠᛃᚱᛨᚦᛉᛋᚬᚬᚬᛒᚱᚠᛖᛉᛈᛨᚺᛨᛟᚠᛈᛗᚱᚨᛨᚹᚺᛇᛧᛨᛈᛉᛗᛨᛗᚹᚱᛨᛉᛈᛃᛣᛨᛉᛈᚱᛧ
44ᚹᚱᛨᚹᚠᚨᛨᚠᛨᛃᛉᚠᚨᛨᛉᛈᛥᛨᚠᛈᚨᛨᚹᚺᛖᛨᚲᚠᚦᚱᛨᛖᚹᛉᛟᚱᚨᛨᛗᚹᚠᛗᛨᚹᚱᛨᚨᚱᛖᛋᚺᛖᚱᚨᛨᛋᚱᛉᛋᛃᚱᛨᛇᛉᛒᚱᛨᛗᚹᚠᛈᛨᛚᛖᛚᚠᛃᛧᛨᛖᚺᛃᚱᛈᛗᛃᛣᛨᚺᛨᛋᛉᛚᛒᚱᚨᛨᚠᛨᚨᛉᛚᚢᛃᚱᛨᛖᚹᛉᛗᛨᛉᚲᛨᛉᛃᚨᛨᛚᛈᚨᚱᛒᛟᚱᚠᛒᛨᚠᛈᚨᛨᛃᚱᚲᛗᛨᛗᚹᚱᛨᚢᛉᛗᛗᛃᚱᛧᛨᚹᚱᛨᚨᛒᚠᛈᛁᛨᚺᛗᛥᛨᛋᛉᛚᛒᚱᚨᛨᚠᛈᛉᛗᚹᚱᛒᛧ
45ᚺᛨᛟᚺᛋᚱᚨᛨᛗᚹᚱᛨᚢᚠᛒᛨᛗᛉᛋᛧᛨᚭᚹᛉᛟᚯᛖᛨᛗᚹᚱᛨᚯᛚᛈᛇᚠᛒᛒᚺᚱᚨᛨᛇᛉᛗᚹᚱᛒᚯᛨᛒᚠᚦᛁᚱᛗᛩᚭ
46ᚹᚺᛖᛨᚲᚺᛈᚷᚱᛒᛖᛨᛗᚺᚷᚹᛗᚱᛈᚱᚨᛨᛉᛈᛨᛗᚹᚱᛨᚷᛃᚠᛖᛖᛨᚠᛈᚨᛨᚹᚱᛨᛖᚱᚱᛇᚱᚨᛨᚠᚢᛉᛚᛗᛨᛗᛉᛨᛗᚹᛒᛉᛟᛨᚺᛗᛨᚠᛗᛨᛇᚱᛦᛨᚺᛨᚲᚱᛃᛗᛨᚲᛉᛒᛨᛗᚹᚱᛨᛖᚠᛋᛨᛚᛈᚨᚱᛒᛨᛗᚹᚱᛨᚢᚠᛒᛧᛨᚺᛈᛨᛗᚱᛇᛋᛉᛒᚠᛃᛨᛇᚠᛈᚺᛋᛚᛃᚠᛗᚺᛉᛈᛨᛣᛉᛚᛨᛗᛒᛣᛨᛗᛉᛨᚲᚺᚷᛚᛒᚱᛨᚱᛜᚱᛒᛣᛗᚹᚺᛈᚷᛥᛨᚢᛚᛗᛨᛗᚹᚱᛒᚱᛨᚠᛒᚱᛨᛖᛉᛨᛇᚠᛈᛣᛨᚲᚠᚦᛗᛉᛒᛖᛨᛗᚹᚠᛗᛨᛣᛉᛚᛨᛈᚱᛜᚱᛒᛨᛗᚠᛁᚱᛨᛈᚱᚱᚨᛃᚱᛖᛖᛨᛒᚺᛖᛁᛖᛧ"""
47
48    mapping = build_mapping(plaintext, ciphertext)
49    print_mapping(mapping)

符文映射构建成功后,就可以开始进行魔方的翻转构造和路径选择了

首先可以留意到靶机存在时间限制,必须在40s内获取到flag,否则就得重新连接靶机,所有状态都会打乱(每次的魔方都是随机的),所以如果频繁的跟服务器进行交互会浪费掉大量的时间,因此我们必须将大部分的计算量放在本地,运算完成后再一次性将结果发送给服务器让其进行翻转,最后在尝试命令构造即可。

核心的解题思路如下:

  • 进入靶机后,选择翻转模式,通过翻转去构建出这个魔方的每一个面都有着哪些字符,都分别在什么位置
  • 获取魔方的移动逻辑,在本地脚本中构建一个等效的魔方,还原所有操作
  • 利用算法规划将合适的字符翻转到正面
  • 最后进行命令构造

第一步很简单,对于魔方六面的探测,最简单的方法就是利用R和C操作,每行每列分别翻转4次(正反各两次),即可确认六面的所有内容

第二步也不难,通过执行RCF三个操作就可以确定他们的翻转方向和对应的位置,可以通过手动操作来确定魔方的翻转操作具体的效果,也可以利用脚本对每次翻转连续执行4次进行4种状态的观察,利用4-cycle的思路去确定每个位置的魔方格子对应的闭合环,即四种状态a0,a1,a2,a3,a0时X在格子A,a1时X在格子B,a2时X在格子C,a3时X在格子D,由于魔方每行四次翻转即可恢复原样,因此可以形成A-B-C-D-A的闭合,从而确定魔方的翻转操作

第三步通过算法,比如BFS求解魔方的操作序列,对于已经翻转到正面的格子,要保证其不被影响,始终停留在正面。当已固定的格子较多导致单步操作都无法保持不变时,还可以利用交换子(commutator)[m1,m2]=m1·m2·m1’·m2’ 作为复合操作,它只影响少量位置,从而在不破坏已有布局的前提下继续移动新的格子

第四步则需要我们考虑去构造什么样的命令,通过DFS算法找到命令的执行路径,从列不变开始,挑选第一行的任意字符,然后逐步切换纵横移动,当前面的思路实现后,我们可以进行一些简单的构造尝试,比如执行ls查看当前目录的内容,这里通过ls可以发现flag根本不在当前目录,而通过符文映射我们也会发现没有/这个符号,那就没办法直接去到根目录下查看,所以只能通过构造cd .. 并用;进行命令拼接,从而查看上一级目录的内容,拼接的命令为cd ..;ls,而到了上一级目录后我们就会发现在该目录下存在flag文件,所以只需要参考刚刚的拼接命令,再次拼接查看flag的命令即可,预期的解法有两种,分别是cd ..;nl flagcd ..;cat flag,为了保证一定有可执行解,实际上我在源码里预设了cd ..;nl flag的路径,不过cd ..;cat flag也是可以构造的(存在一定偶然性,用nl的准确率会高点

最终exp如下,这里我直接用了全符文映射表,但是只靠附件给的量也是能构建出来的

  1#!/usr/bin/env python3
  2import hashlib
  3import itertools
  4import re
  5import socket
  6import ssl
  7import string
  8import sys
  9import time
 10from collections import deque
 11
 12rmap = {
 13    'a':'ᚠ','b':'ᚢ','c':'ᚦ','d':'ᚨ','e':'ᚱ','f':'ᚲ','g':'ᚷ','h':'ᚹ',
 14    'i':'ᚺ','j':'ᚾ','k':'ᛁ','l':'ᛃ','m':'ᛇ','n':'ᛈ','o':'ᛉ','p':'ᛋ',
 15    'q':'ᛏ','r':'ᛒ','s':'ᛖ','t':'ᛗ','u':'ᛚ','v':'ᛜ','w':'ᛟ','x':'ᛞ',
 16    'y':'ᛣ','z':'ᛤ',',':'ᛥ',';':'ᛦ','.':'ᛧ',' ':'ᛨ','?':'ᛩ',
 17    '{':'ᚪ','}':'ᚫ','-':'ᚬ','"':'ᚭ','!':'ᚮ',"'":'ᚯ',
 18}
 19rrev = {v: k for k, v in rmap.items()}
 20
 21def decode(text):
 22    return "".join(rrev.get(c, c) for c in text)
 23
 24def solve_pow(prefix):
 25    charset = string.ascii_letters + string.digits
 26    print(f'[*] Solving PoW: sha256("{prefix}" + S)[:6] == "000000"')
 27    t0, cnt = time.time(), 0
 28    for length in range(1, 20):
 29        for combo in itertools.product(charset, repeat=length):
 30            s = "".join(combo)
 31            if hashlib.sha256((prefix + s).encode()).hexdigest()[:6] == "000000":
 32                print(f'[+] PoW solved: S="{s}" ({cnt} attempts, {time.time()-t0:.2f}s)')
 33                return s
 34            cnt += 1
 35            if cnt % 5_000_000 == 0:
 36                e = time.time() - t0
 37                print(f"  [pow] {cnt/1e6:.1f}M attempts, {cnt/e/1e6:.2f}M/s, {e:.1f}s")
 38
 39cmd = "cd ..;nl flag"
 40sz = 5
 41fnames = ["F", "R", "B", "L", "U", "D"]
 42all_moves = [f"{p}{i}{s}" for p in "RCF" for i in range(1, 6) for s in ("", "'")]
 43allpos = [(f, r, c) for f in fnames for r in range(sz) for c in range(sz)]
 44orbsz = {(0,0):6, (0,1):24, (0,2):24, (1,1):24, (1,2):48, (2,2):24}
 45arrows = {"up": b"\x1b[A", "down": b"\x1b[B", "right": b"\x1b[C", "left": b"\x1b[D"}
 46ansi_re = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
 47grid_re = re.compile(
 48    r'([\u16A0-\u16EB](?:\s+[\u16A0-\u16EB]){4})'
 49    r'\s+\|\s+'
 50    r'([\u16A0-\u16EB](?:\s+[\u16A0-\u16EB]){4})'
 51)
 52mapall_seq = (
 53    [f"R{i}" for i in range(1, 6)] + [f"R{i}" for i in range(1, 6)] +
 54    [f"R{i}'" for i in range(1, 6)] + [f"R{i}'" for i in range(1, 6)] +
 55    [f"C{i}'" for i in range(1, 6)] + [f"C{i}" for i in range(1, 6)] +
 56    [f"C{i}" for i in range(1, 6)] + [f"C{i}'" for i in range(1, 6)]
 57)
 58mapall_idx = {4: ("B", False), 9: ("L", False), 24: ("U", True), 34: ("D", True)}
 59
 60def orb(c, r):
 61    a, b = abs(c - 2), abs(r - 2)
 62    return (min(a, b), max(a, b))
 63
 64def inv(m):
 65    return m[:-1] if m.endswith("'") else m + "'"
 66
 67class Conn:
 68    def __init__(self, host, port, use_ssl=False):
 69        raw = socket.create_connection((host, port), timeout=30)
 70        if use_ssl:
 71            ctx = ssl.create_default_context()
 72            ctx.check_hostname = False
 73            ctx.verify_mode = ssl.CERT_NONE
 74            self.s = ctx.wrap_socket(raw, server_hostname=host)
 75        else:
 76            self.s = raw
 77        self.buf = b""
 78
 79    def recvuntil(self, delim, timeout=15):
 80        dl = time.time() + timeout
 81        while delim not in self.buf:
 82            rem = dl - time.time()
 83            if rem <= 0: break
 84            self.s.settimeout(max(rem, 0.1))
 85            try:
 86                d = self.s.recv(4096)
 87                if not d: break
 88                self.buf += d
 89            except socket.timeout:
 90                break
 91        i = self.buf.find(delim)
 92        if i >= 0:
 93            r = self.buf[:i + len(delim)]
 94            self.buf = self.buf[i + len(delim):]
 95            return r
 96        r = self.buf
 97        self.buf = b""
 98        return r
 99
100    def send(self, data):
101        self.s.sendall(data + b"\n")
102
103    def close(self):
104        self.s.close()
105
106def parse_grid(raw):
107    clean = ansi_re.sub("", raw)
108    fr, rr = [], []
109    for line in clean.splitlines():
110        m = grid_re.search(line)
111        if m:
112            fr.append([decode(c) for c in m.group(1).split()])
113            rr.append([decode(c) for c in m.group(2).split()])
114    return (fr[-sz:], rr[-sz:]) if len(fr) >= sz else (None, None)
115
116def batch_send(r, mvs):
117    if mvs:
118        r.s.sendall(b"\n".join(m.encode() for m in mvs) + b"\n")
119
120def batch_recv(r, n):
121    return [r.recvuntil(b"move>", timeout=10).decode(errors="replace") for _ in range(n)]
122
123def read_faces(resps, front, right):
124    st = {"F": front, "R": right}
125    for idx, (name, use_f) in mapall_idx.items():
126        f, r = parse_grid(resps[idx])
127        g = f if use_f else r
128        if g is None: return None
129        st[name] = g
130    return st
131
132def cyc(t):
133    return (t[1], t[2], t[3], t[0])
134
135def deduce_perm(s0, s1, s2, s3):
136    perm, used = {}, set()
137    keys = {}
138    for p in allpos:
139        fn, r, c = p
140        keys[p] = (s0[fn][r][c], s1[fn][r][c], s2[fn][r][c], s3[fn][r][c])
141    for p in allpos:
142        k = keys[p]
143        if k[0] == k[1] == k[2] == k[3]:
144            perm[p] = p
145            used.add(p)
146    di = {}
147    for p in allpos:
148        if p in used: continue
149        di.setdefault(cyc(keys[p]), []).append(p)
150    amb = []
151    for p in allpos:
152        if p in perm: continue
153        k = keys[p]
154        if k[0] == k[1] == k[2] == k[3]: continue
155        cands = [q for q in di.get(k, []) if q not in used]
156        if len(cands) == 1:
157            perm[p] = cands[0]
158            used.add(cands[0])
159        elif cands:
160            amb.append((p, cands))
161    for p, cands in amb:
162        if p in perm: continue
163        fn, r, c = p
164        o = orb(c, r)
165        oc = [q for q in cands if q not in used and orb(q[2], q[1]) == o]
166        if oc:
167            perm[p] = oc[0]
168            used.add(oc[0])
169    for p in allpos:
170        if p not in perm:
171            perm[p] = p
172    return perm
173
174def probe_perms(conn, s0):
175    perms = {}
176    seq = list(mapall_seq)
177    for px in "RCF":
178        for i in range(1, 6):
179            base = f"{px}{i}"
180            ib = inv(base)
181            batch = [base] + seq + [base] + seq + [base] + seq + [base] + seq + [ib] * 4
182            batch_send(conn, batch)
183            resps = batch_recv(conn, 168)
184            states, ok = [], True
185            for j in range(3):
186                off = j * 41
187                f, r = parse_grid(resps[off])
188                if f is None: ok = False; break
189                st = read_faces(resps[off+1:off+41], f, r)
190                if st is None: ok = False; break
191                states.append(st)
192            if not ok or len(states) < 3: continue
193            pm = deduce_perm(s0, states[0], states[1], states[2])
194            perms[base] = pm
195            perms[base + "'"] = {v: k for k, v in pm.items()}
196    return perms
197
198def build_comms(perms):
199    comms = {}
200    for m1 in all_moves:
201        m1i = inv(m1)
202        p1, p1i = perms[m1], perms[m1i]
203        for m2 in all_moves:
204            if m2 == m1 or m2 == m1i: continue
205            m2i = inv(m2)
206            p2, p2i = perms[m2], perms[m2i]
207            res, nontrivial = {}, False
208            for p in allpos:
209                d = p2i[p1i[p2[p1[p]]]]
210                res[p] = d
211                if d != p: nontrivial = True
212            if nontrivial:
213                comms[(m1, m2)] = (res, [m1, m2, m1i, m2i])
214    return comms
215
216def safe_ops(frozen, perms, comms=None):
217    ops = [(perms[t], [t]) for t in all_moves if all(perms[t][p] == p for p in frozen)]
218    if comms:
219        ops += [(pm, mv) for (_, _), (pm, mv) in comms.items()
220                if all(pm[p] == p for p in frozen)]
221    return ops
222
223def bfs(start, goal, ops):
224    if start == goal: return []
225    vis = {start: None}
226    q = deque([start])
227    while q:
228        cur = q.popleft()
229        for pm, ms in ops:
230            nxt = pm[cur]
231            if nxt not in vis:
232                vis[nxt] = (cur, ms)
233                if nxt == goal:
234                    path, n = [], nxt
235                    while vis[n] is not None:
236                        prev, m = vis[n]
237                        path = list(m) + path
238                        n = prev
239                    return path
240                q.append(nxt)
241    return None
242
243def copy_state(st):
244    return {n: [row[:] for row in st[n]] for n in fnames}
245
246def apply_moves(st, tokens, perms):
247    for tok in tokens:
248        nf = {n: [[None] * sz for _ in range(sz)] for n in fnames}
249        for (sf, sr, sc), (df, dr, dc) in perms[tok].items():
250            nf[df][dr][dc] = st[sf][sr][sc]
251        for n in fnames:
252            st[n] = nf[n]
253
254def solve(state, path, perms, comms):
255    orderings = [
256        sorted(path, key=lambda x: orbsz[orb(x[1][0], x[1][1])]),
257        sorted(path, key=lambda x: -orbsz[orb(x[1][0], x[1][1])]),
258        list(path), list(reversed(path)),
259        sorted(path, key=lambda x: (x[1][0], orbsz[orb(x[1][0], x[1][1])])),
260        sorted(path, key=lambda x: (orbsz[orb(x[1][0], x[1][1])], x[1][0])),
261        sorted(path, key=lambda x: x[1][1]),
262    ]
263    seen = set()
264    for order in orderings:
265        k = tuple(order)
266        if k in seen: continue
267        seen.add(k)
268        sim = copy_state(state)
269        frozen, seq, ok = set(), [], True
270        for ch, (tc, tr) in order:
271            tp = ("F", tr, tc)
272            to = orb(tc, tr)
273            cands = [(f, r, c) for f in fnames for r in range(sz) for c in range(sz)
274                     if sim[f][r][c] == ch and orb(c, r) == to]
275            if not cands: ok = False; break
276            cands.sort(key=lambda p: 0 if p[0] == 'F' else 1)
277            found = False
278            for uc in [False, True]:
279                ops = safe_ops(frozen, perms, comms if uc else None)
280                for cd in cands:
281                    res = bfs(cd, tp, ops)
282                    if res is not None:
283                        apply_moves(sim, res, perms)
284                        seq.extend(res)
285                        frozen.add(tp)
286                        found = True
287                        break
288                if found: break
289            if not found: ok = False; break
290        if not ok: continue
291        if all(sim["F"][r][c] == ch for ch, (c, r) in path):
292            return seq
293    return None
294
295def find_paths(state):
296    needed = {}
297    for ch in cmd:
298        needed[ch] = needed.get(ch, 0) + 1
299    avail = {}
300    for ch in needed:
301        av = {}
302        for fn in fnames:
303            for r in range(sz):
304                for c in range(sz):
305                    if state[fn][r][c] == ch:
306                        o = orb(c, r)
307                        if o != (0, 0):
308                            av[o] = av.get(o, 0) + 1
309        avail[ch] = av
310    paths, used = [], set()
311    def dfs(idx, cx, cy, hz, cur):
312        if idx == len(cmd):
313            paths.append(cur[:])
314            return
315        ch = cmd[idx]
316        if ch not in avail: return
317        for c in range(sz):
318            for r in range(sz):
319                if hz and r != cy: continue
320                if not hz and c != cx: continue
321                if (c, r) in used: continue
322                o = orb(c, r)
323                if o == (0, 0): continue
324                if avail[ch].get(o, 0) == 0: continue
325                used.add((c, r))
326                cur.append((ch, (c, r)))
327                dfs(idx + 1, c, r, not hz, cur)
328                cur.pop()
329                used.discard((c, r))
330    dfs(0, 0, 0, True, [])
331    paths.sort(key=lambda p: (
332        -len(set(c for _, (c, _) in p)),
333        -len(set(r for _, (_, r) in p)),
334        sum(orbsz[orb(c, r)] for _, (c, r) in p)
335    ))
336    return paths
337
338def drain(conn, timeout=0.6):
339    chunks = [conn.buf]
340    conn.buf = b""
341    dl = time.time() + timeout
342    while True:
343        rem = dl - time.time()
344        if rem <= 0: break
345        conn.s.settimeout(max(rem, 0.05))
346        try:
347            d = conn.s.recv(4096)
348            if not d: break
349            chunks.append(d)
350            dl = time.time() + 0.3
351        except socket.timeout:
352            break
353    conn.buf = b""
354    return b"".join(chunks)
355
356def key_wait(conn, key):
357    conn.s.sendall(key)
358    conn.recvuntil(b"Cursor:", timeout=3)
359    conn.recvuntil(b"\n", timeout=1)
360    conn.recvuntil(b"\n", timeout=1)
361
362def navigate(conn, coords):
363    acts, cx, cy, hz = [], 0, 0, True
364    for tx, ty in coords:
365        if hz:
366            rd, ld = (tx - cx) % sz, (cx - tx) % sz
367            acts.append(("right", rd) if rd <= ld else ("left", ld))
368            cx = tx
369        else:
370            dd, ud = (ty - cy) % sz, (cy - ty) % sz
371            acts.append(("down", dd) if dd <= ud else ("up", ud))
372            cy = ty
373        hz = not hz
374    print(f"  [nav] {len(coords)} chars, {sum(c for _, c in acts)} key presses")
375    conn.send(b"2")
376    got = conn.recvuntil(b"Cursor:", timeout=8)
377    if b"Cursor:" not in got:
378        return "(error)", "(error)"
379    conn.recvuntil(b"\n", timeout=1)
380    conn.recvuntil(b"\n", timeout=1)
381    for si, (d, n) in enumerate(acts):
382        for _ in range(n):
383            key_wait(conn, arrows[d])
384        conn.s.sendall(b"\r")
385        conn.recvuntil(b"Cursor:", timeout=3)
386        conn.recvuntil(b"\n", timeout=1)
387        sp = conn.recvuntil(b"\n", timeout=1)
388        print(f"  [nav] step {si+1}/{len(acts)}: {ansi_re.sub('', sp.decode(errors='replace')).strip()}")
389    conn.s.sendall(b"x")
390    raw = drain(conn, timeout=4.0)
391    text = ansi_re.sub("", raw.decode(errors="replace"))
392    runes, plain, out = [], [], False
393    for line in text.splitlines():
394        s = line.strip()
395        if not s: continue
396        if "activating" in s: out = True; continue
397        if out:
398            if any(k in s for k in ["Enter", "continue", "Press", "hums"]): break
399            runes.append(s)
400            plain.append(decode(s))
401    conn.s.sendall(b"\r")
402    time.sleep(0.3)
403    return "\n".join(runes) or "(no output)", "\n".join(plain) or "(no output)"
404
405def main():
406    if len(sys.argv) < 3:
407        print(f"Usage: {sys.argv[0]} <host> <port> [--ssl]")
408        sys.exit(1)
409    host, port = sys.argv[1], int(sys.argv[2])
410    use_ssl = "--ssl" in sys.argv
411    t0 = time.time()
412    conn = Conn(host, port, use_ssl)
413
414    raw = conn.recvuntil(b"S: ", timeout=15).decode(errors="replace")
415    m = re.search(r'sha256\("([^"]+)"\s*\+\s*S\)', raw)
416    if not m:
417        print("[-] bad pow"); conn.close(); return
418    ans = solve_pow(m.group(1))
419    if not ans:
420        print("[-] pow failed"); conn.close(); return
421    conn.send(ans.encode())
422    data = conn.recvuntil(b"> ", timeout=15).decode(errors="replace")
423    if "OK" not in data:
424        print("[-] pow rejected"); conn.close(); return
425
426    front, right = parse_grid(data)
427    if front is None:
428        print("[-] parse failed"); conn.close(); return
429    conn.send(b"1")
430    conn.recvuntil(b"move>", timeout=10)
431    batch_send(conn, mapall_seq)
432    resps = batch_recv(conn, 40)
433    s0 = read_faces(resps, front, right)
434    if s0 is None:
435        print("[-] faces failed"); conn.close(); return
436
437    perms = probe_perms(conn, s0)
438    if len(perms) < 30:
439        print(f"[-] only {len(perms)} perms"); conn.close(); return
440
441    comms = build_comms(perms)
442    paths = find_paths(s0)
443    if not paths:
444        print("[-] no paths"); conn.close(); return
445
446    sol, used_path, tried = None, None, 0
447    for p in paths[:500]:
448        if time.time() - t0 > 30: break
449        tried += 1
450        ms = solve(s0, p, perms, comms)
451        if ms is not None:
452            sol, used_path = ms, p
453            break
454    if sol is None:
455        print(f"[-] no solution ({tried} tried)"); conn.close(); return
456    print(f"[+] Solution: {len(sol)} moves, tried {tried} paths ({time.time()-t0:.1f}s)")
457
458    batch_send(conn, sol)
459    batch_recv(conn, len(sol))
460    conn.send(b"q")
461    conn.recvuntil(b"> ", timeout=10)
462
463    coords = [(c, r) for _, (c, r) in used_path]
464    rout, pout = navigate(conn, coords)
465    elapsed = time.time() - t0
466
467    print(f"\n{'='*50}")
468    print(f"Moves: {len(sol)} | Time: {elapsed:.1f}s")
469    print(f"\nRune:\n{rout}")
470    print(f"\nPlain:\n{pout}")
471    print(f"{'='*50}")
472    try:
473        conn.send(b"q")
474    except Exception:
475        pass
476    conn.close()
477
478if __name__ == "__main__":
479    main()