前言:题目总览
题目类型
题目名称
难度
解出情况
Misc
(Multi) Constructed Dataline
Medium
❌
Misc
Constructed Dataline
Easy
✅
Misc
Feedback
Trivial
✅
Misc
SignIn
Trivial
✅
Misc
优质 Flag 募集中!
Medium
❌
Misc
心理念写
Hard
❌
Misc
红芯铸魂・码践初心
Trivial
✅
Crypto
Broken AES
Expert
❌
Crypto
Hyper Substitution
Easy
✅
Crypto
Leak CKKS
Normal
✅
Crypto
Noisy LCG
Medium
❌
Crypto
Scale CKKS
Medium
✅
Crypto
Weird RSA
Hard
❌
Crypto
手撕平方根
Normal
✅
Pwn
御坂网络
Expert
❌
Pwn
御坂美琴
Hard
❌
Pwn
相互置换
Medium
❌
Pwn
等长条
Normal
✅
Pwn
能力开发・缺陷电气
Normal
✅
Pwn
能力开发・路径穿过
Easy
✅
Pwn
能力开发・鸟瞰把握
Medium
✅
Pwn
超电磁炮
Medium
❌
Web
Fail Open
Medium
❌
Web
Safe Store
Normal
✅
Web
Terraria 百科
Hard
❌
Web
ez blog
Medium
✅
Web
ez xss
Hard
❌
Web
不明意味
Medium
❌
Web
乌萨奇的硬币
Easy
✅
Reverse
Bluntstone
Medium
✅
Reverse
FunSokoban
Medium
✅
Reverse
Strange puts
Hard
✅
Reverse
Vigorcheck
Easy
✅
Reverse
开始电力运输
Normal
✅
Forensics
损坏的 Sysprep
Normal
✅
PPC
10 秒不顶号就毁 flag
Medium
❌
PPC
日历
Expert
❌
PPC
日历・序章
Normal
✅
Pentest
内网迷踪
Medium
❌
OSINT
Find Me
Easy
✅
OSINT
Journey across
Medium
❌
OSINT
无论到哪都爱吃麦麦
Easy
✅
1 Misc
1.2 Constructed Dataline
题目描述
我翻开附件一查,这附件没有后缀,歪歪斜斜的每叶上都写着 Dataline 几个字。我横竖睡不着,仔细看了半夜,才从字缝里看出字来,整个文件都写着几个字是 flag!
打开附件,发现由大量重复格式的 Dataline 数据行构成:
1 2 3 4 5 6 Dataline(32202,32204,0.80,1.26,0.00,0.00); Dataline(32202,32216,0.80,0.80,0.00,0.00); Dataline(32204,32221,1.26,1.26,0.00,0.00); ... Dataline(36317,36320,1.02,0.85,0.00,0.00); Dataline(36322,36322,1.10,1.06,0.00,0.00);
根据提示,对所有数据行进行解析,还原每条线段并做适当的变换,最终可在生成的图片中看出 flag。
观察数据特征可知,每行最后两项数值恒为 0.00,属于无效信息;前四项为有效参数,可分别解析为线段端点坐标 (x1, x2) 与 (y1, y2),其中 x1、x2 为整数类型,y1、y2 为小数类型。
由于 y 方向的数值很小,直接画出来会很扁,需要放大。尝试放大 100 倍:
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 import reimport matplotlib.pyplot as pltY_SCALE = 100 def main () -> None : with open ("flag" , "r" , encoding="utf-8" , errors="ignore" ) as f: text = f.read() fig, ax = plt.subplots(figsize=(16 , 4 ), dpi=400 ) for x1, x2, y1, y2 in re.findall( r"Dataline\((\d+),(\d+),([0-9.]+),([0-9.]+),[0-9.]+,[0-9.]+\);" , text ): ax.plot([int (x1), int (x2)], [float (y1) * Y_SCALE, float (y2) * Y_SCALE], "k-" , lw=0.25 ) ax.axis("off" ) fig.savefig("yup_100.png" , bbox_inches="tight" , pad_inches=0 ) if __name__ == "__main__" : main()
可以看到图片本身的比例不合适,需要对整体纵向压缩。同时,首字母类似 M 而不是 W,说明还需要上下翻转图像。
发现当压缩系数设置为 0.1 时,可以输出较为清晰的图案:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import ioimport reimport matplotlib.pyplot as pltfrom PIL import ImageY_SCALE = 100.0 Y_SQUISH = 0.1 def main () -> None : with open ("flag" , "r" , encoding="utf-8" , errors="ignore" ) as f: text = f.read() segs = re.findall( r"Dataline\((\d+),(\d+),([0-9.]+),([0-9.]+),[0-9.]+,[0-9.]+\);" , text ) max_y = max (max (float (y1), float (y2)) for _, _, y1, y2 in segs) fig, ax = plt.subplots(figsize=(16 , 4 ), dpi=400 ) for x1, x2, y1, y2 in segs: ax.plot( [int (x1), int (x2)], [(max_y - float (y1)) * Y_SCALE, (max_y - float (y2)) * Y_SCALE], "k-" , lw=0.25 , ) ax.axis("off" ) buf = io.BytesIO() fig.savefig(buf, format ="png" , bbox_inches="tight" , pad_inches=0 ) plt.close(fig) buf.seek(0 ) img = Image.open (buf) w, h = img.size img = img.resize((w, max (1 , int (h * Y_SQUISH))), Image.Resampling.LANCZOS) img.save("inv_squish_10.png" ) if __name__ == "__main__" : main()
从图片中读出 flag。
1 W4terCTF{Ev3ryb0dy_plz_V_Hachiwa0_50_bcUz_He_i5_soOoOoOoOo_hUn9rY_on_KFCCrazyThurs}
1.3 Feedback
题目描述
感谢参与 W4terCTF 2026!我们准备了一份问卷,期待你来填写,给我们你的反馈,帮助我们做得更好。
填写问卷,完成赛事反馈后获得 flag。
1 W4terCTF{4_w33k_70_r3m3mb3r_4nd_533_y0u_n3x7_71m3}
1.4 SignIn
题目描述
欢迎关注中山大学网络空间安全协会,祝你比赛顺利。
网页内嵌简易终端,在内置 shell 中浏览工作目录,发现并读取 flag 文件。
1 W4terCTF{Welc0me_t0_W4terCTF2026!!!}
1.7 红芯铸魂・码践初心
题目描述
社会主义核心价值观正是凝聚民族力量、引领青年前行的精神旗帜,为传承红色基因、践行社会主义核心价值观,现核心价值观关键词进行安全编码传输,请解出其中蕴含的秘密。
附件为「核心价值观」编码这一常见的趣味编码形式,借助在线解码器还原明文得到 flag。
1 W4terCTF{Br41nW4sh1n9_5uck5}
2 Crypto
2.2 Hyper Substitution
首先,将题目给出的密文保存为 ciphertext。每一次创建实例时生成的密文不相同,以下是解题所用密文:
1 z#D!+; h#e>GCC< :cG v!g>/c /y>G;:>/:< c#/ /EG;: c>/ g>CG:>BG /:+dS>;" #y:>\G \!gy#;!G/ #;d dGGE y#\G/ >; #gg E#D:/ !C :cG ?!DgdT }; ;>;G:GG; C!D:S'G>"c:< cG ?G;: :! *#$G `>\+ >; :cG -!;"! :! !A/GD\G # ;G? \!gy#;! ?c>yc cG g#:GD ;#BGd `>:+D!T h#e>GCC ?#/ #AgG :! /G: +E c>/ y#BE \GDS yg!/G :! :cG \!gy#;! ?c>gG >: ?#/ GD+E:>;" \>!gG;:gST hc!+"c cG B#;#"Gd :! :#$G # ;+BAGD !C AD>gg>#;: Ec!:!"D#Ec/< cG y!+gd ;!: /:#S ;G#D :cG \!gy#;! C!D \GDS g!;"T zG ;!:>yGd :c#: # D>\GD !C g>Q+>d D!y$ ?#/ y!B>;" :!?#Dd/ c>BT }: :cDG#:G;Gd :! /+DD!+;d c>B y!BEgG:GgS< A+: h#e>GCC B#;#"Gd :! G/y#EG F+/: >; :>BGT zG ?#>:Gd +;:>g :cG \!gy#;! AGy#BG Q+>G: #;d cG ?#/ #AgG :! DG:+D; :?! d#S/ g#:GDT hc>/ :>BG< cG B#;#"Gd :! yg>BA >;:! :cG B!+:c !C `>:+D! /! :c#: cG y!+gd :#$G Ec!:!"D#Ec/ #;d BG#/+DG :GBEGD#:+DG/T h#e>GCC c#/ !C:G; D>/$Gd c>/ g>CG >; :c>/ ?#ST zG c#/ AGG; #AgG :! :Ggg +/ B!DG #A!+: #y:>\G \!gy#;!G/ :c#; #;S B#; #g>\GT
将明文候选字符集固定为:空格 + a-z + .,'\";:!?-(共 36 个符号),与密文字符集建立一一映射即可建模本题。映射总数对应 36 阶排列,精确枚举不可行,故采用脚本在启发式框架下搜索较优结果。
实现要点如下:
初始化 :依据密文字符频率构造初始映射 —— 高频符号优先对应空格,其余位置参考英文常用字母频率顺序 etaoin... 排列字母与标点;将映射修正为合法排列,并对空格位置施加约束以保证密文空格与明文空格对齐。
评分 :对解密结果中提取的长度不小于 2 的连续小写字母串,使用 wordfreq.zipf_frequency 累加词频得分;对「字母 – 标点 – 字母」这类不合理片段施加惩罚,并对连续空格、重复标点等现象扣分;对 " the "、" and "、" to "、" of " 等常见片段给予小幅加分以稳定搜索。
搜索 :在保持双射的前提下,每一步随机交换映射中的两个位置;评分上升则接受,下降则以模拟退火准则按温度接受扰动解,温度由约 5.0 按指数衰减至约 0.05。脚本采用多组随机种子重启(默认 12 轮,每轮约 120_000 次迭代),取全局最优映射及对应明文。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 from __future__ import annotationsimport mathimport osimport randomimport reimport sysfrom collections import Counterfrom typing import Dict , List from wordfreq import zipf_frequencyPUNCT = ".,'\";:!?-" PLAINTEXT_ALPHABET = " " + "abcdefghijklmnopqrstuvwxyz" + PUNCT def read_cipher (path: str ) -> str : """读取密文文件""" return open (path, "r" , encoding="utf-8" ).read() def initial_mapping (cipher_alpha: str , plain_alpha: str , cipher: str ) -> Dict [str , str ]: """ 根据字符频率初始化映射表(模拟退火初始解)""" cipher_counts = Counter(cipher) cipher_sorted = [c for c, _ in cipher_counts.most_common()] target_order = ( [" " ] + list ("etaoinshrdlucmfwypvbgkjqxz" ) + list (".,'\";:!?-" ) ) target_order = [c for c in target_order if c in plain_alpha] if len (target_order) != len (plain_alpha) or len (set (target_order)) != len (plain_alpha): target_order = list (plain_alpha) if len (cipher_alpha) != len (plain_alpha): raise ValueError(f"alphabet size mismatch: cipher={len (cipher_alpha)} plain={len (plain_alpha)} " ) remaining = [c for c in plain_alpha if c not in target_order] perm = target_order + remaining perm = perm[: len (plain_alpha)] mapping = {c: perm[idx] for idx, c in enumerate (cipher_sorted)} for c in cipher_alpha: mapping.setdefault(c, random.choice(plain_alpha)) return mapping WORD_RE = re.compile (r"[a-z]{2,}" ) def score_plaintext (pt: str ) -> float : """文本评分函数""" pt_low = pt.lower() words = WORD_RE.findall(pt_low) if not words: return -1e9 s = 0.0 for w in words: z = zipf_frequency(w, "en" ) s += max (-5.0 , min (7.0 , z)) inside_word = re.findall(rf"[a-z][{re.escape(PUNCT)} ][a-z]" , pt_low) s -= 25.0 * len (inside_word) s -= 5.0 * pt_low.count(" " ) s -= 1.5 * pt_low.count(".." ) s -= 1.5 * pt_low.count(",," ) s -= 1.0 * pt_low.count("''" ) s -= 1.0 * pt_low.count('""' ) s -= 1.0 * pt_low.count("--" ) s -= 2.0 * pt_low.count("?" ) s += 1.0 * (pt_low.count("." ) + pt_low.count("," )) s += 0.5 * (pt_low.count("'" ) + pt_low.count("!" )) for tri in (" the " , " and " , " to " , " of " ): s += 6.0 * pt_low.count(tri) return s def decrypt_with_mapping (cipher: str , mapping: Dict [str , str ] ) -> str : """根据当前映射表,将密文转换为候选明文""" return "" .join(mapping.get(ch, ch) for ch in cipher) def anneal ( cipher: str , cipher_alpha: str , plain_alpha: str , *, iters: int , seed: int , ) -> tuple [float , Dict [str , str ], str ]: """模拟退火主算法""" rnd = random.Random(seed) plain_perm = [initial_mapping(cipher_alpha, plain_alpha, cipher)[c] for c in cipher_alpha] if " " in cipher_alpha and " " in plain_alpha: space_idx = cipher_alpha.index(" " ) plain_perm[space_idx] = " " used = set () missing = [c for c in plain_alpha if c not in plain_perm] mi = 0 for idx, ch in enumerate (plain_perm): if ch in used: plain_perm[idx] = missing[mi] mi += 1 used.add(plain_perm[idx]) def perm_to_map (pp: List [str ] ) -> Dict [str , str ]: return dict (zip (cipher_alpha, pp)) mapping = perm_to_map(plain_perm) cur_pt = decrypt_with_mapping(cipher, mapping) cur_score = score_plaintext(cur_pt) best_score = cur_score best_perm = plain_perm[:] best_pt = cur_pt t0 = 5.0 t1 = 0.05 for step in range (1 , iters + 1 ): frac = step / iters t = t0 * ((t1 / t0) ** frac) a = rnd.randrange(len (plain_perm)) b = rnd.randrange(len (plain_perm)) if " " in cipher_alpha: fixed = cipher_alpha.index(" " ) if a == fixed or b == fixed: continue if a == b: continue plain_perm[a], plain_perm[b] = plain_perm[b], plain_perm[a] mapping2 = perm_to_map(plain_perm) pt2 = decrypt_with_mapping(cipher, mapping2) sc2 = score_plaintext(pt2) d = sc2 - cur_score accept = d >= 0 or rnd.random() < math.exp(d / max (1e-9 , t)) if accept: cur_score = sc2 cur_pt = pt2 if sc2 > best_score: best_score = sc2 best_perm = plain_perm[:] best_pt = pt2 else : plain_perm[a], plain_perm[b] = plain_perm[b], plain_perm[a] if step % (iters // 5 ) == 0 and step != iters: plain_perm = best_perm[:] for _ in range (20 ): i = rnd.randrange(len (plain_perm)) j = rnd.randrange(len (plain_perm)) plain_perm[i], plain_perm[j] = plain_perm[j], plain_perm[i] mapping = perm_to_map(plain_perm) cur_pt = decrypt_with_mapping(cipher, mapping) cur_score = score_plaintext(cur_pt) return best_score, perm_to_map(best_perm), best_pt def main () -> int : cipher_path = sys.argv[1 ] if len (sys.argv) > 1 else "ciphertext" cipher = read_cipher(cipher_path) cipher_alpha = "" .join(sorted (set (cipher))) if len (cipher_alpha) != len (PLAINTEXT_ALPHABET): print ("[-] Cipher alphabet size:" , len (cipher_alpha), "expected:" , len (PLAINTEXT_ALPHABET)) print (" cipher alphabet repr:" , repr (cipher_alpha)) return 2 print ("[*] Cipher length:" , len (cipher), flush=True ) print ("[*] Alphabet:" , repr (cipher_alpha), flush=True ) best_score = float ("-inf" ) best_map: Dict [str , str ] = {} best_pt = "" starts = 12 iters = 120_000 for s in range (starts): seed = 1337 + s * 99991 sc, mp, pt = anneal(cipher, cipher_alpha, PLAINTEXT_ALPHABET, iters=iters, seed=seed) if sc > best_score: best_score, best_map, best_pt = sc, mp, pt print (f"[+] New best score {best_score:.2 f} at start {s} " , flush=True ) preview = pt[:300 ].replace("\n" , " " ) print (" preview:" , preview, flush=True ) out_txt = os.path.join(os.path.dirname(cipher_path) or "." , "decrypted_best.txt" ) with open (out_txt, "w" , encoding="utf-8" ) as f: f.write(best_pt) out_map = os.path.join(os.path.dirname(cipher_path) or "." , "mapping_best.txt" ) with open (out_map, "w" , encoding="utf-8" ) as f: for c in cipher_alpha: f.write(f"{repr (c)} -> {repr (best_map[c])} \n" ) print ("[*] Wrote:" , out_txt, flush=True ) print ("[*] Wrote:" , out_map, flush=True ) return 0 if __name__ == "__main__" : raise SystemExit(main())
运行上述脚本得到的较优明文:
1 jaroun .axieff! the ;olish scientist! has spent his lifetime studying active volcanoes and deep caves in all parts of the world, -n nineteen fortyzeight! he went to ?ake 'ivu in the :ongo to observe a new volcano which he later named 'ituro, .axieff was able to set up his camp very close to the volcano while it was erupting violently, .hough he managed to take a number of brilliant photographs! he could not stay near the volcano for very long, je noticed that a river of liquid rock was coming towards him, -t threatened to surround him completely! but .axieff managed to escape "ust in time, je waited until the volcano became quiet and he was able to return two days later, .his time! he managed to climb into the mouth of 'ituro so that he could take photographs and measure temperatures, .axieff has often risked his life in this way, je has been able to tell us more about active volcanoes than any man alive,
脚本不能保证还原出正确明文,在处理完毕后可能差几个字符,需要人工校正(因为大写字母无法还原)。
密文符号
求解器给出
正确明文
典型出现
z
j
H
Haroun, He
h
.
T
Tazieff, Though, This
v
;
P
Polish
}
-
I
In, It
'
z
-
forty-eight
*
?
L
Lake
`
'
K
Kivu, Kituro
-
:
C
Congo
e
x
z
Tazieff, axieff, zeight
F
"
j
just
T
,
.
句号
<
!
,
逗号
可以根据英语经验,搜索专有名词反复试验得出最终正确的明文:
1 Haroun Tazieff, the Polish scientist, has spent his lifetime studying active volcanoes and deep caves in all parts of the world. In nineteen forty-eight, he went to Lake Kivu in the Congo to observe a new volcano which he later named Kituro. Tazieff was able to set up his camp very close to the volcano while it was erupting violently. Though he managed to take a number of brilliant photographs, he could not stay near the volcano for very long. He noticed that a river of liquid rock was coming towards him. It threatened to surround him completely, but Tazieff managed to escape just in time. He waited until the volcano became quiet and he was able to return two days later. This time, he managed to climb into the mouth of Kituro so that he could take photographs and measure temperatures. Tazieff has often risked his life in this way. He has been able to tell us more about active volcanoes than any man alive.
1 W4terCTF{sTa7ls7ICS_15_7h3_BIg6esT_EnEmy_OF_ClaSs1CA1_cryPtO6raPHY}
2.3 Leak CKKS
题目描述
CKKS 是一种可以使用浮点数运算的全同态加密算法,a1wAys 在上现代密码学课程的时候不小心泄漏了一些明密文对,你能据此得到他的私钥吗 🤓?
(1) 题目分析
本题基于 TenSEAL 的 CKKS 方案。服务端对全零明文做对称加密后执行 multiply,在 rescale 之前 把乘法密文的系数数组 ct 以及 decrypt 得到的明文系数 dec 一并输出。同时用派生密钥对 flag 做 AES-ECB 加密。
关键代码如下:
1 2 sk_active = dump_dyn_array(sk.data())[: len (active_moduli) * N] key = sha256("," .join(map (str , sk_active)).encode()).hexdigest()[:32 ]
AES 密钥由 sk_active 决定:sk.data() 在 NTT 域下,对每个当前 RNS 模数 $q_j$ 取一段长度为 $N$ 的系数。只要恢复这 $L \times N$ 个整数,就能复现 key 并解密 flag。
generate_instance 中循环 NUM_SAMPLES = 2 次,每次独立加密全零、平方、解密,得到两组 (ct, dec)。两组样本的私钥相同,但密文随机性不同,因而得到两条不同的代数方程。
(2) 建立方程
乘法后密文为 3 分量 $(c_0, c_1, c_2)$。在模素数 $q$、系数位置 $i$ 上,解密满足:
$$
\text{dec} \equiv c_0 + c_1 \cdot s + c_2 \cdot s^2 \pmod q
$$
其中 $s$ 为私钥在该模数、该位置上的 NTT 系数。移项:
$$
c_2 s^2 + c_1 s + (c_0 - \text{dec}) \equiv 0 \pmod q
$$
记 $A = c_0 - \text{dec}$,$B = c_1$,$C = c_2$,则
$$
C s^2 + B s + A \equiv 0 \pmod q
$$
这是 $\mathbb{F}_q$ 上的二次方程。单独解它会有两个根,无法唯一确定 $s$。题目提供两组样本,正是为了在 不单独求二次根 的前提下,用联立方程直接解出 $s$。
(3) 联立消元
设两组样本对应系数 $(A_0, B_0, C_0)$ 与 $(A_1, B_1, C_1)$,共享未知数 $s$:
$$
C_0 s^2 + B_0 s + A_0 \equiv 0 \pmod q
$$
$$
C_1 s^2 + B_1 s + A_1 \equiv 0 \pmod q
$$
第一式乘 $C_1$、第二式乘 $C_0$ 后相减,消去 $s^2$:
$$
(B_0 C_1 - B_1 C_0), s + (A_0 C_1 - A_1 C_0) \equiv 0 \pmod q
$$
记 $\Delta = B_0 C_1 - B_1 C_0$。当 $\Delta \not\equiv 0 \pmod q$ 时,
$$
s \equiv \Delta^{-1} (A_1 C_0 - A_0 C_1) \pmod q
$$
对 active_moduli 中每个模数 $q_j$、每个 $i \in [0, N)$ 各算一次,拼接结果即为 sk_active。模数由 CoeffModulus.Create 生成,均为奇素数;随机实例下 $\Delta \equiv 0$ 的概率极低。
(4) 数据格式与下标
程序打印两行:
1 2 [+] ciphertext: <hex> [+] challenge data: <json>
challenge data 中含 poly_modulus_degree、active_moduli、samples 等字段。每个 sample 有 ct、dec,以及 ciphertext_size = 3、coeff_modulus_size = L。
ct 按 SEAL 动态数组顺序展开:先密文分量,再 RNS 模数,再多项式系数。令 total = L * N:
1 2 ct[comp * total + j * N + i] 分量 comp ∈ {0, 1, 2},模数 j,位置 i dec[j * N + i] 模数 j,位置 i
运行以下脚本拉取远程输出并保存为 output.txt:
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 26 27 28 29 30 31 param( [string]$HostName = "127.0.0.1" , [int]$Port = xxxxx, [string]$OutFile = "output.txt" ) $client = $null $stream = $null try { $client = New-Object System.Net.Sockets.TcpClient($HostName , $Port ) $stream = $client .GetStream() $req = [Text.Encoding]::ASCII.GetBytes("GET / HTTP/1.0`r`nHost: $HostName `r`n`r`n" ) $stream .Write($req , 0, $req .Length) $out = New-Object System.IO.MemoryStream $buf = New-Object byte[] 65536 while (($n = $stream .Read($buf , 0 , $buf .Length)) -gt 0) { $out .Write($buf , 0, $n ) } [IO.File]::WriteAllBytes($OutFile , $out .ToArray()) "saved $($out.Length) bytes -> $OutFile " } catch { Write-Error "connect/read failed: $($_.Exception.Message) " exit 1 } finally { if ($stream ) { $stream .Close() } if ($client ) { $client .Close() } }
(5) 完整解题脚本
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 import jsonimport refrom hashlib import sha256from Crypto.Cipher import AESfrom Crypto.Util.number import inversefrom Crypto.Util.Padding import unpaddef derive_key (active_sk_ntt ): """从恢复出的私钥推导 AES 密钥""" blob = "," .join(map (str , active_sk_ntt)).encode() return sha256(blob).hexdigest()[:32 ] def parse_input (text: str ): """解析题目输出文本:提取密文和 JSON 数据""" ct_m = re.search(r"ciphertext:\s*([0-9a-fA-F]+)" , text) json_m = re.search(r"challenge data:\s*(\{.*\})" , text, flags=re.S) if not (ct_m and json_m): raise ValueError("unexpected output format" ) return ct_m.group(1 ).strip(), json.loads(json_m.group(1 )) def recover_active_secret_key (data: dict ) -> list [int ]: """ 核心:利用两组明密文对,联立二次方程,逐位求解私钥 s 每个 (j, i) 对应一个模数 q_j 和一个系数位置 i """ N = int (data["poly_modulus_degree" ]) moduli = [int (x) for x in data["active_moduli" ]] L = len (moduli) total = L * N s0, s1 = data["samples" ] ct_0, dec_0 = s0["ct" ], s0["dec" ] ct_1, dec_1 = s1["ct" ], s1["dec" ] recovered = [0 ] * total def ct_idx (comp, j, i ): return comp * total + j * N + i def dec_idx (j, i ): return j * N + i for j, q in enumerate (moduli): for i in range (N): c00 = ct_0[ct_idx(0 , j, i)] % q c01 = ct_0[ct_idx(1 , j, i)] % q c02 = ct_0[ct_idx(2 , j, i)] % q d0 = dec_0[dec_idx(j, i)] % q c10 = ct_1[ct_idx(0 , j, i)] % q c11 = ct_1[ct_idx(1 , j, i)] % q c12 = ct_1[ct_idx(2 , j, i)] % q d1 = dec_1[dec_idx(j, i)] % q A0, B0, C0 = (c00 - d0) % q, c01, c02 A1, B1, C1 = (c10 - d1) % q, c11, c12 den = (B0 * C1 - B1 * C0) % q num = (A1 * C0 - A0 * C1) % q recovered[j * N + i] = num * inverse(den, q) % q return recovered def main (): with open ("output.txt" , "r" , encoding="utf-8" , errors="ignore" ) as f: text = f.read() ciphertext_hex, data = parse_input(text) sk_active = recover_active_secret_key(data) key = derive_key(sk_active) aes = AES.new(bytes .fromhex(key), AES.MODE_ECB) flag = unpad(aes.decrypt(bytes .fromhex(ciphertext_hex)), AES.block_size) print (flag.decode(errors="replace" )) if __name__ == "__main__" : main()
运行脚本得出 flag。
1 W4terCTF{7H15_is_AN_34sY_PrO8lEM_1oR_YOU_AnD_dO_N0t_b3_a1RaId}
2.5 Scale CKKS
题目描述
CKKS 的 Scale 是什么?a1wAys 在 rescale 的时候没有按照标准去做,他觉得好像没什么大不了的 😊 ,你能发现他的错误并得到他的 flag 🚩 吗?
(1) 题目分析
本题基于 TenSEAL 的 CKKS 方案。服务端将 flag 按字节编码为向量并加密,随后执行 50 次随机线性查询:每次生成一个 7 稀疏的 ±1 系数行 row、两个小扰动系数 g1, g2,调用 buggy_measurement() 计算一个整数,叠加有界噪声后取模输出。全部查询参数与答案以 JSON 形式公开。
关键代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def buggy_measurement (..., ct_flag, row, g1, g2 ): pt_row = encode_vector(..., row, ct_flag.scale, ct_flag.parms_id()) ev.multiply_plain(ct_flag, pt_row, ct_mul) ct_sum = slot_sum(ev, galois_keys, ct_mul, nslots) pt_g1 = encode_vector(..., [g1], ct_sum.scale, ct_sum.parms_id()) ev.multiply_plain(ct_sum, pt_g1, ct_tmp) ev.rescale_to_next_inplace(ct_tmp) ct_tmp.scale = float (delta) pt_g2 = encode_vector(..., [g2], ct_tmp.scale, ct_tmp.parms_id()) ev.multiply_plain(ct_tmp, pt_g2, ct_out) ev.rescale_to_next_inplace(ct_out) ct_out.scale = float (delta) decoded = decrypt_first_slot(dec, encoder, ct_out) return int (round (decoded / report_scale))
以及外层采样逻辑:
1 2 3 4 5 6 row = random_sparse_row(rng_py, len (flag_vec), ROW_WEIGHT) g1 = 1.0 + rng_py.randint(-80 , 80 ) / (2 ** 12 ) g2 = 1.0 + rng_py.randint(-80 , 80 ) / (2 ** 12 ) raw_answer = buggy_measurement(...) e = rng_py.randint(-ERROR_BOUND, ERROR_BOUND) answer = centered_mod(raw_answer + e, LWE_MODULUS)
CKKS 中 rescale_to_next 会沿模数链丢弃尾部素数模,使密文 scale 约除以该模数。题目在每次 rescale 后强行把 ct.scale 改回 delta = 2 ** 30,导致解密时按更小的 scale 解释同一密文,底层数值被额外放大。这一错误把同态运算结果变成了关于 flag 的 可预测线性泄露 。
(2) 建立方程
内积折叠 :multiply_plain(ct_flag, pt_row) 后每个 slot 近似为 flag[j] * row[j];slot_sum() 通过 Galois 旋转把所有 slot 累加到第 0 槽,得到内积:
$$
s = \langle row, flag\rangle = \sum_j row_j \cdot flag_j
$$
scale 篡改带来的放大 :设两次 rescale 丢弃的模数分别为 $q_{n-1}, q_{n-2}$(即 JSON 中 active_coeff_moduli 的最后两项),第 0 槽在最终解密前的真实量级约为:
$$
v \approx \frac{\Delta^4}{q_{n-1} q_{n-2}} \cdot g_1 g_2 \cdot s, \qquad \Delta = 2^{30}
$$
除以 report_scale 并四舍五入,得到 raw_answer;服务端再叠加噪声并取模(cmod 即 centered_mod):
$$
ans \equiv \mathrm{cmod}!\left(\mathrm{round}(H \cdot s) + e,; p\right) \pmod p
$$
其中:
$$
H = \frac{\Delta^4}{q_{n-1} q_{n-2} \cdot R} \cdot g_1 g_2, \qquad R = \text{report_scale} = 2^{40}, \qquad |e| \le B = 1024
$$
$H$ 完全由公开参数确定,g1, g2 也在 JSON 里。CKKS 舍入误差被吸收进 $e$,在 ERROR_BOUND 基础上额外留约 256 的 slack 便可覆盖。
内积上界 :每条 row 仅有 7 个 ±1,flag 字节为可打印 ASCII(32 ~ 126),故:
$$
|s| \le 7 \times 126 = 882
$$
取值空间极小,可对每条 query 逐一枚举 $s$,检查是否与观测 answer 在模 $p$ 意义下足够接近。
(3) 筛选候选与 Z3 求解
对第 $k$ 条 query,定义候选集合:
$$
C_k = { s \mid -882 \le s \le 882,\ \mathrm{dist}_p(\mathrm{ans}_k, \mathrm{round}(H_k s)) \le B + slack }
$$
其中 $\mathrm{dist}_p(a, b)$ 是模 $p$ 下的中心距离:
$$
\mathrm{dist}_p(a, b) = |\mathrm{cmod}(a - b, p)|
$$
又记:
$$
H_k = H \cdot g_{1,k} \cdot g_{2,k}
$$
实测每条 query 的 $|C_k|$ 通常只有 1 ~ 2 个。
建立整数变量 $c_0, \ldots, c_{n-1}$,约束 $32 \le c_i \le 126$,并对每条 query 添加:
$$
\langle \mathrm{row}_k, c \rangle \in C_k
$$
50 条稀疏线性约束足以唯一确定 flag。
(4) 数据格式与下标
程序输出一行:
1 [+] challenge data: <json>
JSON 主要字段:
字段
含义
num_slots_used
flag 字节数 $n$
active_coeff_moduli
当前 RNS 模数链,取最后两项作为 $q_{n-1}, q_{n-2}$
delta, report_scale
$\Delta$ 与 $R$
lwe_modulus, error_bound
模数 $p$ 与噪声界 $B$
rows[k][j]
第 $k$ 条 query 的稀疏系数,长度 $n$
g1[k], g2[k]
第 $k$ 条 query 的扰动系数
answers[k]
第 $k$ 条 query 的模加噪输出
运行以下脚本拉取远程输出并保存为 instance.json:
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 26 27 28 29 30 31 32 import jsonimport reimport socketdef main (): host, port = "127.0.0.1" , xxxxx out_path = "instance.json" with socket.create_connection((host, port), timeout=5.0 ) as s: buf = b"" while True : d = s.recv(65536 ) if not d: break buf += d txt = buf.decode(errors="replace" ) m = re.search(r"\[\+\] challenge data: (\{.*\})" , txt, re.S) if not m: raise SystemExit("[-] could not find challenge JSON in response" ) obj = json.loads(m.group(1 )) with open (out_path, "w" , encoding="utf-8" ) as f: json.dump(obj, f) print (f"[+] saved {out_path} " ) if __name__ == "__main__" : main()
(5) 完整解题脚本
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 import jsonimport refrom typing import List from z3 import ArithRef, Int, IntVal, Or, Solver, Sum, satCHAR_MIN = 32 CHAR_MAX = 126 NOISE_SLACK = 256 def centered_mod (x: int , mod: int ) -> int : """中心模运算:把结果映射到 [-mod/2, mod/2) 区间""" x %= mod return x - mod if x > mod // 2 else x def mod_dist (a: int , b: int , mod: int ) -> int : """计算模 p 下的中心距离""" d = (a - b) % mod return abs (d - mod) if d > mod // 2 else abs (d) def parse_instance (text: str ) -> dict : """从题目输出文本中解析 JSON 格式的挑战数据""" m = re.search(r"\[\+\]\s*challenge data:\s*(\{.*\})" , text, re.S) if not m: raise ValueError("challenge JSON not found" ) return json.loads(m.group(1 )) def scale_factor (inst: dict ) -> float : """ 核心:计算题目 BUG 造成的固定放大系数 H H = delta⁴ / (q_{n-1} * q_{n-2} * report_scale) """ delta = inst["delta" ] report_scale = inst["report_scale" ] q_last, q_prev = inst["active_coeff_moduli" ][-1 ], inst["active_coeff_moduli" ][-2 ] return (delta ** 4 ) / (q_last * q_prev * report_scale) def candidate_dots (inst: dict ) -> List [List [int ]]: """ 对每条查询,枚举出合法的内积 s s = row · flag 满足:观测答案 ≈ round(H * g1 * g2 * s) + e """ p = inst["lwe_modulus" ] err = inst["error_bound" ] + NOISE_SLACK base = scale_factor(inst) all_cands: List [List [int ]] = [] for row, g1, g2, ans in zip (inst["rows" ], inst["g1" ], inst["g2" ], inst["answers" ]): coef = base * g1 * g2 bound = CHAR_MAX * sum (abs (x) for x in row) cands = [ s for s in range (-bound, bound + 1 ) if mod_dist(centered_mod(round (coef * s), p), ans, p) <= err ] if not cands: raise ValueError(f"empty candidate set at query {len (all_cands)} " ) all_cands.append(cands) return all_cands def recover_flag (inst: dict ) -> str : """ 用 Z3 求解器: 给定 50 条稀疏线性约束 row·flag = s 求唯一满足所有约束的 ASCII 字节数组 flag """ n = inst["num_slots_used" ] cands = candidate_dots(inst) chars: List [ArithRef] = [Int(f"c_{i} " ) for i in range (n)] solver = Solver() for c in chars: solver.add(c >= CHAR_MIN, c <= CHAR_MAX) for row, cand in zip (inst["rows" ], cands): lin = Sum([IntVal(row[j]) * chars[j] for j in range (n)]) solver.add(Or(*(lin == IntVal(v) for v in cand))) if solver.check() != sat: raise RuntimeError("Z3 UNSAT" ) model = solver.model() return "" .join(chr (model[c].as_long()) for c in chars) def main (): with open ("instance.json" , "r" , encoding="utf-8" ) as f: inst = json.load(f) print (recover_flag(inst)) if __name__ == "__main__" : main()
运行脚本得出 flag。
1 W4terCTF{Us3_tHE_5cA13_WEIL_4nD_r3scalE_Wi7h_cARE!}
2.7 手撕平方根
(1) 题目分析
服务端生成 1024-bit RSA,公钥指数 $e = 2147483647 = 2^{31}-1$,明文为 flag || random_padding(总长 120 字节)。除输出 n、c 外,还提供最多 50 次 平方根 Oracle:输入 $a \in [0, 10000)$,若 $a$ 是模 $n$ 的二次剩余则返回一个平方根,否则报错。
关键代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 p, q = mpz(getPrime(1024 )), mpz(getPrime(1024 )) n = p * q m = mpz(int .from_bytes(flag + urandom(120 - len (flag)), byteorder="big" )) c = powmod(m, 2147483647 , n) for i in range (50 ): a = mpz(input ()) sp, sq = Sqrt(a, p), Sqrt(a, q) if sp is None or sq is None : print (f"there does not exist an integer x such that x * x % n == {int (a)} " ) else : x = min (CRT(sp, sq), CRT(sp, -sq), CRT(-sp, sq), CRT(-sp, -sq)) print (f"the least non-negative integer x such that x * x % n == {int (a)} is {int (x)} " )
模 $n = pq$ 下,二次方程 $x^2 \equiv a \pmod n$ 若有解,共有 4 个 平方根(来自 $(\pm sp, \pm sq)$ 的 CRT 组合)。服务端固定返回其中 最小非负 的那一个。
(2) 分解原理
若找到 $x_0, x_1$ 满足:
$$
x_0^2 \equiv x_1^2 \pmod n, \qquad x_0 \not\equiv \pm x_1 \pmod n,
$$
则 $(x_0 - x_1)(x_0 + x_1) \equiv 0 \pmod n$,而 $n \nmid (x_0 - x_1)$、$n \nmid (x_0 + x_1)$,故:
$$
\gcd(x_0 - x_1, n) \in {p, q}
\quad\text{或}\quad
\gcd(x_0 + x_1, n) \in {p, q}.
$$
得到 $p, q$ 后即可正常 RSA 解密。本题的核心是:在 50 次查询内构造 同一数的两个不同平方根 。
(3) 构造两个不同的平方根
找三个小素数 $\alpha, \beta, \gamma$,满足:
各自是模 $n$ 的 二次非剩余 (Oracle 对单查返回不存在);
两两乘积 $\alpha\beta,\ \alpha\gamma,\ \beta\gamma$ 均为 二次剩余 。
对每对乘积向 Oracle 查询,记:
$$
x = \sqrt{\alpha\beta},\quad y = \sqrt{\alpha\gamma},\quad z = \sqrt{\beta\gamma}.
$$
则:
$$
(xyz)^2 \equiv (\alpha\beta)(\alpha\gamma)(\beta\gamma) \equiv \alpha^2\beta^2\gamma^2 \equiv (\alpha\beta\gamma)^2 \pmod n.
$$
令:
$$
x_0 = xyz \bmod n, \qquad x_1 = \alpha\beta\gamma,
$$
两者都是 $(\alpha\beta\gamma)^2$ 的平方根。由于 $x_0$ 由 Oracle 返回值相乘得到、$x_1$ 是小于 $10000$ 的整数,通常 $x_0 \not\equiv \pm x_1 \pmod n$,一次 $\gcd$ 即可分解。
(4) 查询流程
在 $[2, 100)$ 的小素数中随机扫描,维护三条并行 链 。每条链形如:
1 [(α, []), (β, [√(αβ)]), (γ, [√(αγ), √(βγ)]), ...]
对素数 $p$:
若 $p$ 是二次剩余,跳过;
若某条链为空,将 $p$ 作为链首(第一个非剩余);
否则查询 $p \cdot \text{链首}$:若仍是非剩余则换链;若是剩余则把 $p$ 加入链,并记录 Oracle 返回的平方根;
链长 $\ge 3$ 时,对新加入的 $p$ 再查 $p \cdot \text{链中第 } k \text{ 个素数}$,按公式计算候选 $(x_0, x_1)$ 并尝试 $\gcd$。
三条链对应不同的链首非剩余,提高在 50 次内凑齐合法三元组的概率。实测通常 10 次以内 即可分解。
(5) 完整解题脚本
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 import randomimport reimport socketfrom Crypto.Util.number import GCD, inverse, isPrime, long_to_bytesHOST = "127.0.0.1" PORT = xxxxx def recv_until (sock: socket.socket, marker: bytes , limit: int = 200_000 ) -> bytes : """辅助函数:从 socket 接收数据直到遇到指定标记""" data = b"" while marker not in data and len (data) < limit: chunk = sock.recv(4096 ) if not chunk: break data += chunk return data def recv_line (sock: socket.socket ) -> bytes : """辅助函数:读取一行数据""" data = b"" while b"\n" not in data: chunk = sock.recv(4096 ) if not chunk: break data += chunk return data.split(b"\n" , 1 )[0 ] def query_sqrt (sock: socket.socket, a: int ) -> int | None : """核心交互函数:向服务端发送数字 a,请求其平方根""" sock.sendall(f"{a} \n" .encode()) line = recv_line(sock) m = re.search(rb"is (\d+)$" , line) return int (m.group(1 )) if m else None def factor_with_oracle (sock: socket.socket, n: int ) -> tuple [int , int ]: """核心算法:利用平方根预言机构造碰撞,分解模数 n""" primes = [i for i in range (2 , 100 ) if isPrime(i)] random.shuffle(primes) chains: list [list [tuple [int , list [int ]]]] = [[], [], []] for p in primes: if query_sqrt(sock, p) is not None : continue for chain in chains: if not chain: chain.append((p, [])) break root = query_sqrt(sock, chain[0 ][0 ] * p) if root is None : continue chain.append((p, [root])) for k in range (1 , len (chain) - 1 ): root_k = query_sqrt(sock, p * chain[k][0 ]) chain[-1 ][1 ].append(root_k) for u in range (k): x = chain[-1 ][1 ][k] * chain[-1 ][1 ][u] * chain[k][1 ][u] % n y = chain[-1 ][0 ] * chain[k][0 ] * chain[u][0 ] for diff in (x - y, x + y): g = GCD(diff % n, n) if 1 < g < n: return g, n // g break raise RuntimeError("failed to factor within query budget" ) def extract_flag (m_int: int , n: int ) -> bytes : k = (n.bit_length() + 7 ) // 8 blob = long_to_bytes(m_int, k) start = blob.find(b"W4terCTF{" ) if start == -1 : return blob end = blob.find(b"}" , start) return blob[start: end + 1 ] if end != -1 else blob[start:] def main (): sock = socket.create_connection((HOST, PORT), timeout=10 ) sock.settimeout(10 ) banner = recv_until(sock, b"Feel free" ) ns = re.search(rb"n = (\d+)" , banner) cs = re.search(rb"c = (\d+)" , banner) if not ns or not cs: raise RuntimeError("failed to parse n/c" ) n, c = int (ns.group(1 )), int (cs.group(1 )) p, q = factor_with_oracle(sock, n) if p > q: p, q = q, p e = 2147483647 phi = (p - 1 ) * (q - 1 ) d = inverse(e, phi) m = pow (c, d, n) print (extract_flag(m, n).decode(errors="replace" )) if __name__ == "__main__" : main()
运行脚本得出 flag。
1 W4terCTF{H0W_TO_1aCtorI23_a_NUm83R_w1th_Qu4dRAT1c_r3sldU3}
3 Pwn
3.4 等长条
题目描述
你好,玩家。欢迎回到 W4terCTF 游戏厅:Capture The Fun!
在过去的几年里,我们曾一起在 2048、贪吃蛇里冲刺高分,也共同在扫雷、推箱子中步步为营 …… 是大家的努力,让 W4terCTF 游戏厅仍然屹立不倒,依旧蒸蒸日上!
而在 4 月 26 日,我们游戏厅也将展开新的篇章。没错,听好了 — 游戏厅就此陷落(倒闭)!都怪你们一年里只来一个星期,我们连发奖金的钱都没了。现在只剩下这台 Beyond 难度的俄罗斯方块 ,你们凑合玩去吧。
1 W4terCTF{I've_833n_waITIn6_FOr_4935!_h4S_THE_PRO8A81LI7y_6Een_5eCREt1y_chaN6eD?!}
3.5 能力开发・缺陷电气
题目描述
数据不保证,1 ≤ q ≤ 100,开始的字符串长度 ≤ 100。
1 W4terCTF{WH3n_aLl_GUAR4n7EeS_V4nIsH,_evErythInG_CO1lAPs3S}
3.6 能力开发・路径穿过
1 W4terCTF{wAIK_THrOu6h_tHE_P47h_tO_bEhoID_A_bE7Ter_vIEW}
3.7 能力开发・鸟瞰把握
1 W4terCTF{I_s3aRcH_the_woR1d_fOr_yoU,_wI5H1nG_t0_5C4N_TH3_r3S7_wi7h_M3}
4 Web
4.2 Safe Store
题目描述
Rust 以安全著称,既然通过了编译,那这个商店就是安全的 …… 吗?
1 W4terCTF{c0MPiLa7lON_Doe5_n0T_m34N_SAFe7y}
4.4 ez blog
题目描述
小李子的博客经历了不停的迁移,某天小李子突然看到了博客的远古版本备份,但是他用正确的账号密码怎么登录不上去???小李子最后发现这个 blog 没有他想象中那么 ez …
1 W4terCTF{whY_WOu1d_A_r3V3rs3_shE1l_AnD_cve_4PPeaR_In_A_8lO9?}
4.7 乌萨奇的硬币
题目描述
Usagi 撿到了一枚奇怪的硬幣,Hachiware 試著投擲了幾次,發現正反面的概率竟然不一樣!硬幣的側面有一行小字:“flag を探しているって聞いた?正面の確率を推測してみて!”
1 W4terCTF{lo77Ry_1S_faK3_aND_PRopeR_use_O1_S355ION_CO0kle_Or_NAn_CAn_5Olv3_Th3_PR0BI3M}
5 Reverse
5.1 Bluntstone
题目描述
Tarnished, a hardened Bluntstone blocks your path. Shatter its compressed shell to reveal the shifting logic within. Reverse the chaotic magic and reclaim your lost Grace.
1 W4terCTF{3VeN_A_bIun7s7oNE_can_DE1IEct_60DS}
5.2 FunSokoban
题目描述
请逆向分析附件,找出一段正确的操作序列,使程序进入成功分支。
你需要提交的是触发成功输出所需的输入字符串。flag 格式为:W4terCTF{输入字符串}。
1 W4terCTF{dwwwaawdsdsssaawwssddwwwawaasdsssddwwwdwaadssssaawwwdwdsswaaawdf}
5.3 Strange puts
1 W4terCTF{NIc3_CA7CH_THa7_pTr_WaS_5WAPpEd_aI1_AI0n9}
5.4 Vigorcheck
题目描述
The Tree Sentinel stands as the first trial for any Tarnished who dares set foot in the Lands Between. He wields three devastating strokes to shatter the resolve of the weak. Canst thou fell the golden guardian and claim thy Grace?
1 W4terCTF{F0U1_7ArNIsh3D_P4sSED_7h3_vIgOr_cHECk!}
5.5 开始电力运输
题目描述
《终末地的脉动:第十三号能源网络的重建》
在“佩丽卡”这片被历史遗忘的大地上,风总是带着铁锈和源石尘埃的苦涩味道。对于阿戈尔而言,这里既是流放之地,也是重生的摇篮。巨大的工业遗迹如同远古巨兽的骸骨,横亘在红色的荒原之上,而在这些骸骨之间,技术开拓者们正用双手编织着新的神经网络。
故事发生在一处代号为“扇区-09”的资源采集点。这里曾经是旧时代繁华的能源中枢,但在漫长的历史断层中,它早已沉寂,只有那些锈迹斑斑的输电塔还依稀保留着往日的威严。开拓者小队抵达这里时,迎接他们的只有死一般的寂静和警报灯微弱的闪光。这次的任务很明确,却异常艰巨:重启 扇区-09 的核心供电系统,并将周边荒废的前哨站重新接入主网络。
拉电线,听起来是一件枯燥乏味的基础体力活,但在终末地,这项工作被称为“接引生命线”。每一根沉重的线缆都承载着数万伏特的源石动能,它们不仅仅是传输电流的介质,更是连接各个分散据点的纽带。没有了这些线缆,环境改造机无法运转,滤净塔无法提供呼吸用的空气,那些在此地驻守的工程师们也将被黑暗与严寒吞噬。
女主角,年轻的开拓组组长,站在布满碎石的高地上。她的视野尽头是 扇区-09 的主控室,那里有一座巨大的变电站,像是一颗停止跳动的心脏。她手中的终端屏幕闪烁着,显示着错综复杂的电路拓扑图。她深吸一口气,通过通讯频道下达了指令:“铺设组,准备进行最后一段的架设。这根线缆一旦接通,第 13 号能源网络就能全线点亮。”
风沙骤起,打在防护服的玻璃面罩上,发出细碎的声响。重型工程机甲缓缓启动,它们的机械臂上缠绕着粗大的黑色管线。这不是普通的数据线,而是能够承受极端环境的输电主干。机甲操作员必须拥有极高的精准度,因为在狂风和崎岖地形的干扰下,将线缆对接到只有几毫米宽的接口上,无异于在风暴中穿针引线。
随着绞盘的轰鸣声,第一根线缆被拉向了远方的塔架。线缆在空中绷直,发出沉闷的低鸣,仿佛是一张巨大的弓被拉满。这就是“拉电线”的艺术 —— 它是对物理法则的挑战,也是对意志力的考验。每一个拉扯的动作都需要协同,前方的机甲在探路,后方的工程兵在固定支点,中间的观察员时刻监视着线缆的张力,防止因为过紧而崩断,或者因为过松而触地磨损。
在这个过程中,开拓者们面对的不仅仅是物理上的困难。扇区-09 的地下深处隐藏着不稳定的源石矿脉,电磁风暴时刻威胁着线路的安全。有一次,一场突如其来的地磁暴让刚刚架设好的两百米线路瞬间过载,绝缘层冒出了青烟。如果不及时切断并重新铺设,整个前哨站的备用能源都会被烧毁。
“稳住!不要慌!”组长在频道中大声喊道。她冲了上去,手动开启了紧急隔离阀。那一刻,蓝色的电弧在她头顶炸裂,将昏暗的荒原照得亮如白昼。她手动调整了变压器的输出功率,利用旧时代遗留的稳压技术,强行驯服了狂暴的电流。她的手套被高温烫得发黑,但她没有后退半步。当电流重新平稳地流过线缆时,她感觉到的不是恐惧,而是一种源自工业理性的震撼——电流在流动,意味着信息在流动,意味着文明在流动。
经过三天三夜的连续奋战,最后一根主电缆终于抵达了变电站的接口。
那个瞬间,是整段工程的高潮。所有的工程机甲都停止了轰鸣,所有的开拓者都屏住了呼吸。组长颤抖着双手,将插头对准了那深邃的接口。随着“咔哒”一声清脆的机械咬合声,仿佛命运的齿轮重新啮合。
线路接通了。
刹那间,灯光如多米诺骨牌般沿着塔架一层层亮起,原本漆黑的 扇区-09 瞬间被点亮。巨大的输电塔顶端的警示灯开始有节奏地闪烁,红色的光穿透了漫天的风沙,向远处的荒原宣告着人类的归来。电流顺着新铺设的线缆奔向远方的前哨站,那些沉寂的机器重新苏醒,滤净塔开始轰鸣,温暖的灯光驱散了室内的寒冷。
但这不仅仅是光明的降临。在变电站的屏幕上,一组组数据开始疯狂跳动。这是通信数据的潮汐,是佩丽卡协议的握手信号。通过这根物理上的电线,数字逻辑开始在网络中蔓延。分散在各地的服务器、数据库、自动防御系统,因为这一根根导线的连接,重新形成了一个统一的智能体。
组长瘫坐在碎石地上,摘下头盔,任由汗水流下。她看着远处的通明灯火,看着那些横跨天际的黑色线缆,它们在夜色中显得格外迷人。这就是终末地的浪漫——用钢铁和铜线,在绝望中编织希望。每一次接线,都是在对这片说“不”的世界说“是”。电流的嗡嗡声在耳边回响,听起来就像是这片大地重新恢复的心跳声。
在这片荒原上,没有谁是孤岛。只要电线还在连接,信号还在传输,文明的火种就不会熄灭。这便是技术开拓者的誓言,也是每一个在风沙中拉线的人,内心最深的骄傲。
1 W4terCTF{COnN3C7_0NE_8Y_OnE_wiTH1N_boM_th4t_15_EAsY_70_PerfoRM_8UT_l_PreF3r_u5In6_4_x1ranl7E_Re14Y_iNS7EAd}
6 Forensics
6.1 损坏的 Sysprep
题目描述
小李子从「LilRan 德厚门」网站上下载了一个 Windows 镜像想要重装系统,但他发现无人值守文件被删掉了,到底要怎么办口牙!
访问网页后得到了 install.wim 文件。这是 Windows 的标准映像文件,里面可能打包了多份映像。
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 26 27 28 29 30 31 32 33 34 35 36 37 scc@Ubuntu:/data$ wiminfo install.wim WIM Information: ---------------- Path: install.wim GUID: 0x4a14c8dcf7960a55db18143f8b82a91a Version: 68864 Image Count: 2 Compression: LZX Chunk Size: 32768 bytes Part Number: 1/1 Boot Index: 0 Size: 1139056 bytes Attributes: Relative path junction Available Images: ----------------- Index: 1 Name: Windows 10 Pro Description: Windows build 19045 Directory Count: 6 File Count: 1 Total Bytes: 36 Hard Link Bytes: 0 Creation Time: Mon Apr 27 02:12:05 2026 UTC Last Modification Time: Mon Apr 27 02:12:05 2026 UTC WIMBoot compatible: no Index: 2 Name: Windows PE Description: ES5S temporary recovery disk Directory Count: 1 File Count: 1 Total Bytes: 10485760 Hard Link Bytes: 0 Creation Time: Mon Apr 27 02:12:05 2026 UTC Last Modification Time: Mon Apr 27 02:12:28 2026 UTC WIMBoot compatible: no
可以看到有 2 个映像。其中 Windows 10 Pro 只有 36 字节,是个空壳。把第二个 Windows PE 挂载下来:
1 2 3 4 5 6 scc@Ubuntu:/data$ mkdir mount scc@Ubuntu:/data$ wimextract install.wim 2 / --dest-dir mount Extracting file data: 10 MiB of 10 MiB (100%) done Done extracting files. scc@Ubuntu:/data$ ls /data/mount windows_sysprep.img
如果在 Windows 下查看,双击该文件会提示“光盘映像文件已损坏”。
1 2 scc@Ubuntu:/data/mount$ file windows_sysprep.img windows_sysprep.img: DOS/MBR boot sector, code offset 0x58+2, OEM-ID "mkfs.fat" , sectors 20480 (volumes <=32 MB), Media descriptor 0xf8, sectors/track 32, FAT (32 bit), sectors/FAT 158, serial number 0x5f512357, unlabeled
1 W4terCTF{dEIe7inG_do3s_nO7_meaN_de1Et3d}
7 PPC
7.3 日历・序章
题目描述
小 T 在不断练习括号序列匹配问题。为了让练习更容易一些,小 T 在每天会根据日历执行一个简单的操作。这样的练习一直持续到 2026 年 12 月 31 日,然而小 T 盼望的 2027 年没有到来。相反,他回到了 2026 年 1 月 1 日。
小 T 瞬间意识到,自己进入了一个时间循环。他希望找到一个新的日历,使得即使在时间循环中也能正常完成练习。显然,小 T 不会规划每天需要执行的操作,因此他希望你能为他设计一个新的日历。
身处时间循环之外的你可以从附件中查看循环和操作的细节。
1 W4terCTF{cONGrA7uI4tions_FOR_yOUr_FirSt_ca1eND4R_pRogr4M_k33p_0N_to_Th3_nex7_0N3}
9 OSINT
9.1 Find Me
题目描述
通常来说,在 OSINT 题里只要找到图中大致的位置就可以了。但是在 W4terCTF 的赛场上,似乎一切都需要更加精确才行 … ?
flag 的内容:图中拍摄者所在地的经纬度,纬度在前经度在后,精确到小数点后七位。
flag 样例:W4terCTF{12.3456789S,98.7654321E}
图片中带有 Google 水印,可以确定来源为谷歌街景地图。观察图片右侧的路牌提取关键信息:道路编号为 N277 ,主路通往 Zeeland 和 Uden 两地,同时可见 Wa 与 Wi 开头的不完整地名标识。
打开 谷歌地图 :搜索 N277,发现该道路位于荷兰,但无法具体定位。搜索 Zeeland 或 Uden 可以匹配到具体的城市。缩小地图发现下图所示的路口符合条件,同时确认了不完整字样为 Wanroij 和 Wilbertoord 这两处地点。
点击该路口,进入街景模式。反复尝试发现在该点位选择合适的角度可以完美匹配原图,读取网址上的经纬度。
按照指定格式构造 flag。
1 W4terCTF{51.6329259N,5.7476876E}
9.3 无论到哪都爱吃麦麦
题目描述
Hachiwa0 在旅途中饥肠辘辘,走着走着竟然看到麦当劳!他毫不犹豫地点了麦麦大快朵颐,这是他吃过的最好吃的一顿麦当劳。许久之后,他翻到了这张照片,却不记得这到底是哪里了。请帮 Hachiwa0 找到这家麦当劳的电话号码和经纬度。
flag 的内容:图中麦当劳的十位电话号码,图中 Hachiwa0 所在地的经纬度,纬度在前经度在后,精确到小数点后四位。
flag 样例:W4terCTF{1145141919_88.8888N_88.8888E}
图的关键信息集中在左下角的餐车,但实在过于模糊无法辨认出与地点有关的信息。利用 谷歌识图 找到了相似的店铺。
打开谷歌地图,搜索 McDonald's Kokusaidori Makishi 找到了店铺,并确定了店铺名称为 国際通り牧志店。
进入街景模式,走进左边的小路,匹配成功!读取网址上的经纬度,精确到小数点后四位。
下方给出了电话号码,但尝试过 81988690719 和 988690719 均不正确(不是十位)。
后来在 日本麦当劳官网 上看到了符合要求的电话号码。
按照指定格式构造 flag。
1 W4terCTF{0988600719_26.2168N_127.6890E}