使用指南
动态 flag

动态 flag

GZCTF 自带对于动态 flag 分发的支持,将会在容器启用时采用 GZCTF_FLAG 环境变量进行注入。

采用此环境变量的主要原因是出于防止 GZCTF 被商业滥用的考虑,因此短时间内不会开放此功能的自定义。

配置规则

在动态题目的 flag 及附件管理页面中,flag 模板将作为生成动态 flag 的依据,具有如下的规则:

  1. 留空以生成随机 GUID 作为 flag
  2. 指定 [GUID] 则会 替换此处的占位符为随机 GUID
  3. 若指定 [TEAM_HASH] 则它将会被替换为队伍 Token 与相关信息所生成的哈希值
  4. 若未指定 [TEAM_HASH] 则将启用 Leet 字符串功能,将会基于模版对花括号内字符串进行变换,需要确保 flag 模版字符串的熵足够高
  5. 若需要在指定 [TEAM_HASH] 的情况下启用 Leet 字符串功能,请在 flag 模版字符串之前添加 [LEET] 标记,此时不会检查 flag 模版字符串的熵
  6. 若需要在 Leet 字符串时启用特殊字符(可能会导致注入问题),请在 flag 模版字符串之前添加 [CLEET] 标记

规则示例

  1. 留空会得到 flag{1bab71b8-117f-4dea-a047-340b72101d7b}
  2. MyCTF{[GUID]} 会得到 MyCTF{1bab71b8-117f-4dea-a047-340b72101d7b}
  3. flag{hello world} 会得到 flag{He1lo_w0r1d}
  4. [CLEET]flag{hello sara} 会得到 flag{He1!o_$@rA}
  5. flag{hello_world_[TEAM_HASH]} 会得到 flag{hello_world_5418ce4d815c}
  6. [LEET]flag{hello world [TEAM_HASH]} 会得到 flag{He1lo_w0r1d_5418ce4d815c}

Leet 字符串

Leet 字符串是一种将字符串中的字符替换为数字或符号的方法,例如将 a 替换为 4,将 e 替换为 3 等,GZCTF 采用的 Leet 字符串规则如下:

字符替换为字符替换为字符替换为字符替换为
AAa4BBb68CCcDDd
EEe3FFf1GGg69HHh
IIi1lJJjKKkLLl1I
MMmNNnOOo0PPp
QQq9RRrSSs5TTt7
UUuVVvWWwXXx
YYyZZz200oO11lI
22zZ33eE44aA55Ss
66Gb77T88bB99g

启用复杂 Leet 字符串时,请注意字符注入问题,它采用的规则如下,由于可能性更多,达到指定的熵所需的长度会更短:

| 字符 | 替换为 | 字符 | 替换为 | 字符 | 替换为 | 字符 | 替换为 | | :--: | :------ | :--: | :----- | :--: | :----- | :--: | :------ | --- | | A | Aa4@ | B | Bb68 | C | Cc( | D | Dd | | E | Ee3 | F | Ff1 | G | Gg69 | H | Hh | | I | Ii1l! | J | Jj | K | Kk | L | Ll1I! | | M | Mm | N | Nn | O | Oo0# | P | Pp | | Q | Qq9 | R | Rr | S | Ss5$ | T | Tt7 | | U | Uu | V | Vv | W | Ww | X | Xx | | Y | Yy | Z | Zz2? | 0 | 0oO# | 1 | 1lI | | | 2 | 2zZ? | 3 | 3eE | 4 | 4aA | 5 | 5Ss | | 6 | 6Gb | 7 | 7T | 8 | 8B& | 9 | 9g |

安全性

Leet 字符串的安全性取决于 flag 模版字符串的熵,对于 flag 模版中每一个字符,它都有可能被替换为多个字符。我们采用每一个可变字符的可变字符集合的长度对 2 取对数后累加,从而得到了 Leet 字符串的熵:

H=i=1nlog2mimi={len(LeetMap[ci])if ci is in LeetMap0otherwise\begin{aligned} H &= \sum_{i=1}^{n} \log_2{m_i} \\ m_i &= \begin{cases} \text{len}(\text{LeetMap}[c_i]) & \text{if } c_i \text{ is in LeetMap} \\ 0 & \text{otherwise} \end{cases} \end{aligned}

在 GZCTF 中,这一指标被限制不得低于 32,否则将会导致 flag 的安全性降低。

队伍哈希

队伍哈希是一种将队伍 Token 与相关信息进行哈希的方法,它将会被用于动态 flag 的生成,以保证每一个队伍都有唯一的 flag。

在 GZCTF 中,队伍哈希为 SHA256 哈希的中部 12 位,例如 5418ce4d815c,它将会被用于替换 flag 模版中的 [TEAM_HASH] 占位符。

队伍哈希的计算采用了三个参数:

  • 队伍 Token:在队伍注册时由系统生成、签发的、可被公钥验证的 ed25519 签名
  • 题目 ID:题目的唯一标识符
  • 比赛哈希盐:加密后的比赛签名私钥加盐之后的 SHA256 哈希

生成 Team Hash 的类 python 代码如下:

from hashlib import sha256
 
str_sha256 = lambda s: sha256(s.encode()).hexdigest()
 
encrypted_game_pk = "...some base64..."
chal_id = 114
team_token = "114:...some base64..."
 
# you can get this salt from /api/edit/games/{id}/teamhashsalt
game_salt = str_sha256(f"GZCTF@{encrypted_game_pk}@PK")
 
# you should calculate this hash by yourself, and put it in challenge
chal_salt = str_sha256(f"{game_salt}::{chal_id}")
 
# let your challenge to calculate team hash
team_hash = str_sha256(f"{chal_salt}::{team_token}")[12:24]

其中,比赛哈希盐 game_salt 可以通过管理员权限访问 /api/edit/games/{id}/hashsalt 接口获取,如需使用请注意保密。

安全性

  • 队伍 Token 由 GZCTF 签发,它是一个 ed25519 签名,可以被公钥验证,因此不会被伪造
  • 比赛哈希盐是一个比赛特异的值,源自比赛签名私钥的哈希,需要管理员保证其安全性
  • 题目 ID 是一个整数,在创建题目时由系统生成,由管理员结合比赛哈希盐来计算题目哈希盐
  • 题目哈希盐是一个题目特异的值,应当被用于最终的 Team Hash 计算,其泄漏不会影响其他题目的安全性

正确使用

队伍哈希的一个核心的使用场景是外部题目(队伍所访问的最终容器并非 GZCTF 所启动的容器),例如某些 Web 题目的部署难度高、依赖复杂的情况下,题目可能只有一个外部实例,而不是每一个队伍都有一个独立的实例。

在这种情况下,我们可以通过校验队伍 Token 并根据队伍 Token 来独立生成 flag,从而保证每一个队伍都有唯一的动态 flag。

队伍签名校验

比赛公钥可以直接从比赛管理页面获取,它是一个被 Base64 编码的 ed25519 公钥,例如:

s2r5WQUClYNsldJrRKanrKivBUtyN+3MjeOiKNL3znI=

队伍 Token 是一个被 Base64 编码的 ed25519 签名,它的格式为:

1201:HCdjp352NcQoL/4gS8RP3xRt5B9xX2V4m2UeoqfM2dxcLrI5FiYQ7HC9pqreG+tudWjYJf0atzQhhAKyYDKsCg==

可以使用以下代码来校验队伍 Token,其中 base64nacl 为 python 库:

from base64 import b64decode
from nacl.signing import VerifyKey
 
token = "1201:HCdjp352NcQoL/4gS8RP3xRt5B9xX2V4m2UeoqfM2dxcLrI5FiYQ7HC9pqreG+tudWjYJf0atzQhhAKyYDKsCg=="
verify_key = VerifyKey(b64decode("s2r5WQUClYNsldJrRKanrKivBUtyN+3MjeOiKNL3znI="))
 
data = f"GZCTF_TEAM_{token.split(':')[0]}".encode()
 
try:
    verify_key.verify(data, b64decode(token.split(':')[1]))
except:
    print("Invalid token")

PyNaCl 是 libsodium 的 python 封装,在常见的系统中大概率已经预装了 libsodium,详情参考: PyNaCl (opens in a new tab)

你也可以使用任何其他语言的 ed25519 签名校验库来校验队伍 Token 是否为平台所签发的有效签名,并为下发 flag 的安全性做密码学保证。