Skip to content

Commit

Permalink
feat: update dynamic flag
Browse files Browse the repository at this point in the history
  • Loading branch information
GZTimeWalker committed Jun 12, 2023
1 parent ba915f0 commit 9bcabf7
Show file tree
Hide file tree
Showing 10 changed files with 714 additions and 86 deletions.
10 changes: 5 additions & 5 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@
},
"dependencies": {
"@vercel/analytics": "^1.0.1",
"next": "^13.4.4",
"next": "^13.4.5",
"next-themes": "^0.2.1",
"nextra": "^2.6.2",
"nextra-theme-docs": "^2.6.2",
"nextra": "^2.7.1",
"nextra-theme-docs": "^2.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "^20.2.5",
"@types/react": "^18.2.8",
"@types/node": "^20.3.0",
"@types/react": "^18.2.12",
"typescript": "^5.1.3"
}
}
3 changes: 2 additions & 1 deletion docs/pages/guide/_meta.zh.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"challenge": "赛题配置"
"challenge": "赛题配置",
"dynamic-flag": "动态 flag"
}
137 changes: 137 additions & 0 deletions docs/pages/guide/dynamic-flag.zh.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Callout } from "nextra-theme-docs";

# 动态 flag

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

<Callout type="info">

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

</Callout>

## 配置规则

在动态题目的 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 模版字符串的熵

## 规则示例

1. 留空会得到 `flag{1bab71b8-117f-4dea-a047-340b72101d7b}`
2. `MyCTF{[GUID]}` 会得到 `MyCTF{1bab71b8-117f-4dea-a047-340b72101d7b}`
3. `flag{hello world}` 会得到 `flag{He1lo_w0r1d}`
4. `flag{hello_world_[TEAM_HASH]}` 会得到 `flag{hello_world_5418ce4d815c}`
5. `[LEET]flag{hello world [TEAM_HASH]}` 会得到 `flag{He1lo_w0r1d_5418ce4d815c}`

## Leet 字符串

Leet 字符串是一种将字符串中的字符替换为数字或符号的方法,例如将 `a` 替换为 `4`,将 `e` 替换为 `3` 等,GZCTF 采用的 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` | `8bB` | `9` | `9g` |

<Callout type="info">

早期的 GZCTF 版本中,Leet 字符串的规则包含了一些特殊字符,例如 `$``@``!` 等,但是由于这些字符在实际的题目环境中造成了各种语言的字符注入问题,因此我们移除了这些字符。

</Callout>

### 安全性

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

$$
\begin{aligned}
H &= \sum_{i=1}^{n} \log_2{m_i} \\
m_i &= \begin{cases}
0 & \text{if } c_i \text{ is not in LeetMap} \\
\text{len}(\text{LeetMap}[c_i]) & \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 代码如下:

```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..."

salt = str_sha256(f"GZCTF@{encrypted_game_pk}@PK")
team_hash = str_sha256(f"{team_token}::{salt}::{chal_id}")[12:24]
```

其中,队伍哈希加盐 `salt` 可以通过管理员权限访问 `/api/edit/games/{id}/teamhashsalt` 接口获取,如需使用请注意保密。

### 正确使用

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

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

### 队伍签名校验

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

```
s2r5WQUClYNsldJrRKanrKivBUtyN+3MjeOiKNL3znI=
```

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

```
1201:HCdjp352NcQoL/4gS8RP3xRt5B9xX2V4m2UeoqfM2dxcLrI5FiYQ7HC9pqreG+tudWjYJf0atzQhhAKyYDKsCg==
```

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

```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](https://pynacl.readthedocs.io/en/latest/)

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

0 comments on commit 9bcabf7

Please sign in to comment.