916 字
5 分钟
乌托邦·王的实验室29
九九的艺术空间
题目信息
分类:Math / 图片隐写
关键词:希尔伯特曲线、像素隐写
分析过程
打开靶场是一个”九九的艺术沙盘”网站,有四个页面:首页、沙盘展厅、源文件获取、加密频道。

加密频道里的聊天记录疯狂暗示:
- “纯粹的数理与分形几何”
- “一维与二维的折叠”
- “沙子走过的轨迹”
- “混沌沙海里的坐标法则”
结合题目标签”希尔伯特曲线”和”图片隐写”,方向很明确了。
从”源文件获取”页面下载 JiuJiu_Artwork.png,拿到手一看:
- 512×512 的 PNG
- 只有 3 种颜色:沙色 (220,200,180) 作为背景,蓝色 (0,0,255) 和红色 (255,0,0) 散落其中
- 蓝色像素 139 个,红色像素 165 个,总共 304 个彩色像素
- 304 / 8 = 38 字节,刚好是
flag{32位hex}的长度
所以思路就是:这些蓝/红像素代表 0/1 比特,但它们在图上的排列看起来是”混沌”的——因为排列顺序不是按行列扫描,而是按希尔伯特曲线的路径。
希尔伯特曲线
希尔伯特曲线是一种空间填充曲线,能把一维的线连续地映射到二维平面上,填满整个正方形。对于 512×512 的网格,这是一条 9 阶希尔伯特曲线,总共经过 512×512 = 262144 个点。
每个像素在曲线上都有一个唯一的一维索引 d。核心算法是 xy2d(n, x, y),把二维坐标转成曲线距离:
def xy2d(n, x, y): d = 0 s = n // 2 while s > 0: rx = 1 if (x & s) > 0 else 0 ry = 1 if (y & s) > 0 else 0 d += s * s * ((3 * rx) ^ ry) if ry == 0: if rx == 1: x = s - 1 - x y = s - 1 - y x, y = y, x s //= 2 return d解题步骤
- 下载
JiuJiu_Artwork.png - 遍历所有像素,找出蓝色和红色的坐标
- 对每个彩色像素计算希尔伯特曲线距离 d
- 按 d 从小到大排序
- 蓝色 = 1,红色 = 0(试了两种映射,这个方向出可读文本)
- 每 8 bit 一组转 ASCII
排序后的比特流解码结果直接就是 flag。
有个小细节:颜色到比特的映射方向需要试一下。blue=0/red=1 出来是乱码,反过来 blue=1/red=0 直接出 flag{...}。
完整 EXP
"""九九的艺术空间 - Hilbert 曲线图片隐写 EXP一键运行即可提取 flag"""from PIL import Imageimport numpy as np
# ===== 靶机配置 =====TARGET_URL = "" # 靶机地址,留空IMAGE_PATH = "JiuJiu_Artwork.png" # 本地已下载的图片
def xy2d(n, x, y): """将 (x, y) 坐标转换为 Hilbert 曲线上的一维距离 d""" d = 0 s = n // 2 while s > 0: rx = 1 if (x & s) > 0 else 0 ry = 1 if (y & s) > 0 else 0 d += s * s * ((3 * rx) ^ ry) if ry == 0: if rx == 1: x = s - 1 - x y = s - 1 - y x, y = y, x s //= 2 return d
def solve(): img = Image.open(IMAGE_PATH) pixels = np.array(img) n = img.size[0] # 512
blue = np.array([0, 0, 255]) red = np.array([255, 0, 0])
# 收集所有非沙色像素,计算其 Hilbert 曲线索引 colored = [] for y in range(n): for x in range(n): p = pixels[y, x] if np.array_equal(p, blue): colored.append((xy2d(n, x, y), 1)) # blue -> bit 1 elif np.array_equal(p, red): colored.append((xy2d(n, x, y), 0)) # red -> bit 0
# 按 Hilbert 距离排序 colored.sort(key=lambda t: t[0])
# 提取比特流,8-bit 分组解码 ASCII bits = ''.join(str(b) for _, b in colored) flag = '' for i in range(0, len(bits) - 7, 8): flag += chr(int(bits[i:i+8], 2))
print(f"[+] Extracted: {flag}") return flag
if __name__ == "__main__": solve() 乌托邦·王的实验室29
https://wenject.github.io/posts/乌托邦王的实验室29/