第四届数信杯数据安全之svg隐写"

admin 2026-11-01 02:17:08 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

第四届数信杯数据安全之svg隐写

原创

弈秋
弈秋

一只岸上的鱼

2026年10月28日 20:09
江苏

去阅读

第四届数信杯数据安全之svg隐写

缘起

对于ctf,我是真的热爱,屡败屡战。

这次数信杯,成绩也不理想,但是还是有很多值得学习和总结的地方。

唯一意外的是,这次官方居然不禁止,甚至有点推荐使用AI,这让我很是惊讶。在没有agent的加持下,那就是拼模型了,而好的模型都是闭源的,都是收费的,有点变成了烧钱比赛,实际也是如此,grok好像一骑绝尘!

svg隐写

说到图片隐写,png和jpg绝对是主角,还从来没有见过svg隐写的题目,这让我很是好奇。刚好这次遇到了,就来研究一下。

题目

海报中没有隐藏文本,渲染器所需的一切都已存在。请在不执行脚本、不依赖截图或 OCR 的前提下,从 SVG 的几何关系中恢复 Flag

题目附件,我放在了cnb中:

数据信杯题目附件

https://cnb.cool/netvvorm/items/datasecurity26

基础知识

想要理解并解这道题,还是必须要先了解一些svg的基础知识。

1. SVG 文件格式与特点

SVG(Scalable Vector Graphics,可缩放矢量图形) 是一种基于 XML 的二维矢量图形格式,特点包括:

  • ‎基于文本:本质是纯 XML 文本,可以直接用文本方式解析,适合藏数据。
  • ‎矢量描述:用几何图元(‎<rect>、‎<circle>、‎<path> 等)描述图形,而非像素点阵。
  • ‎坐标系:‎viewBox 定义逻辑坐标空间;元素坐标可以是负数、小数,允许超出画布范围。
  • ‎‎<defs> 定义 + ‎<use> 引用:可复用的图元(‎<symbol>、‎<clipPath>)定义在 ‎<defs> 中,通过 ‎<use href=\"#id\"> 实例化,实现\"一处定义、多处摆放\"。
  • ‎变换(transform):‎translate(x y) 平移、‎matrix(a b c d e f) 线性变换(旋转、翻转、缩放、切变)。
  • ‎裁剪(clip-path):‎<clipPath> + ‎clip-path=\"url(#id)\" 将内容裁剪到指定区域,区域外的图形不显示。

2. 本题 SVG 的数据结构特点

本题 poster.svg 的结构非常典型,是\"大量微小图元 + 掩码 + 变换\"的隐写载体:

<svg width=\"1024\" height=\"768\" viewBox=\"0 0 1024 768\">
  <defs>
    <symbol id=\"asset_000\">        <!-- 232 个 symbol:asset_000..asset_231 -->
      <rect x=\"16\" y=\"-43\" width=\"5\" height=\"5\"/>   <!-- 每个 symbol 含 ~8 个 5×5 小方块 -->
      <rect x=\"21\" y=\"577\" width=\"5\" height=\"5\"/>
      ...
    </symbol>
    <clipPath id=\"mask_000\">       <!-- 232 个对应的裁剪掩码 -->
      <rect x=\"15\" y=\"-104\" width=\"12\" height=\"732\"/>
    </clipPath>
    ...(共 232 组 asset / mask)
  </defs>

  <g id=\"poster\">
    ...(124 个装饰性大 rect,构成海报背景与中央横带)
  </g>

  <g id=\"frame_83\">                 <!-- 232 个摆放单元 -->
    <g transform=\"translate(767 456)\">
      <g transform=\"matrix(0 -1 -1 0 0 0)\">
        <use href=\"#asset_000\" fill=\"#1b344d\" clip-path=\"url(#mask_000)\"/>
      </g>
    </g>
    ...(共 232 个,每个对应一个 symbol)
  </g>
</svg>

关键数据特点:

元素 数量 作用
‎<symbol> asset\_000..231 232 每个含约 8 个 5×5 的 ‎<rect>(230 个含 8 个,1 个含 7 个,1 个含 5 个)
‎<clipPath> mask\_000..231 232 与 symbol 一一对应,起裁剪作用
‎<g id=\"frame_83\"> 232 组 每组 = 外层 ‎translate(x y) + 内层 ‎matrix(...) + 一个 ‎<use>
‎<use> 的 fill 4 种 ‎#1b344d、‎#16263a、‎#24496b、‎#2d5d87

整张海报没有任何 <text> 元素(符合题干\"没有隐藏文本\"),全部信息都藏在几何数据里。

3. 隐写用到的特性

本题用到了三种关键特性,缺一不可:

  1. ‎掩码裁剪(clip-path)选择信息:每个 symbol 的 8 个 5×5 小方块中,只有落在 ‎mask 矩形内部的小方块才会被渲染出来(本题每个 symbol 恰好有 6 个通过掩码,2 个被裁掉,存活位型恒为 ‎11111100)。经过 ‎matrix 翻转和 ‎translate 平移后,所有存活的小方块最终都落在海报中央的一条水平横带(y 约 405–440)内。
  2. ‎重叠覆盖与奇偶(XOR)隐藏:这是真正的隐藏机制。不同 symbol 的小方块经平移/翻转后会重叠落在同一个 5px 网格单元上。每个单元被覆盖的次数是 1 或 2。取覆盖次数的奇偶性(即 mod 2,等价于 XOR):奇数被覆盖的单元点亮,偶数被覆盖的单元熄灭——这样原本渲染成\"噪声\"的点阵,就显露出一段可读的文本。这正是\"XOR shapes reveal flag\"的含义。
  3. ‎5×7 点阵字体:显露出来的文本是用 5×7 点阵字(每字符宽 5 点、高 7 点,字符间空 1 列)排布的。5×7 是经典的 LED/点阵显示字库规格。

二、解题思路

第 1 步:整体观察与结构分析

先用文本方式读取 SVG(不能靠截图/OCR),统计元素:

  • 232 个 ‎<symbol>、232 个 ‎<use>、232 个 ‎<clipPath>、2208 个 ‎<rect>、0 个 ‎<text>。
  • 没有文本元素 → 题干\"没有隐藏文本\"得到印证,flag 必然藏在几何数据里。

第 2 步:还原每个小方块的最终位置(几何关系)

对每个摆放单元做如下几何计算:

symbol 局部坐标 (px, py)  —— 先经 mask 裁剪(保留在掩码内的) ——>
matrix 线性变换:  x' = a·px + c·py;  y' = b·px + d·py  ——>
translate 平移:  ax = tx + x';  ay = ty + y'

统计发现:所有存活小方块都落在 y∈[405,440]、x∈[90,930] 的横带内(约 168 列 × 7 行,每格 5px)。用肉眼/直接渲染看这条带,它只是随机的噪声点,并不是文字。

第 3 步:发现奇偶覆盖(XOR)通道

既然直接看到的点阵是噪声,说明信息被\"隐藏\"了。注意到不同 symbol 的小方块会重叠在同一个 5px 网格单元上。于是统计每个单元被覆盖的次数:

  • 覆盖次数直方图:‎{2: 494, 1: 400} —— 恰好存在大量 2 次覆盖,说明重叠是刻意的。
  • ‎取覆盖次数 mod 2(奇偶 / XOR):奇数点亮、偶数熄灭。

得到一张 168×7 的网格图,肉眼可见是一段清晰的 5×7 点阵文字!

##### #      ###   ####   ### #   #  ###  ####         #### #   #  ###  ####  #####  #### ...
#     #     #   # #       #    # #  #   # #   #       #     #   # #   # #   # #     #     ...
####  #     #   # # ###   #     #   #   # #   #       #     ##### #   # #   # ####  #     ...
...

显示因为文档的远古不是很清楚,看一下console打印的图片:

第 4 步:分割字符并识别

将 168 列按\"5 列一个字符 + 1 列空格\"分割成 28 个 5×7 字形,对照标准 5×7 点阵字库逐个识别:

F L A G { X O R _ S H A P E S _ R E V E A L _ F L A G }

第 5 步:得到 Flag

FLAG{XOR_SHAPES_REVEAL_FLAG}

该 flag 本身也印证了解法:对点阵形状(shapes)做 XOR,即可显现(reveal)flag。


三、用到的脚本

以下两个脚本即可完整复现解题过程。均只做文本解析与几何计算,不渲染、不截图、不 OCR,完全符合题干要求。

脚本 1:解析 SVG,计算覆盖度奇偶网格

# -*- coding: utf-8 -*-
import re
from collections import Counter

svg = open(\"poster.svg\", encoding=\"utf-8\").read()

# ---------- 1. 解析所有 symbol 及其 5×5 rect ----------
sym_re  = re.compile(r'<symbol id=\"(asset_\\d+)\">(.*?)</symbol>', re.S)
rect_re = re.compile(r'<rect x=\"([-\\d.]+)\" y=\"([-\\d.]+)\" width=\"([\\d.]+)\" height=\"([\\d.]+)\"/>')
sym = {}
for m in sym_re.finditer(svg):
    sym[m.group(1)] = [(float(a), float(b), float(c), float(d))
                       for a, b, c, d in rect_re.findall(m.group(2))]

# ---------- 2. 解析所有 clipPath 掩码 ----------
cp_re = re.compile(r'<clipPath id=\"(mask_\\d+)\"[^>]*>(.*?)</clipPath>', re.S)
mask = {}
for m in cp_re.finditer(svg):
    r = rect_re.search(m.group(2))
    mask[m.group(1)] = tuple(float(v) for v in r.groups())

# ---------- 3. 解析 frame_83 的每个摆放单元(translate + matrix + use) ----------
frame_re = re.compile(
    r'<g transform=\"translate\\(([-\\d.]+) ([-\\d.]+)\\)\">'
    r'<g transform=\"matrix\\(([-\\d.]+) ([-\\d.]+) ([-\\d.]+) ([-\\d.]+) 0 0\\)\">'
    r'<use href=\"#(asset_\\d+)\" fill=\"([^\"]+)\"')
frames = [(float(a), float(b), float(c), float(d), float(e), float(f), g, h)
          for a, b, c, d, e, f, g, h in frame_re.findall(svg)]

# ---------- 4. 找出每个 symbol 中能通过掩码的小方块(局部坐标) ----------
def visible_local(sid):
    mx, my, mw, mh = mask[sid.replace('asset', 'mask')]
    out = []
    for (x, y, w, h) in sym[sid]:
        # 判断 5×5 方块与掩码矩形是否相交
        if (x + w > mx) and (x < mx + mw) and (y + h > my) and (y < my + mh):
            out.append((x + w / 2.0, y + h / 2.0))   # 方块中心(局部坐标)
    return out

# ---------- 5. 把每个可见方块经 matrix+translate 放到最终位置,并统计 5px 网格覆盖次数 ----------
cover = Counter()
for (tx, ty, a, b, c, d, sid, fill) in frames:
    for (px, py) in visible_local(sid):
        ax = tx + a * px + c * py      # matrix: x' = a·px + c·py
        ay = ty + b * px + d * py      #         y' = b·px + d·py
        cell = (int((ax - 2.5) // 5), int((ay - 2.5) // 5))  # 5px 对齐到网格单元
        cover[cell] += 1

# ---------- 6. 输出覆盖次数奇偶网格(mod 2 = XOR):奇数点亮、偶数熄灭 ----------
xs = [c[0] for c in cover]; ys = [c[1] for c in cover]
xmin, xmax = min(xs), max(xs)
ymin, ymax = min(ys), max(ys)
print(f\"grid: {xmax-xmin+1} x {ymax-ymin+1}\")
for y in range(ymin, ymax + 1):
    print(''.join('#' if cover.get((x, y), 0) % 2 == 1 else ' '
                   for x in range(xmin, xmax + 1)))

输出(168×7 奇偶网格,已可肉眼读出 5×7 文字):

##### #      ###   ####   ### #   #  ###  ####         #### #   #  ###  ####  #####  ####       ####  ##### #   # #####  ###  #           ##### #      ###   #### ###
#     #     #   # #       #    # #  #   # #   #       #     #   # #   # #   # #     #           #   # #     #   # #     #   # #           #     #     #   # #       #
####  #     #   # # ###   #     #   #   # #   #       #     ##### #   # #   # ####  #           #   # ####  #   # ####  #   # #           ####  #     #   # # ###   #
#     #     ##### #   #  ##     #   #   # ####         ###  #   # ##### ####  #      ###        ####  #     #   # #     ##### #           #     #     ##### #   #   ##
#     #     #   # #   #   #     #   #   # # #             # #   # #   # #     #         #       # #   #     #   # #     #   # #           #     #     #   # #   #   #
#     #     #   # #   #   #    # #  #   # #  #            # #   # #   # #     #         #       #  #  #      # #  #     #   # #           #     #     #   # #   #   #
#     ##### #   #  ###    ### #   #  ###  #   # ##### ####  #   # #   # #     ##### ####  ##### #   # #####   #   ##### #   # ##### ##### #     ##### #   #  ###  ###

脚本 2:将奇偶网格分割为 5×7 字形并打印(便于识别)

# -*- coding: utf-8 -*-
# 输入:脚本 1 得到的 168×7 奇偶网格 grid[y][x],0/1
# 输出:按“5 列一个字符 + 1 列空格”切分的字形

def segment(grid, W, H):
    glyphs = []
    x = 0
    while x < W:
        # 跳过全空列(字符间隔)
        if all(grid[y][x] == 0 for y in range(H)):
            x += 1
            continue
        # 取宽 5 的一列字符
        g = [[grid[y][x + k] for k in range(5)] for y in range(H)]
        glyphs.append(g)
        x += 5
        # 跳过字符后的间隔列
        if x < W and all(grid[y][x] == 0 for y in range(H)):
            x += 1
    return glyphs

# glyphs = segment(grid, W, H)
# for i, g in enumerate(glyphs):
#     print(f\"--- glyph {i} ---\")
#     for row in g:
#         print(''.join('#' if b else '.' for b in row))

对照标准 5×7 点阵字库,逐个字形识别得到:

序号 字形图案(#=亮) 字符
0 ‎#####/#..../####./#..../#..../#..../#.... F
1 ‎#..../.../#####(左竖+底横) L
2 ‎.###./#...#/#...#/#####/#...#/#...#/#...# A
3 ‎.####/#..../#.###/#...#/#...#/#...#/.###. G
4 ‎.###./.#.../.#.../##.../.#.../.#.../.###. {
5 ‎#...#/.#.#./..#../..#../..#../.#.#./#...# X
6 ‎.###./#...#/#...#/#...#/#...#/#...#/.###. O
7 ‎####./#...#/#...#/####./#.#../#..#./#...# R
8 ‎...../...../...../...../...../...../##### \_
9 ‎.####/#..../#..../.###./....#/....#/####. S
10 ‎#...#/#...#/#####/#...#/#...#/#...#/#...# H
11 ‎.###./#...#/#...#/#####/#...#/#...#/#...# A
12 ‎####./#...#/#...#/####./#..../#..../#.... P
13 ‎#####/#..../####./#..../#..../#..../##### E
14 同 9 S
15 同 8 \_
16 同 7 R
17 同 13 E
18 ‎#...#/#...#/#...#/#...#/#...#/.#.#./..#.. V
19 同 13 E
20 同 11 A
21 同 1 L
22 同 8 \_
23 同 0 F
24 同 1 L
25 同 2 A
26 同 3 G
27 ‎###../..#../..#../..##./..#../..#../###.. }

拼接后即为:FLAG{XOR_SHAPES_REVEAL_FLAG}


四、总结

环节 方法 说明
数据载体 SVG ‎<symbol> + ‎<use> 232 个可复用图元
信息筛选 ‎<clipPath> 掩码 每个 symbol 只有 6/8 个小方块通过
位置变换 ‎translate + ‎matrix 把小方块散布到横带内,视觉上呈噪声
‎隐藏机制 ‎重叠覆盖的奇偶(XOR) 每个 5px 单元覆盖次数 mod 2
显形方式 5×7 点阵字 奇数覆盖点亮,拼出文本
最终结果 ‎FLAG{XOR_SHAPES_REVEAL_FLAG} 题目自指:XOR 形状可揭示 flag

本题的巧妙之处在于:单个 symbol 的图案是噪声,直接渲染也是噪声;只有当多个 symbol 的方块重叠后取覆盖奇偶性,隐藏的文本才浮现。 全程通过纯几何计算即可还原,完全符合题干\"不执行脚本、不依赖截图或 OCR\"的约束。

    第四届数信杯数据安全之svg隐写" 网络安全文章

    第四届数信杯数据安全之svg隐写"

    本文解析第四届数信杯数据安全赛题中SVG隐写技术。作者通过分析SVG几何数据而非渲染截图,利用掩码裁剪、重叠覆盖与奇偶XOR机制,从232个symbol中恢复5×7点阵文字,最终得到FlagFLAG{XOR_SHAPES_REVEAL\\_FLAG}。文章提供完整解题思路与两个Python脚本,可复现全过程,对CTF隐写研究具有参考价值。
    值得警惕!人人当黑客的时代来临了" 网络安全文章

    值得警惕!人人当黑客的时代来临了"

    文章指出AI大模型与智能体正大幅降低网络攻击门槛,使缺乏专业技能者也能借助AI完成信息收集、漏洞分析与攻击脚本编写,攻击成本与数量可能增加。企业防御需从被动采购转向持续暴露面管理、身份权限控制、AI驱动安全运营与实战化攻防验证,并提升全员安全意识,以应对AI时代的安全风险。
    通过近五年美亚杯推演2026年美亚杯" 网络安全文章

    通过近五年美亚杯推演2026年美亚杯"

    本文复盘2021至2025五届美亚杯1198道真题,提炼出五项核心规律:案情背景即出题脚本,每件检材对应8至15题且题量呈摊平趋势;个人赛侧重人物行为,团体赛聚焦资金与组织;新物件必考;时间矛盾是高频考点;老动作新载体易得分。作者据此预测2026年考点,建议考生对照官方清单识别新设备与时间矛盾,针对性强化取证技能。
    评论:0   参与:  0