第四届黄河流域wp

admin 2026-06-23 05:41:28 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文为第四届黄河流域CTF赛题writeup,重点分析两道Web题目。realGrafana利用CVE-2024-9264漏洞通过DuckDB的readblob函数实现任意文件读取获取flag;ezlog通过原型污染绕过proto过滤污染Object.prototype,结合数组特性绕过路径校验实现文件读取。文档提供完整漏洞原理分析、利用步骤和可执行PoC代码。 综合评分: 85 文章分类: CTF,WEB安全,漏洞分析


1.3 数据特征分析

提取结果包含两部分:

1.559位十进制数字 — 这是一个非常大的整数2.3913字节二进制数据 — 恰好等于 559 × 7

4472 = 559 × 8 = 3913 × 8 / 7

这种完美对齐暗示:每个数字对应 7 字节二进制数据(56 位),可能是 7×8 的位图字体数据。


Phase 2:Tupper 自指公式

2.1 关键发现

对提取的 559 位数字进行整除性检测:

num % 2 = 0num % 17 = 0      ← 关键!

Tupper 自指公式的标准形式为:

½ < ⌊mod(⌊y/17⌋ ×&nbsp;2^(-17⌊x⌋ - mod(⌊y⌋,&nbsp;17)),&nbsp;2)⌋

其中参数 k 由 k = num / 17 计算得到。

2.2 渲染实现

from&nbsp;PIL import Image
k&nbsp;= num //&nbsp;17# 计算渲染宽度:k的二进制位数除以17后向上取整W&nbsp;= (k.bit_length() +&nbsp;16) //&nbsp;17&nbsp; # =&nbsp;109H&nbsp;=&nbsp;17
img&nbsp;= Image.new("1", (W, H))for&nbsp;x in range(W):&nbsp; &nbsp;&nbsp;for&nbsp;y in range(H):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# Tupper公式的核心位运算&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;bit&nbsp;= (k >> (17&nbsp;* x + y)) &&nbsp;1&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;img.putpixel((x, H -&nbsp;1&nbsp;- y),&nbsp;255&nbsp;if bit else&nbsp;0)
img.resize((W *&nbsp;16, H *&nbsp;16), Image.NEAREST).save("tupper_result.png")

2.3 宽度问题(踩坑点)

经典的 Tupper 公式演示通常用 106 列宽度,但本题的 k 参数需要 109 列才能完整渲染出所有字符。如果仅渲染 106 列,最右侧的字符”Y”会被截断,导致密码错误。

验证方法:

print(k.bit_length() /&nbsp;17) &nbsp;# ≈&nbsp;108.82&nbsp;→ 需要ceil=109列

2.4 渲染结果

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ██&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ██████ &nbsp; ████ &nbsp;████ &nbsp;████ &nbsp; ██ &nbsp;██ &nbsp; &nbsp;███ &nbsp; ██ &nbsp; &nbsp;████ &nbsp; &nbsp;██ &nbsp;████ &nbsp; &nbsp; &nbsp; ██ &nbsp;██ ██ &nbsp; ██ &nbsp;██ &nbsp; &nbsp;██ &nbsp;██ &nbsp; &nbsp;██ &nbsp;█████ &nbsp; ████ &nbsp;█ &nbsp;██ &nbsp;██ ██ &nbsp;█ ██ &nbsp;█ ██ &nbsp;██ &nbsp;██ &nbsp;█ &nbsp;██ &nbsp; ██ &nbsp;█ &nbsp; &nbsp;██ &nbsp;██ &nbsp;█ &nbsp; &nbsp; &nbsp; ██ &nbsp;██ ██ &nbsp; ██ &nbsp;██ &nbsp; &nbsp;██ &nbsp;██ &nbsp; &nbsp;██ &nbsp;██ &nbsp; &nbsp; &nbsp;████ &nbsp;█ &nbsp;████ &nbsp; ██ &nbsp; &nbsp;██ &nbsp; ██ ██ ██ &nbsp;██ &nbsp;█ &nbsp;██ &nbsp; ██ &nbsp;█ &nbsp; &nbsp;██ &nbsp; &nbsp;██ &nbsp; ██████ ██ ██ &nbsp;██ &nbsp; ██ &nbsp;██ &nbsp; &nbsp;██ &nbsp;██ &nbsp; &nbsp;██ &nbsp;██ &nbsp; &nbsp; &nbsp;████ &nbsp;█ &nbsp; &nbsp; █ &nbsp;██ &nbsp; ██ &nbsp; &nbsp;██ &nbsp;██ ██ &nbsp;██ &nbsp;█ &nbsp;██ &nbsp; ██ &nbsp;█ &nbsp; &nbsp;██ &nbsp;██ &nbsp; &nbsp; &nbsp;██ &nbsp;█ &nbsp;██ ██ &nbsp;██ &nbsp; ██ &nbsp;██████ &nbsp;██████ &nbsp;██ &nbsp; &nbsp; &nbsp;████ &nbsp;█ &nbsp;██ &nbsp;█ ██ &nbsp;█ ██ &nbsp;█ ██ &nbsp;██ █ &nbsp;██ &nbsp;█ &nbsp;██ &nbsp; ██ &nbsp;█ &nbsp; &nbsp;██ &nbsp;██ &nbsp;█ &nbsp; &nbsp; ██ █ &nbsp;██ ██ &nbsp;██ &nbsp;██ &nbsp;██ &nbsp; &nbsp;██ &nbsp;██ &nbsp; &nbsp;██ &nbsp;██ &nbsp; &nbsp; ██ █████ &nbsp; ███ &nbsp; ███ &nbsp; ███ &nbsp; ██ &nbsp;██ &nbsp;█ &nbsp;███ &nbsp; ███ &nbsp;████ &nbsp; &nbsp;██ &nbsp;███ &nbsp; &nbsp; &nbsp;██ █ ███ ████ &nbsp; ██ &nbsp; &nbsp;██ &nbsp;██ &nbsp; &nbsp;██ &nbsp;██ &nbsp; &nbsp; &nbsp;██ █&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ██ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ██ &nbsp;██ ██ &nbsp;██ &nbsp; &nbsp; &nbsp;██ &nbsp; &nbsp;██ &nbsp;██ &nbsp; &nbsp;██ &nbsp;██ &nbsp; &nbsp; ██ &nbsp; ██&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ██ &nbsp; &nbsp;██ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ██ &nbsp;██ &nbsp;██ &nbsp; &nbsp; &nbsp;██ &nbsp; &nbsp;██ &nbsp;██ &nbsp; &nbsp;██ &nbsp;██ &nbsp; &nbsp; ██ &nbsp; ██

读取得到密码:4thHHLY

观察图像可发现,”4th”是序数词”第四”的缩写,”HHLY”是四个大写字母,整体看起来像是一串随机密码。


Phase 3:SilentEye 解密

3.1 工具选择依据

题目描述中的”寂静”二字(英文 silent)直接指向 SilentEye这款隐写工具。SilentEye 是一个跨平台的隐写工具,支持 BMP/JPEG/WAV 格式,使用 F5 算法 对 JPEG 图像进行隐写。

F5 算法的特点:

•在 JPEG 的 DCT 量化系数中嵌入数据•使用矩阵编码(matrix embedding)减少修改量•支持密码保护的伪随机系数选择•支持 AES 加密和 zlib 压缩

3.2 解密操作

打开 SilentEye,执行:

1.拖入 2.jpg 到窗口2.选择 Decode 模式3.输入密码 4thHHLY4.点击提取

3.3 关联验证

“高塔之巅”的双关含义:

| 中文 | 英文 | 对应 | | — | — | — | | 高塔 | Tower → Tupper | Tupper 自指公式(谐音双关) | | 寂静 | Silent → SilentEye | SilentEye 隐写工具 |

Tupper 公式生成密码 → SilentEye 用密码解密 → 最终 flag 呼应了这条链条:

Flag

sdpcsec{get_to_the_upper_and_to_the_Tupper}

flag 语义:”去往高塔之巅,到达 Tupper”——与题目描述”于高塔之巅,窥视寂静中的真相”完美呼应。

encrypt

一道将算法逆向数字水印结合的 Misc 题,考察 PyInstaller 解包、SRPM 置换分析与频域水印提取能力。


文件清单

| 文件 | 说明 | | — | — | | Encrypt.exe | PyInstaller 打包的加密程序 | | 3.png | 加密后的图片,文件名提示轮数 |


0x1 Encrypt.exe 逆向

用 pyinstxtractor 解包 Encrypt.exe,发现是 Python 3.13 打包。入口为 srpm_cli.py,核心加密逻辑在 encrypt.py 中。

加密算法是一种 SRPM(Self-Reversible Permutation)置换,对 RGB 三通道分别独立操作:

1.将图片每个通道展平为一维数组(长度 length = width × height)2.对每个通道执行 rounds 轮置换3.每轮中,从前到后遍历下标 i,与 swap_target(i) 处的像素交换

def swap_target(index,&nbsp;length, round_index, channel_index):&nbsp; &nbsp;&nbsp;return&nbsp;(&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;index&nbsp;*&nbsp;index&nbsp; &nbsp; &nbsp; &nbsp; + (2&nbsp;* round_index +&nbsp;3) *&nbsp;index&nbsp; &nbsp; &nbsp; &nbsp; +&nbsp;7&nbsp;* (channel_index +&nbsp;1)&nbsp; &nbsp; ) %&nbsp;length

参数说明:

index — 当前处理的下标•length — 通道数组长度•round_index — 当前轮次(0 到 rounds-1)•channel_index — 通道编号(RGB 对应 0, 1, 2


0x2 置换的可逆性分析

观察到置换函数是确定性的纯函数——给定相同输入,输出总是相同。且交换操作是对合的(交换两次恢复原状)。

因此解密只需:

轮次逆序:从最后一轮回到第一轮•下标逆序:从最后一个元素往前处理

关键细节——3.png 的文件名暗示 rounds = 3。如果使用程序默认的 5 轮,解密后仍是噪声。


0x3 逆置换复原

from&nbsp;pathlib import Pathimport&nbsp;numpy as npfrom&nbsp;PIL import Image
INPUT&nbsp;= Path("3.png")ROUNDS&nbsp;=&nbsp;3
def&nbsp;swap_target(index, length, round_index, channel_index):&nbsp; &nbsp;&nbsp;return&nbsp;(index * index + (2&nbsp;* round_index +&nbsp;3) * index +&nbsp;7&nbsp;* (channel_index +&nbsp;1)) % length
img&nbsp;= Image.open(INPUT).convert("RGB")w, h = img.sizeraw&nbsp;= img.tobytes()length&nbsp;= w * h
# 分离 RGB 三通道channels&nbsp;=&nbsp;[bytearray(raw[ch::3]) for ch in range(3)]
# 逆置换(逆序轮次 + 逆序下标)for&nbsp;ci, vals in enumerate(channels):&nbsp; &nbsp;&nbsp;for&nbsp;ri in range(ROUNDS -&nbsp;1, -1, -1):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;i in range(length -&nbsp;1, -1, -1):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;t&nbsp;= swap_target(i, length, ri, ci)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;vals[i], vals[t] = vals[t], vals[i]
# 合并通道merged&nbsp;= bytearray(len(raw))merged[0::3] = channels[0]merged[1::3] = channels[1]merged[2::3] = channels[2]
Image.frombytes("RGB", (w, h), bytes(merged)).save("restored.png")

运行后得到一张清晰的海边风景图,但肉眼看不到 flag。


0x4 FFT 盲水印提取

还原图没有明显水印,考虑单图盲水印——将信息隐藏在频域中。

对灰度图做二维离散傅里叶变换(2D-FFT):

from&nbsp;PIL&nbsp;import&nbsp;ImageOps
gray = img.convert("L")arr = np.array(gray, dtype=np.float32)
freq = np.fft.fftshift(np.fft.fft2(arr))mag = np.log1p(np.abs(freq)) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 对数压缩mag = (mag - mag.min()) / (mag.max() - mag.min()) *&nbsp;255&nbsp;&nbsp;# 归一化
ImageOps.autocontrast(&nbsp; &nbsp; Image.fromarray(mag.astype(np.uint8))).save("watermark_fft.png")

关键操作说明:

| 步骤 | 作用 | | — | — | | fft2 | 二维 FFT,将空间域转到频域 | | fftshift | 将低频移到中心,便于观察 | | log1p | 对数压缩幅度谱的大动态范围 | | autocontrast | 自动增强对比度,使水印清晰可见 |

在频谱图正中央清晰显示:

flag{Wei_Hai_journey}

同时在中心下方存在对称倒置副本——这是实信号 FFT 结果的共轭对称特性。


完整解题脚本

from&nbsp;pathlib&nbsp;import&nbsp;Pathimport&nbsp;numpy&nbsp;as&nbsp;npfrom&nbsp;PIL&nbsp;import&nbsp;Image, ImageOps
INPUT = Path("3.png")RESTORED = Path("restored.png")FFT_OUT = Path("watermark_fft.png")ROUNDS =&nbsp;3
def&nbsp;swap_target(index, length, round_index, channel_index):&nbsp; &nbsp;&nbsp;return&nbsp;(index * index + (2&nbsp;* round_index +&nbsp;3) * index +&nbsp;7&nbsp;* (channel_index +&nbsp;1)) % length
# Step 1: 逆置换img = Image.open(INPUT).convert("RGB")w, h = img.sizeraw = img.tobytes()length = w * h
channels = [bytearray(raw[ch::3])&nbsp;for&nbsp;ch&nbsp;in&nbsp;range(3)]for&nbsp;ci, vals&nbsp;in&nbsp;enumerate(channels):&nbsp; &nbsp;&nbsp;for&nbsp;ri&nbsp;in&nbsp;range(ROUNDS -&nbsp;1, -1, -1):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(length -&nbsp;1, -1, -1):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; t = swap_target(i, length, ri, ci)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; vals[i], vals[t] = vals[t], vals[i]
merged =&nbsp;bytearray(len(raw))merged[0::3] = channels[0]merged[1::3] = channels[1]merged[2::3] = channels[2]restored = Image.frombytes("RGB", (w, h),&nbsp;bytes(merged))restored.save(RESTORED)
# Step 2: FFT 水印提取gray = restored.convert("L")arr = np.array(gray, dtype=np.float32)freq = np.fft.fftshift(np.fft.fft2(arr))mag = np.log1p(np.abs(freq))mag = (mag - mag.min()) / (mag.max() - mag.min()) *&nbsp;255fft_img = ImageOps.autocontrast(Image.fromarray(mag.astype(np.uint8)))fft_img.save(FFT_OUT)
print(f"[+] restored:&nbsp;{RESTORED}")print(f"[+] fft watermark:&nbsp;{FFT_OUT}")print(f"[+] flag: flag{{Wei_Hai_journey}}")

Flag

flag{Wei_Hai_journey}

鲨士比亚王国的金融危机

题目信息

类型: Misc•flag 格式flag{...}附件flag.png + SCB.png


文件分析

| 文件 | 尺寸 | 总像素数 | | — | — | — | | flag.png | 889 × 889 | 790,321 | | SCB.png | 2587 × 307 | 794,209 |

肉眼观察:

flag.png 是一张彩色图片•SCB.png 是黑底白字的 “SCB” 三个字母

关键发现:统计 SCB.png 中白色像素数量,恰好也是 790,321,与 flag.png 像素总数完全一致。


解题思路

Step 1 — 理解题意

两个图片像素数精确匹配,说明 flag.png 的每一个像素可以被填入 SCB.png 的白色区域。

“SCB” 暗示了填充顺序:Spiral Center Begin — 从中心开始螺旋。

Step 2 — 螺旋读取

对 flag.png 从中心像素出发,按照 右 → 下 → 左 → 上 的方向螺旋向外遍历,得到一维像素序列。

Step 3 — 填入模板

将螺旋序列按行优先顺序填入 SCB.png 的所有白色像素位置,黑色像素保持不变。


核心脚本

from&nbsp;pathlib&nbsp;import&nbsp;Pathimport&nbsp;numpy&nbsp;as&nbsp;npfrom&nbsp;PIL&nbsp;import&nbsp;Image

def&nbsp;center_spiral(n):&nbsp; &nbsp;&nbsp;"""从 (n//2, n//2) 开始按右→下→左→上螺旋生成坐标"""&nbsp; &nbsp; x = y = n //&nbsp;2&nbsp; &nbsp; coords = [(y, x)]&nbsp; &nbsp; dirs = [(1,&nbsp;0), (0,&nbsp;1), (-1,&nbsp;0), (0, -1)] &nbsp;# R, D, L, U&nbsp; &nbsp; step, d =&nbsp;1,&nbsp;0
&nbsp; &nbsp;&nbsp;while&nbsp;len(coords) < n * n:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;_&nbsp;in&nbsp;range(2):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dx, dy = dirs[d %&nbsp;4]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;_&nbsp;in&nbsp;range(step):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; x += dx&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; y += dy&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;0&nbsp;<= x < n&nbsp;and&nbsp;0&nbsp;<= y < n:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; coords.append((y, x))&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;len(coords) == n * n:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;coords&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; d +=&nbsp;1&nbsp; &nbsp; &nbsp; &nbsp; step +=&nbsp;1&nbsp; &nbsp;&nbsp;return&nbsp;coords

# 读取图片flag = np.array(Image.open("flag.png").convert("RGB"))scb = np.array(Image.open("SCB.png").convert("RGB"))n = flag.shape[0]
# 获取白色像素掩码white = (scb[:, :,&nbsp;0] >&nbsp;128) & (scb[:, :,&nbsp;1] >&nbsp;128) & (scb[:, :,&nbsp;2] >&nbsp;128)assert&nbsp;white.sum() == n * n &nbsp;# 验证像素数匹配
# 螺旋取像素 → 填入白色区域coords = center_spiral(n)spiral_pixels = flag[[y&nbsp;for&nbsp;y, _&nbsp;in&nbsp;coords], [x&nbsp;for&nbsp;_, x&nbsp;in&nbsp;coords]]
recovered = scb.copy()recovered[white] = spiral_pixelsImage.fromarray(recovered).save("recovered_flag.png")
# 提取绿色文字(手写 flag)+ 黑色 SCB 轮廓r, g, b = recovered[:, :,&nbsp;0].astype(int), recovered[:, :,&nbsp;1].astype(int), recovered[:, :,&nbsp;2].astype(int)green = (g > r +&nbsp;25) & (g > b +&nbsp;5) & (g >&nbsp;90) & (r <&nbsp;230) & (b <&nbsp;230)
clean = np.full_like(recovered,&nbsp;255) &nbsp;# 白色背景clean[green] = [34,&nbsp;177,&nbsp;76] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 绿色文字clean[~white] = [0,&nbsp;0,&nbsp;0] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 黑色 SCB 轮廓Image.fromarray(clean).save("recovered_flag_clean.png")

运行结果

执行脚本后得到 recovered_flag.png,图片中 “SCB” 三个黑色字母区域内出现了绿色的手写文字:

flag{You_saved_SCB_by_the_coin}

可选的 recovered_flag_clean.png 进一步过滤了背景杂色,仅保留绿色文字和黑色 SCB 轮廓,便于阅读。


Flag

flag{You_saved_SCB_by_the_coin}

AD ASTRA

题目信息

题目名称: AD ASTRA•题目类型: Misc(隐写/套娃)•文件flag.pngAD ASTRA.wav


解题思路

Step 1: flag.png 文件分析

首先用 file 或直接查看文件头,发现 flag.png 并非真正的 PNG 文件,而是 WebP/RIFF 格式。

$&nbsp;xxd flag.png |&nbsp;head&nbsp;-152494646..... &nbsp;→ RIFF header (WebP)

WebP 的 RIFF 头部记录了数据大小,由此可以算出 WebP 数据的结束位置:

import&nbsp;struct
data&nbsp;= open("flag.png",&nbsp;"rb").read()riff_size&nbsp;= struct.unpack("<I", data[4:8])[0]webp_end&nbsp;=&nbsp;8&nbsp;+ riff_size
print(webp_end) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#&nbsp;68588print(data[webp_end:webp_end+4]) &nbsp;# b'PK\x03\x04'

在 WebP 数据之后(偏移 68588),发现了 ZIP 文件签名 PK\x03\x04,说明文件尾部被追加了一个 ZIP 压缩包。

提取并解压追加的 ZIP:

with&nbsp;open("hidden.zip",&nbsp;"wb")&nbsp;as&nbsp;f:&nbsp; &nbsp; f.write(data[webp_end:])

解压后得到 9 个文本文件:01.txt ~ 09.txt,每个文件内容都是 Base64 编码的片段

Step 2: Base64 拼接 → 提示图

按文件名顺序拼接所有 Base64 片段并解码:

cat&nbsp;hidden/*.txt |&nbsp;base64&nbsp;-d > clue.png

得到一张提示图,图中文字为:

To&nbsp;the starswe come&nbsp;from

这串文字是下一步 DeepSound 隐写的密码。

Step 3: AD ASTRA.wav — DeepSound 隐写分析

AD ASTRA.wav 是一个 WAV 音频文件。分析其结构,在 data chunk 的 PCM 采样数据中发现了 DeepSound 隐写特征。

提取 DeepSound 头部信息:

import&nbsp;struct
path&nbsp;=&nbsp;"AD ASTRA.wav"data&nbsp;= open(path,&nbsp;"rb").read()
# 找到 data chunkpos&nbsp;=&nbsp;12while&nbsp;pos +&nbsp;8&nbsp;<= len(data):&nbsp; &nbsp;&nbsp;cid&nbsp;= data[pos:pos+4]&nbsp; &nbsp;&nbsp;size&nbsp;= struct.unpack("<I", data[pos+4:pos+8])[0]&nbsp; &nbsp;&nbsp;if&nbsp;cid == b"data":&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;data_start&nbsp;= pos +&nbsp;8&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break&nbsp; &nbsp;&nbsp;pos&nbsp;+=&nbsp;8&nbsp;+ size + (size &&nbsp;1)
# 解码函数(DeepSound 将每4个采样字节的低4位组合成1个数据字节)def&nbsp;decode_normal(buf):&nbsp; &nbsp;&nbsp;out&nbsp;= bytearray()&nbsp; &nbsp;&nbsp;for&nbsp;i in range(0, len(buf) //&nbsp;4&nbsp;*&nbsp;4,&nbsp;4):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;out.append(((buf[i] &&nbsp;0x0f) <<&nbsp;4) | (buf[i+2] &&nbsp;0x0f))&nbsp; &nbsp;&nbsp;return&nbsp;bytes(out)
# 读取头部ds&nbsp;= decode_normal(data[data_start:data_start +&nbsp;104])print("magic &nbsp; &nbsp; &nbsp;=", ds[:4]) &nbsp; &nbsp; &nbsp; # b'DSC2'print("mode &nbsp; &nbsp; &nbsp; =", ds[4]) &nbsp; &nbsp; &nbsp; &nbsp;#&nbsp;4&nbsp;(normal)print("encrypted &nbsp;=", ds[5]) &nbsp; &nbsp; &nbsp; &nbsp;#&nbsp;1&nbsp;(encrypted)print("hash &nbsp; &nbsp; &nbsp; =", ds[6:26].hex()) # SHA1 hash

输出结果:

magic&nbsp; &nbsp; &nbsp; = b'DSC2'mode&nbsp; &nbsp; &nbsp; &nbsp;=&nbsp;4encrypted&nbsp; =&nbsp;1hash&nbsp; &nbsp; &nbsp; &nbsp;= f54db3d444654ff613ca115add3377e62a17205e

头部标志 DSC2 确认这是 DeepSound 2.x 的隐写数据,并且文件处于加密状态。

Step 4: DeepSound 提取隐藏文件

使用 DeepSound 2.2打开 AD ASTRA.wav

1.File → Open Carrier → 选择 AD ASTRA.wav2.输入密码:To the stars we come from3.File → Extract hidden files… → 选择输出目录

成功提取出 flag.docx

Step 5: 解密 flag.docx

flag.docx 是加密的 Office 文档。使用 office2john.py 提取哈希:

python office2john.py flag.docx > hash.txt# flag.docx:$office$*2013*100000*256*16*...

得知密码为 skadi2530,使用 msoffcrypto-tool 解密:

import msoffcrypto
with&nbsp;open("flag.docx",&nbsp;"rb")&nbsp;as&nbsp;f:&nbsp; &nbsp;&nbsp;file&nbsp;= msoffcrypto.OfficeFile(f)&nbsp; &nbsp;&nbsp;file.load_key(password="skadi2530")&nbsp; &nbsp;&nbsp;with&nbsp;open("flag_decrypted.docx",&nbsp;"wb")&nbsp;as&nbsp;of:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;file.decrypt(of)

Step 6: 提取 Flag

DOCX 文件本质上是 ZIP 压缩包,直接解压 flag_decrypted.docx

unzip&nbsp;flag_decrypted.docx -d docx_extracted

查看 word/document.xml 中的文本内容,发现 flag 被拆分成多行隐藏在文档 XML 中:

FLAG&nbsp;NOT THEREsdpcsec{AD_ASTRA_INFINITUM}

拼接得到最终 Flag。

Flag

sdpcsec{AD_ASTRA_INFINITUM}

Ad Astra Infinitum — 向着无尽的星辰

Crypto

Ledger Fog

题目描述

题目提供了一个故意损坏的 ZIP 文件 ledger.broken,其 ZIP 中央目录已被删除,但 local file header 和压缩数据流仍然完整。我们需要从带噪声的观测数据中恢复出一个 128 位的 ledger key。

解压后的每个 page 由 19 字节一条的记录组成,结构如下:

偏移 &nbsp; 长度 &nbsp; &nbsp;字段0&nbsp; &nbsp; &nbsp;&nbsp;2&nbsp; &nbsp; &nbsp; row_id &nbsp; &nbsp; &nbsp; (每页的行号,恢复 key 时用不到)2&nbsp; &nbsp; &nbsp;&nbsp;16&nbsp; &nbsp; &nbsp;mask&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(128&nbsp;位掩码,小端序)18&nbsp; &nbsp; &nbsp;1&nbsp; &nbsp; &nbsp; noisy_bit &nbsp; &nbsp;(0&nbsp;或&nbsp;1,带噪声的观测值)

可用的行满足如下关系:

noisy_bit&nbsp;= parity(mask & key) XOR noise

其中 noise 是一个未知比特,它为 1 的概率约为 14.56%。此外检查所有 mask 后发现其最高 4 位恒为 0,因此实际参与线性方程约束的只有低 124 位。最后 4 个 key bit 需要通过暴力枚举并用以下 sanity check 确认:

sha256(b"ledger-fog-check:"&nbsp;+ key_bytes).hexdigest()[:4] ==&nbsp;"8f42"

flag 的计算方式为:

flag{sha256(b"ledger-fog:"&nbsp;+ key_bytes).hexdigest()[:32]}

第一步:从损坏的 ZIP 中恢复数据

由于中央目录丢失,常规的解压工具无法直接处理。我们可以逐字节扫描 PK\x03\x04 (即 50 4B 03 04) 标记,手动解析 local file header 中的元数据字段来定位和提取压缩流。

import&nbsp;structimport&nbsp;zlib
with&nbsp;open("ledger.broken",&nbsp;"rb") as f:&nbsp; &nbsp;&nbsp;data&nbsp;= f.read()
rows&nbsp;=&nbsp;[]offset&nbsp;=&nbsp;0while&nbsp;True:&nbsp; &nbsp;&nbsp;pos&nbsp;= data.find(b"PK\x03\x04", offset)&nbsp; &nbsp;&nbsp;if&nbsp;pos <&nbsp;0:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break&nbsp; &nbsp;&nbsp;# 解析 local file header&nbsp; &nbsp;&nbsp;fields&nbsp;= struct.unpack_from("<IHHHHHIIIHH", data, pos)&nbsp; &nbsp;&nbsp;sig, ver, flags, comp, mtime, mdate, crc, csize, usize, nlen, elen = fields&nbsp; &nbsp;&nbsp;# 文件数据从 header 末尾开始(30 字节固定头 + 文件名 + 扩展字段)&nbsp; &nbsp;&nbsp;page_start&nbsp;= pos +&nbsp;30&nbsp;+ nlen + elen&nbsp; &nbsp;&nbsp;raw&nbsp;= data[page_start:page_start + csize]&nbsp; &nbsp;&nbsp;page&nbsp;= zlib.decompress(raw, -15) &nbsp;# raw deflate,无 zlib 头
&nbsp; &nbsp;&nbsp;for&nbsp;i in range(0, len(page),&nbsp;19):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;bit&nbsp;= page[i +&nbsp;18]&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;bit in (0,&nbsp;1): &nbsp;# 只保留可用行&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 提取 16 字节 mask 并清除恒为 0 的高 4 位&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;mask&nbsp;= int.from_bytes(page[i +&nbsp;2:i +&nbsp;18],&nbsp;"little")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;mask&nbsp;&= (1&nbsp;<<&nbsp;124) -&nbsp;1&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;rows.append((mask, bit))&nbsp; &nbsp;&nbsp;offset&nbsp;= pos +&nbsp;1

共恢复出 3 个 page,合计 20480 条有效记录。

第二步:理解 LPN 问题

本题是一个经典的 Learning Parity with Noise (LPN) 问题实例:

方程数 n = 20480未知数 k = 124(key 的有效比特数)•噪声率 τ ≈ 0.1456

写成矩阵形式:

A&nbsp;· key = b + e &nbsp; (mod&nbsp;2)

其中 A 是 20480×124 的 mask 矩阵,b 是 noisy_bit 向量,e 是噪声向量(每个比特以概率 τ 为 1)。

如果没有噪声,直接用 124 条线性无关的行做 GF(2) 高斯消元即可恢复 key。但噪声的存在使得这个简单方法失效——124 行中平均有约 18 行带有错误 bit,直接求解得到的结果完全是随机的。

对于当前参数 (n=20480, k=124, τ=0.1456),几种常见方法的可行性如下:

| 方法 | 可行性分析 | | — | — | | 直接高斯消元(忽略噪声) | 失败 —— 124 行中 ~18 个错误使解完全混乱 | | BKW 算法 | 样本不足 —— 每轮归约使样本量减半同时噪声上升,3-4 轮后噪声 ~50% | | 迭代位翻转 | 随机起点不收敛 —— 无初始猜测时每位多数投票为 50/50 | | *信息集解码 (ISD) * | 可行 —— 通过枚举基中少量噪声位置的校正来恢复 key |

第三步:信息集解码 (ISD)

核心思想

随机选取 124 条线性无关的行构成一个基。求解该基对应的线性方程组得到候选 key key0。如果基中没有任何行被噪声污染(即 124 个方程完全正确),那么 key0 就是真 key。

如果基中有 h 行带有噪声,则有:

key0 = M⁻¹ · b = M⁻¹ · (A_basis · key_true + e_basis)&nbsp; &nbsp; &nbsp;= key_true + M⁻¹ · e_basis

修正项 M⁻¹ · e_basis 等于逆矩阵 M⁻¹ 中 h 个列的 XOR。因此真 key 可以通过候选 key 异或若干逆矩阵列得到:

key_true&nbsp;= key0 ⊕ (M⁻¹ 的第 i₁ 列) ⊕ ... ⊕ (M⁻¹ 的第 iₕ 列)

其中 i₁ … iₕ 是基中被噪声污染的行索引。

h ≤ 2 的概率分析

取 τ = 0.1456,124 行中恰好有 h 行带噪声的概率为:

•P(h = 0) = (1−τ)^124 ≈ 3.4×10⁻⁹•P(h = 1) = 124·τ·(1−τ)^123 ≈ 7.2×10⁻⁸•P(h = 2) = C(124,2)·τ²·(1−τ)^122 ≈ 7.5×10⁻⁷•P(h ≤ 2) ≈ 8.2×10⁻⁷

也就是说平均每 120 万个随机基中才有 1 个满足 h ≤ 2。这是整个求解过程的性能瓶颈。

算法流程

对每个随机选取的&nbsp;124&nbsp;行基:&nbsp; &nbsp;&nbsp;1. GF(2) 高斯消元求解 key0,同时计算 M⁻¹&nbsp; &nbsp;&nbsp;2. 生成候选 key:&nbsp; &nbsp; &nbsp; &nbsp;- key0 本身(h=0,1&nbsp;个)&nbsp; &nbsp; &nbsp; &nbsp;- key0 ⊕ inv_col[i](h=1,124&nbsp;个)&nbsp; &nbsp; &nbsp; &nbsp;- key0 ⊕ inv_col[i] ⊕ inv_col[j](h=2,C(124,2)=7626&nbsp;个)&nbsp; &nbsp;&nbsp;3. 用&nbsp;128~256&nbsp;条随机验证行筛选所有候选 key:&nbsp; &nbsp; &nbsp; &nbsp;- 正确 key 的预期错误数:~18~37&nbsp; &nbsp; &nbsp; &nbsp;- 随机 key 的预期错误数:~64~128&nbsp; &nbsp; &nbsp; &nbsp;- 保留明显低于随机水平的候选&nbsp; &nbsp;&nbsp;4. 对存活候选进行全量&nbsp;20480&nbsp;行评分&nbsp; &nbsp;&nbsp;5. 正确 key 的错误数约为&nbsp;2982(14.56%),随机 key 约为&nbsp;10240(50%)

GF(2) 高斯消元求逆

关键子程序是在增广矩阵 [M | I | b] 上做原地高斯消元:

def&nbsp;gf2_invert_and_solve(M, v):&nbsp; &nbsp;&nbsp;"""在 GF(2) 上求解 M·x = v 并返回 M⁻¹ 的各列。"""&nbsp; &nbsp; n =&nbsp;len(M)&nbsp; &nbsp; aug = [(M[i],&nbsp;1&nbsp;<< i, v[i])&nbsp;for&nbsp;i&nbsp;in&nbsp;range(n)]
&nbsp; &nbsp;&nbsp;for&nbsp;col&nbsp;in&nbsp;range(n):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 找主元&nbsp; &nbsp; &nbsp; &nbsp; pivot =&nbsp;next((r&nbsp;for&nbsp;r&nbsp;in&nbsp;range(col, n)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if&nbsp;(aug[r][0] >> col) &&nbsp;1), -1)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;pivot <&nbsp;0:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;None,&nbsp;None&nbsp;&nbsp;# 奇异矩阵&nbsp; &nbsp; &nbsp; &nbsp; aug[col], aug[pivot] = aug[pivot], aug[col]&nbsp; &nbsp; &nbsp; &nbsp; pm, pi, pv = aug[col]&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 消去其他行&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;r&nbsp;in&nbsp;range(n):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;r != col&nbsp;and&nbsp;((aug[r][0] >> col) &&nbsp;1):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; aug[r] = (aug[r][0] ^ pm, aug[r][1] ^ pi, aug[r][2] ^ pv)
&nbsp; &nbsp;&nbsp;# 提取解和逆矩阵的各列&nbsp; &nbsp; x =&nbsp;0&nbsp; &nbsp; inv_cols = [0] * n&nbsp; &nbsp;&nbsp;for&nbsp;r&nbsp;in&nbsp;range(n):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;aug[r][2]:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; x |= (1&nbsp;<< r)&nbsp; &nbsp; &nbsp; &nbsp; inv_row = aug[r][1]&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;j&nbsp;in&nbsp;range(n):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(inv_row >> j) &&nbsp;1:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; inv_cols[j] |= (1&nbsp;<< r)&nbsp; &nbsp;&nbsp;return&nbsp;x, inv_cols

第四步:性能优化

验证集预计算

对于大量候选 key 的筛选,直接对每个候选计算 128 行的 parity 效率太低。可以预计算:

# 对候选 key_c = key0 ⊕ delta_c:# &nbsp; errors(c) = sum_i parity(verif_mask[i] & key_c) XOR verif_bit[i]# &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = sum_i [parity(verif_mask[i] & key0) XOR parity(verif_mask[i] & delta_c)] XOR verif_bit[i]## 预计算 base_errors[i] 和 parity(verif_mask[i] & inv_col[j]),# 则每个候选的筛选只需 O(128) 的 XOR 操作。

纯 Python 的性能瓶颈

在纯 Python 实现中,每个基的高斯消元约需 3ms,即每秒处理约 300 个基。要找到 h ≤ 2 的基(期望约 120 万次尝试)需要约 1 小时。一个实用化的求解器应使用 numpy 或 C 扩展来加速。

第五步:求解结果

恢复出的 128 位 key(小端序字节):

93&nbsp;35&nbsp;7a&nbsp;03&nbsp;26&nbsp;c0&nbsp;95&nbsp;9b&nbsp;74&nbsp;32&nbsp;6f c8&nbsp;74&nbsp;54&nbsp;cc b6

Sanity check 通过:

sha256(b"ledger-fog-check:"&nbsp;+ key_bytes).hexdigest()[:4] =&nbsp;"8f42"&nbsp; ✓

全量验证的错误数:2982 / 20480 = 14.56%,与预期噪声率一致。

Flag

flag{e0237ecf9df86738a2b50ad66174efed}

λd

题目信息

•分值:500 pts•类型:Crypto•考点:RSA 变种 + LLL 小根求解

题目分析

题目是一个 RSA 变种,关键代码:

N&nbsp;= p * qM&nbsp;= (p **&nbsp;2&nbsp;-&nbsp;1) * (q **&nbsp;2&nbsp;-&nbsp;1)e&nbsp;= inverse(d, M)c&nbsp;= pow(m, e, N)

注意这里的模数 不是 标准的 φ(N) = (p-1)(q-1),而是 (p²-1)(q²-1)

此外参数有限制:

pq 是 1024 bit 素数,两者之差 |p-q| < 2⁸¹⁹•私钥 d 只有约 1331 bit,相对于模数 M(约 4096 bit)很小

由于 d 很小,考虑连分数/LLL 攻击路线。

数学推导

第一步:改写 M

展开:

M = (p² - 1)(q² - 1)&nbsp; = p²q² - p² - q² + 1&nbsp; = N² - (p² + q²) + 1

利用恒等式 (p+q)² = p² + q² + 2N 代入:

M = N² - ((p+q)² - 2N) + 1&nbsp; = (N+1)² - (p+q)²

所以 M = (N+1)² - s²,其中 s = p+q

第二步:引入小变量

由于 |p-q| 很小,s = p+q 非常接近 √(4N)。令:

s0&nbsp;= floor(√(4N)) &nbsp; &nbsp;# 已知量t&nbsp; = s - s0 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 小量,|t| < 2⁶²⁰

那么 M = (N+1)² - (s0 + t)²

第三步:构造同余方程

私钥满足 e·d ≡ 1 (mod M),即存在整数 k 使:

e·d - k·M =&nbsp;1

代入 M 的表达式,两边模 e 消去 d:

k·((N+1)² - (s0+t)²) +&nbsp;1&nbsp;≡&nbsp;0&nbsp;(mod e)

令:

B&nbsp;= (N+1)² - s0² &nbsp; &nbsp;&nbsp;# 已知量x&nbsp;= k &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 未知,x < 2¹³³²y&nbsp;= t &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 未知,|y| < 2⁶²⁰

得到二元模方程:

f(x, y) = x·(B -&nbsp;2·s0·y - y²) +&nbsp;1&nbsp;≡&nbsp;0&nbsp;(mod e)

展开即为:

f(x, y) = B·x -&nbsp;2·s0·x·y - x·y² +&nbsp;1&nbsp;≡&nbsp;0&nbsp;(mod e)

这是一个关于 (x, y) 的二元二次模方程,且两个未知数都有较小的上界。

求解方法:二⽆ Coppersmith

使用 defund/coppersmith 风格的多元小根算法:

1.在模 e 的剩余类环上构造多项式 f(x, y)2.用参数 m=2, d=3 生成 shift 多项式:

•对 k=0..mi,j=0..d-1g = xⁱ·yʲ·fᵏ·eᵐ⁻ᵏ

3.构造系数矩阵,按 bounds X=2¹³³², Y=2⁶²⁰ 缩放列      4.用 LLL 约化格基      5.从短向量恢复多项式,利用结式消元求根

恢复私钥与解密

从 LLL 得到 y = t 后:

s = s0 + t &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 恢复 p+qδ = √(s² - 4N) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 恢复 |p-q|p = (s + δ) / 2q = (s - δ) / 2

验证 p·q = N,然后按题目方式恢复私钥:

M&nbsp;= (p² -&nbsp;1)(q² -&nbsp;1)d&nbsp;= inverse(e, M)m&nbsp;= pow(c, d, N)

核心代码

from&nbsp;fpylll import IntegerMatrix, LLLfrom&nbsp;math import isqrtfrom&nbsp;Crypto.Util.number import long_to_bytesimport&nbsp;sympy as spimport&nbsp;itertools
N&nbsp;= ...e&nbsp;= ...c&nbsp;= ...
s0&nbsp;= isqrt(4&nbsp;* N)B_val&nbsp;= (N +&nbsp;1) **&nbsp;2&nbsp;- s0 **&nbsp;2X&nbsp;=&nbsp;1&nbsp;<<&nbsp;1332Y&nbsp;=&nbsp;1&nbsp;<<&nbsp;620
x, y = sp.symbols('x y')f_expr&nbsp;= B_val * x -&nbsp;2&nbsp;* s0 * x * y - x * y **&nbsp;2&nbsp;+&nbsp;1
# 构造 shift 多项式shifts&nbsp;=&nbsp;[]for&nbsp;k in range(3): &nbsp;# m =&nbsp;2&nbsp; &nbsp;&nbsp;base&nbsp;= (e ** (2&nbsp;- k)) * (f_expr ** k)&nbsp; &nbsp;&nbsp;for&nbsp;i, j in itertools.product(range(3), repeat=2): &nbsp;# d =&nbsp;3&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;shifts.append(sp.expand(base * x ** i * y ** j))
# 构建系数矩阵 + 列缩放mons&nbsp;= sorted(monomial_set, key=...)factors&nbsp;=&nbsp;[X ** mi * Y ** mj for (mi, mj) in mons]rows&nbsp;=&nbsp;[]for&nbsp;g in shifts:&nbsp; &nbsp;&nbsp;row&nbsp;=&nbsp;[coeff * factors[c] for c, mon in enumerate(mons)]&nbsp; &nbsp;&nbsp;rows.append(row)
# 补齐为方阵while&nbsp;len(rows) < len(mons):&nbsp; &nbsp;&nbsp;rows.append([0] * len(mons))
# LLL 约化M&nbsp;= IntegerMatrix.from_matrix(rows)L&nbsp;= LLL.reduction(M)
# 恢复短向量为多项式# 利用结式消去 x,得到 y 的单变量方程# 求根得到 t,进而恢复 p,q,d 并解密

Flag

flag{Lattice_LLL_Defeats_Large_Delta_RSA_Variants!}

Split Personality: Gauge

题目结论

最终 flag:

flag{d73ca785bb4082865e722cff9cdfcca0}

这题的关键不是直接把 30 个 facet 一把梭拟合成一个 4 x 4 矩阵,而是:

1.先把复合模数 m 按素因子拆开。2.在每个素因子模下,用配对把点转成坐标。3.发现 facet 实际上混了多个局部 action,要先分簇。4.再用 alternating pairing multiplier μ 把各个素因子上的正确分支对齐。5.最后对矩阵做 CRT 拼接,并按题目给的 KDF 出 flag。

题目给的信息

题面给了:

•有限域 Fp2 = Fp[i] / (i^2 + 1)•4 条椭圆曲线 E0, E1, E2, E3•每条曲线上 2 个公开 anchor•30 个 facet,每个 facet 都是:

•source 上一对点,位于 E0 x E3•target 上一对点,位于 E1 x E2

目标是恢复全局 action:

target_vector&nbsp;= action_matrix * source_vector &nbsp; over Z/mZ

其中坐标顺序固定为:

source&nbsp;= (E0.anchor0, E0.anchor1, E3.anchor0, E3.anchor1)target&nbsp;= (E1.anchor0, E1.anchor1, E2.anchor0, E2.anchor1)

第一步:分解 m

题目里的

m&nbsp;=&nbsp;66614477777873634335261379299463884620134966921892327060987261

分解后是 8 个互异素数:

261047391440964340868029882291115984762913212929929085139213541963

也就是:

Z/mZ &nbsp;≅ &nbsp;∏ Z/li Z

所以题目里的所有线性关系、离散对数、矩阵恢复,都可以拆到每个素因子 li 上分别做,再用 CRT 合并回来。

这一步是全题最重要的入口,因为后面所有“混合 action”的现象,都是在各个素因子模下先看清楚,最后再拼回去。

第二步:用 Weil/Tate pairing 把点转成 anchor 基

每条曲线给了两个 anchor,记作 A0, A1。它们张成同一个 m-torsion。

对曲线上任意 m-torsion 点 P,如果

P&nbsp;= a*A0 + b*A1

那么可以用配对把 (a, b) 求出来。

zeta&nbsp;= e_m(A0, A1)

则有:

e_m(P, A1) = zeta^ae_m(A0, P) = zeta^b

于是离散对数就能恢复 a, b

因为 m 是合数,所以离散对数不是直接在模 m 上硬做,而是:

1.对每个素因子 li 分开做。2.在 li 上求离散对数。3.再用 CRT 合成 a, b mod m

这样,每个 facet 都能转成:

•一个 source 向量 s ∈ (Z/mZ)^4•一个 target 向量 t ∈ (Z/mZ)^4

满足某个局部 action:

t&nbsp;= M * s

第三步:为什么不能直接拿 30 个 facet 拟合同一个矩阵

题面已经提示了:

Several locally consistent residue-channel actions have been spliced together

意思是 30 个 facet 不是全都来自同一个 action,而是混进了多个“局部一致”的 action。

所以如果直接把全部 facet 当作

t&nbsp;= M * s

去解一个统一矩阵,通常会得到假解,或者只能拟合极少数 facet。

正确做法是:

对每个素因子 *li *单独看•在 F_li 上把 30 个 facet 分成若干个局部 action 簇

你已经跑出来,这里实际会分成 3 个局部 action 簇

也就是说:每个 li 上,不是唯一一个候选矩阵,而是 3 个候选分支。

第四步:在每个素因子上恢复局部 action

在固定一个素因子 li 后:

1.把所有 facet 的 source/target 坐标都降到 mod li2.每个 facet 给出 4 条线性方程3.在 F_li 上恢复候选 4 x 4 矩阵

由于混有 3 个 action,所以这里不是单次解线性方程就结束,而是要做“分簇/分支恢复”:

•哪些 facet 彼此线性一致,就归到同一个簇•每个簇对应一个局部 action M_li^(k)

做到这一步以后,每个 li 上会拿到 3 个候选局部 action。

第五步:用 alternating pairing multiplier μ 对齐各个素因子的正确分支

这一步是本题真正的“校准”。

题目里说:

weight is the unique positive CRT lift below 2^20 of the alternating-pairing multiplier

对每个局部 action M,它都应该满足一个交替配对的相似关系:

M^T * J * M = μ * J

这里 J 是对应 source/target 基的交替型矩阵。

重点不是公式本身,而是:

•同一个真正的全局 action•在每个素因子 li 上降模以后•它的 multiplier μ mod li 必须是同一个整数 μ 的局部投影

所以流程变成:

1.在每个 li 上,对 3 个候选局部 action 分别算 μ2.把这些候选分支横向对齐3.看哪一条分支能在 所有 8 个素因子上 同时成立并 CRT 拼起来

最后唯一能全局对齐的分支,对应:

μ = 65537

这一步也顺手解释了 weight:

因为题目要求的是“小于 2^20 的唯一正 CRT lift”,而这里

65537&nbsp;<&nbsp;2^20

所以:

weight&nbsp;=&nbsp;65537

第六步:对矩阵 entry 做 CRT,恢复全局 action_matrix

当每个素因子上的正确分支都选出来之后,就得到了:

M mod&nbsp;li

对矩阵的 16 个 entry 分别做 CRT,就能恢复唯一的

M&nbsp;mod m

也就是题目要求的全局 action_matrix

这里要注意题面规定的编码方式:

•矩阵按 row-major•entry 取 [0, m) 中的最小非负代表元

第七步:按题目 KDF 出 flag

题目给了明确 KDF:

flag&nbsp;=&nbsp;"flag{"&nbsp;+ sha256("split-mirage-gauge-v1|"&nbsp;+&nbsp;";".join(map(str, [weight] + row_major(action_matrix)))).hexdigest()[:32] +&nbsp;"}"

把:

weight = 65537•恢复出的 action_matrix 按 row-major 展开

拼进去做 SHA-256,取前 32 个 hex,就得到:

flag{d73ca785bb4082865e722cff9cdfcca0}

Double²

这个题到公众号格式有点问题,可看语雀

https://www.yuque.com/g/maple-uuprx/zpp8n3/ae36wki27i9vogtv/collaborator/join?token=FapiGiWLjowGt0yD&source=doc_collaborator# 《黄河流域wp》


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:赛查查 《第四届黄河流域wp》

    评论:0   参与:  0