动态 flag
GZCTF 自带对于动态 flag 分发的支持,将会在容器启用时采用 GZCTF_FLAG
环境变量进行注入。
INFO
采用此环境变量的主要原因是出于防止 GZCTF 被商业滥用的考虑,因此短时间内不会开放此功能的自定义。
配置规则
在动态题目的 flag 及附件管理页面中,flag 模板将作为生成动态 flag 的依据,具有如下的规则:
- 留空以生成随机
GUID
作为 flag
- 指定
[GUID]
则会 仅 替换此处的占位符为随机 GUID
- 若指定
[TEAM_HASH]
则它将会被替换为队伍 Token 与相关信息所生成的哈希值
- 若未指定
[TEAM_HASH]
则将启用 Leet 字符串功能,将会基于模版对花括号内字符串进行变换,需要确保 flag 模版字符串的熵足够高
- 若需要在指定
[TEAM_HASH]
的情况下启用 Leet 字符串功能,请在 flag 模版字符串之前添加 [LEET]
标记,此时不会检查 flag 模版字符串的熵
- 若需要在 Leet 字符串时启用特殊字符(可能会导致注入问题),请在 flag 模版字符串之前添加
[CLEET]
标记
规则示例
- 留空会得到
flag{1bab71b8-117f-4dea-a047-340b72101d7b}
MyCTF{[GUID]}
会得到 MyCTF{1bab71b8-117f-4dea-a047-340b72101d7b}
flag{hello world}
会得到 flag{He1lo_w0r1d}
[CLEET]flag{hello sara}
会得到 flag{He1!o_$@rA}
flag{hello_world_[TEAM_HASH]}
会得到 flag{hello_world_5418ce4d815c}
[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 字符串的熵:
Hmi=i=1∑nlog2mi={len(LeetMap[ci])0if ci is in LeetMapotherwise
在 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,其中 base64
和 nacl
为 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 的安全性做密码学保证。