916 字
5 分钟
乌托邦·王的实验室29
2026-02-28
2026-03-01

九九的艺术空间#

题目信息#

分类:Math / 图片隐写
关键词:希尔伯特曲线、像素隐写

分析过程#

打开靶场是一个”九九的艺术沙盘”网站,有四个页面:首页、沙盘展厅、源文件获取、加密频道。

image-20260228205959529

加密频道里的聊天记录疯狂暗示:

  • “纯粹的数理与分形几何”
  • “一维与二维的折叠”
  • “沙子走过的轨迹”
  • “混沌沙海里的坐标法则”

结合题目标签”希尔伯特曲线”和”图片隐写”,方向很明确了。

从”源文件获取”页面下载 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

解题步骤#

  1. 下载 JiuJiu_Artwork.png
  2. 遍历所有像素,找出蓝色和红色的坐标
  3. 对每个彩色像素计算希尔伯特曲线距离 d
  4. 按 d 从小到大排序
  5. 蓝色 = 1,红色 = 0(试了两种映射,这个方向出可读文本)
  6. 每 8 bit 一组转 ASCII

排序后的比特流解码结果直接就是 flag。

有个小细节:颜色到比特的映射方向需要试一下。blue=0/red=1 出来是乱码,反过来 blue=1/red=0 直接出 flag{...}

完整 EXP#

"""
九九的艺术空间 - Hilbert 曲线图片隐写 EXP
一键运行即可提取 flag
"""
from PIL import Image
import 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/
作者
wenject
发布于
2026-02-28
许可协议
CC BY-NC-SA 4.0