iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0
自我挑戰組

新手挑戰 picoCTF:資安入門紀錄系列 第 28

picoCTF — substitution0 學習紀錄

  • 分享至 

  • xImage
  •  

今天要挑戰的題目是:
https://ithelp.ithome.com.tw/upload/images/20251010/20178898JjFhbvfDtN.png
題目說「it seems to have the key at the beginning」
→ 常見情形是檔案第一行是 substitution cipher 的 key(長度 26),接下來是被加密的訊息(密文)。

目標:根據檔案開頭的 key 還原密文,並從解出的英文中找到 flag。


解題思路

  1. 先取出檔案的key。

  2. 判斷 key 的意義(大致可分為兩種):
    -情形 A:key 表示 cipher alphabet,也就是密文字母表。要解密時,對每個密文字母 c,找 c 在 key 的 index → 對應回標準 alphabet a..z(index 對應的明文字母)。

-情形 B:key 表示 plaintext alphabet order(不常見),即 a..z 對應到 key。解法會不同,因此需要嘗試兩種方向,看看哪個產出可讀英文。

  1. 對密文只替換英文字母(保留大小寫),其他字元(空白、標點)不變。

  2. 若結果為可讀英文,就找 flag(通常是 The flag is: 或 picoCTF{...})。


解題步驟
我用了一個小 script(自動下載 / 解析 / 嘗試兩種 mapping):

cat > solve_substitution.py <<'PY'
import sys, string, urllib.request

def get_text(src):
    if src.startswith("http://") or src.startswith("https://"):
        data = urllib.request.urlopen(src).read()
        try:
            return data.decode()
        except:
            return data.decode('latin1')
    else:
        return open(src, encoding="utf8").read()

def try_decode(key, ciphertext):
    alpha = string.ascii_lowercase
    results = []
    if len(key) == 26:
        key = key.lower()
        map1 = { key[i]: alpha[i] for i in range(26) }
key[index_of_cipher_in_alpha]
        map2 = { alpha[i]: key[i] for i in range(26) }
        for name,mapping in [("assume key == cipher_alphabet (decode by key->plain)", map1),
                             ("assume key == plaintext_alphabet order (decode by a..z->key)", map2)]:
            out = []
            for ch in ciphertext:
                low = ch.lower()
                if low in mapping:
                    plain = mapping[low]
                    out.append(plain.upper() if ch.isupper() else plain)
                else:
                    out.append(ch)
            results.append((name, "".join(out)))
    else:
        results.append(("key-not-26", "Key length != 26; raw key: " + key))
    return results

def main():
    src = sys.argv[1] if len(sys.argv)>1 else "https://artifacts.picoctf.net/c/154/message.txt"
    txt = get_text(src).strip().splitlines()
    first = txt[0].strip()
    if " " in first:
        key = first.split()[0]
    else:
        key = first
    cipher_lines = txt[1:] if len(txt)>1 else []
    if not cipher_lines:
        rest = first.split(maxsplit=1)
        if len(rest) > 1:
            key = rest[0]; cipher_lines = [rest[1]]
    ciphertext = "\n".join(cipher_lines).strip()
    print("Parsed key:", repr(key))
    print("\nCiphertext (preview):\n", ciphertext[:500], "\n")
    decs = try_decode(key, ciphertext)
    for name, dec in decs:
        print("-----", name, "-----")
        print(dec[:4000])
        print()
    print("Done.")

if __name__ == '__main__':
    main()
PY

chmod +x solve_substitution.py
./solve_substitution.py

系統輸出:
https://ithelp.ithome.com.tw/upload/images/20251010/20178898FrYAF6VLPd.png
兩種嘗試中,第一種(assume key == cipher_alphabet) 產生的是清楚的英文段落,其中包含:
The flag is: picoCTF{5UB5717U710N_3V0LU710N_357BF9FF}
因此我們可以確定第一種 mapping 正確,此題正確Flag就是:picoCTF{5UB5717U710N_3V0LU710N_357BF9FF}


原理說明及心得
為何第一種 mapping 是正確方向?
直觀檢查:把密文字母 c 在 key 中尋找其 index,然後用 a..z 的那個 index 做對應,得到的解是通順英文 → 這是 cipher-alphabet 的典型行為。

如果相反方向(把 a..z 映到 key),解出來是無意義的雜湊文字(script 也輸出作比較),因此可以以可讀性判斷正確 mapping。

看到「key 在開頭」就先檢查第一行是否為 26 字母排列。

針對 substitution 題,寫一支小工具自動嘗試並輸出通常能快速得到答案。


上一篇
picoCTF:VaultDoor1
系列文
新手挑戰 picoCTF:資安入門紀錄28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言