動的フラッグ

GZCTFには動的フラッグの配布をサポートする機能が組み込まれており、コンテナが起動する際にGZCTF_FLAG環境変数を用いて注入します。

INFO

この環境変数を採用した主な理由は、GZCTFが商業的に乱用されるのを防ぐためであり、そのためこの機能のカスタマイズは短期間では開放されません。

設定ルール

動的なチャレンジのフラッグと添付ファイルの管理ページでは、フラッグのテンプレートが動的フラッグの生成の基準となり、以下のルールがあります:

  1. 空白のままにすると、ランダムなGUIDがフラッグとして生成されます。
  2. [GUID]を指定すると、その部分のプレースホルダーだけがランダムなGUIDに置き換えられます。
  3. [TEAM_HASH]を指定すると、それはチームのトークンと関連情報から生成されたハッシュ値に置き換えられます。
  4. [TEAM_HASH]が指定されていない場合は、リート(Leet)文字列機能が有効になり、テンプレートに基づいて波括弧内の文字列を変換します。フラッグのテンプレート文字列のエントロピーが十分に高いことを確認する必要があります。
  5. [TEAM_HASH]を指定した状態でリート文字列機能を有効にする必要がある場合は、フラッグのテンプレート文字列の前に[LEET]マークを追加してください。この場合、フラッグのテンプレート文字列のエントロピーはチェックされません。
  6. [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)文字列

リート文字列は、文字列中の文字を数字や記号に置き換える方法で、例えばa4に、e3に置き換えるなど、GZCTFでは以下のリート文字列のルールを採用しています:

+-------------+---------------+-------------+---------------+-------------+---------------+
| 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        |
+-------------+----------------+-------------+----------------+-------------+----------------+

セキュリティ

リート文字列のセキュリティは、フラッグのテンプレート文字列のエントロピーに依存します。フラッグのテンプレート中の各文字は、複数の文字に置き換えられる可能性があります。我々は、各可変文字の可変文字集合の長さを2で対数を取り、それを累加することで、リート文字列のエントロピーを得ます:

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以下にはならないように制限されており、それ以下になるとフラッグのセキュリティが低下します。

チームハッシュ

チームハッシュは、チームのトークンと関連情報をハッシュ化する方法で、それは動的フラッグの生成に使用され、各チームが一意のフラッグを持つことを保証します。

GZCTFでは、チームハッシュはSHA256ハッシュの中央12桁で、例えば5418ce4d815cとなり、それはフラッグのテンプレート中の[TEAM_HASH]プレースホルダーに置き換えられます。

チームハッシュの計算には3つのパラメータが使用されます:

  • チームトークン:チーム登録時にシステムが生成、発行し、公開鍵で検証可能なed25519署名
  • チャレンジID:チャレンジの一意の識別子
  • ゲームハッシュのソルト:暗号化された試合署名の秘密鍵をソルト化した後のSHA256ハッシュ

ハッシュの生成には以下のような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エンドポイントにアクセスすることで取得できます。使用する場合は、その機密性を確保してください。

セキュリティ

  • チームトークンはGZCTFによって発行され、ed25519署名であり、公開鍵で検証可能なので、偽造されることはありません
  • ゲームハッシュのソルトはゲーム固有の値であり、ゲーム署名秘密鍵のハッシュから派生しています。管理者はそのセキュリティを確保する必要があります
  • チャレンジIDは整数であり、チャレンジを作成する際にシステムが生成します。管理者はゲームハッシュのソルトを組み合わせてチャレンジハッシュのソルトを計算します
  • チャレンジハッシュのソルトはチャレンジ固有の値であり、最終的なチームハッシュの計算に使用されるべきです。その漏洩は他のチャレンジのセキュリティに影響を与えません

正しい使用法

チームハッシュの主な使用シーンの一つは、外部チャレンジ(チームが最終的にアクセスするコンテナはGZCTFが起動したコンテナではない)です。例えば、一部のWebチャレンジのデプロイが難しく、依存関係が複雑な場合、チャレンジは外部のインスタンスだけを持つことがあり、各チームが独立したインスタンスを持つことはありません。

このような場合、チームトークンを検証し、チームトークンに基づいてフラッグを自己生成することで、各チームが一意の動的フラッグを持つことを保証できます。

チーム署名の検証

試合の公開鍵は試合管理ページから直接取得できます。これはBase64でエンコードされたed25519の公開鍵です。例えば:

s2r5WQUClYNsldJrRKanrKivBUtyN+3MjeOiKNL3znI=

チームトークンはBase64でエンコードされたed25519の署名で、その形式は次のようになります:

1201:HCdjp352NcQoL/4gS8RP3xRt5B9xX2V4m2UeoqfM2dxcLrI5FiYQ7HC9pqreG+tudWjYJf0atzQhhAKyYDKsCg==

以下のコードを使用してチームトークンを検証することができます。ここで、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署名検証ライブラリを使用して、チームトークンがプラットフォームによって署名された有効な署名であることを検証し、フラッグの安全性を暗号学的に保証することができます。