动态 flag

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

INFO

采用此环境变量的主要原因是出于防止 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 字符串规则如下:

+-------------+---------------+-------------+---------------+-------------+---------------+
| Characters  | Replaced with | Characters  | Replaced with | Characters  | Replaced with |
+-------------+---------------+-------------+---------------+-------------+---------------+
|     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      |      8bB      |      9      |      9g       |
+-------------+---------------+-------------+---------------+-------------+---------------+

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

+-------------+----------------+-------------+----------------+-------------+----------------+
| Characters  | Replaced with  | Characters  | Replaced with  | Characters  | Replaced with  |
+-------------+----------------+-------------+----------------+-------------+----------------+
|     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}/hashsalt
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

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