153 lines
9.6 KiB
Python
153 lines
9.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
CTF 解密脚本:Morse + Zero-Width + Ascii85 + AES(多模式试探)
|
||
用法:python solve_flag.py
|
||
"""
|
||
from __future__ import annotations
|
||
from typing import List, Tuple
|
||
import base64, hashlib, itertools
|
||
from Crypto.Cipher import AES
|
||
from Crypto.Util.Padding import unpad
|
||
|
||
# ---- 题面(保持 HTML 实体,不要手改)----
|
||
RAW = """...‌‌‌‌‍‍‌‌‌‌‍‬‍‍.- . --... ‌‌‌‌‍‬‌‌‌‌‌‍‬‌...‌‌‌‌‍‬ --...‌‌‌‌‍‬‍ .--- -‌‌‌‌‍‬‍‍.-. ‌‌‌‌‍‍‌-- .-. ‌‌‌‌‍‬.--‌‌‌‌‍‬‌ -.‌‌‌‌‍‌‬.. -.‌‌‌‌‍‬‍‌‌‌‌‍‌‌‌‌‌‌‍‍‌ --‌‌‌‌‍‬. -. ‌‌‌‌‍‍.‌‌‌‌‍‬--- -.‌‌‌‌‍‌‬‌‌‌‌‍‬‌.- ...‌‌‌‌‍‬‍‌.- .-. ‌‌‌‌‌‬‍‬‌‌‌‌‌‌‍..‌‌‌‌‍‍‌‍.- ‌‌‌‌‍‌‌‍-. ‌‌‌‌‍‍‬‬.‌‌‌‌‌‌‬...-‌‌‌‌‍‍‍ ‌‌‌‌‍‍‌...‌‌‌‌‍‍‬‌.- -- ..‌‌‌‌‌‌..‌‌‌‌‍‌‍‍‌‌‌‌‍‌‍‌ ‌‌‌‌‍‌‌-.‌‌‌‌‌‍‌-.‌‌‌‌‍‍‌‬‌‌‌‌‍‌‍‬ .. ‌‌‌‌‍‍‍‬.... ..‌‌‌‌‌‍‍..‌‌‌‌‍‍‍‌ .‌‌‌‌‍‌‍‌‌‌‌‍‌‌‬- ‌‌‌‌‌‍‬.. .‌‌‌‌‍‍‬‍--‌‌‌‌‍‌‬‌‌‌‌‌‍‌‬- ‌‌‌‌‌‍-‌‌‌‌‍‍‍‍-‌‌‌‌‍‌‬‬-‌‌‌‌‍‌‍-.‌‌‌‌‌‬‌ ..-. .‌‌‌‌‍‌‬‍-- -. ...‌‌‌‌‍‌‬‌‌‌‌‌‬‍ ‌‌‌‌‍‌.---‌‌‌‌‍‌‌ ‌‌‌‌‌‌‌..‌‌‌‌‍‍‌‌-‌‌‌‌‌‬‍‬ -‍‬‍‌‍‍‍‌‌‌‬‍- .-- ‍‍‌‌‌.‍‌‬‬‍‬‬.‍‍‌‬‬‬‌‌‍‍‬‌‍‬-. ‍‍‍‌‬‌‌.‌‌‌‌‍‬‌‬. ‌‌‌‌‍‬‌‍--..‌‌‌‌‍‌. -..-‌‌‌‌‍‬‍‍ ...‌‌‌‌‌‌ ...‌‌‌‌‌‌‬-- ‍‍‍‍‬‍‍-.-‌‌‌‌‍‬. .. 还真是!实际上是这样"""
|
||
|
||
# ---- Step1: 提取摩斯并 Base62 → 36B 密钥素材 ----
|
||
import html as _html
|
||
MORSE = {".-":"A","-...":"B","-.-.":"C","-..":"D",".":"E","..-.":"F","--.":"G","....":"H","..":"I",
|
||
".---":"J","-.-":"K",".-..":"L","--":"M","-.":"N","---":"O",".--.":"P","--.-":"Q",".-.":"R",
|
||
"...":"S","-":"T","..-":"U","...-":"V",".--":"W","-..-":"X","-.--":"Y","--..":"Z",
|
||
"-----":"0",".----":"1","..---":"2","...--":"3","....-":"4",".....":"5","-....":"6",
|
||
"--...":"7","---..":"8","----.":"9"}
|
||
decoded = _html.unescape(RAW)
|
||
morse_only = ''.join(ch for ch in decoded if ch in '.- ')
|
||
morse_text = ''.join(MORSE.get(tok, '?') for tok in morse_only.split())
|
||
# Base62 解码
|
||
import string
|
||
B62 = string.digits + string.ascii_uppercase + string.ascii_lowercase
|
||
val = 0
|
||
for ch in morse_text:
|
||
val = val * 62 + B62.index(ch)
|
||
b62_bytes = val.to_bytes((val.bit_length()+7)//8, 'big') or b'\x00'
|
||
|
||
# ---- Step2: 解析零宽 → Ascii85 → 密文字节 ----
|
||
ZWS = (0x200C, 0x200D, 0xFEFF, 0x202C) # ZWNJ, ZWJ, ZWNBSP, PDF
|
||
codes = [ord(c) for c in decoded if ord(c) in ZWS]
|
||
|
||
def ascii85_candidates() -> List[bytes]:
|
||
out: List[bytes] = []
|
||
for perm in itertools.permutations(ZWS, 4):
|
||
mp = {perm[i]: format(i, '02b') for i in range(4)}
|
||
bits = ''.join(mp[c] for c in codes)
|
||
for off in range(8):
|
||
bbits = bits[off:][:len(bits[off:])//8*8]
|
||
if not bbits: continue
|
||
data = int(bbits, 2).to_bytes(len(bbits)//8, 'big')
|
||
for take in (data, data[0::2], data[1::2]): # 全量/偶/奇抽样
|
||
s = bytes(ch for ch in take if 33 <= ch <= 117 or ch == 122) # 过滤到 Ascii85 合法区
|
||
if len(s) < 20:
|
||
continue
|
||
try:
|
||
blob = base64.a85decode(s, adobe=False)
|
||
out.append(blob)
|
||
except Exception:
|
||
pass
|
||
# 去重
|
||
uniq: List[bytes] = []
|
||
seen = set()
|
||
for b in out:
|
||
h = hashlib.sha1(b).hexdigest()
|
||
if h not in seen:
|
||
uniq.append(b); seen.add(h)
|
||
return uniq
|
||
|
||
a85_blobs = ascii85_candidates()
|
||
assert a85_blobs, "没解析到任何 Ascii85 候选;请确认文本原样未被改动"
|
||
|
||
# ---- Step3: 生成密钥候选(16/24/32)----
|
||
def key_candidates() -> List[bytes]:
|
||
C: List[bytes] = []
|
||
srcs = [morse_text.encode(), b62_bytes]
|
||
for s in srcs:
|
||
C += [hashlib.md5(s).digest(),
|
||
hashlib.sha1(s).digest()[:16],
|
||
hashlib.sha256(s).digest(),
|
||
hashlib.blake2b(s, digest_size=32).digest()]
|
||
# 额外:直接取 b62 的前/后 16/24/32
|
||
for L in (16,24,32):
|
||
C += [b62_bytes[:L].ljust(L, b'\0'),
|
||
b62_bytes[-L:].rjust(L, b'\0')]
|
||
# 去重
|
||
uniq: List[bytes] = []
|
||
seen = set()
|
||
for k in C:
|
||
h = (len(k), hashlib.sha1(k).hexdigest())
|
||
if h not in seen:
|
||
uniq.append(k); seen.add(h)
|
||
return uniq
|
||
|
||
KEYS = key_candidates()
|
||
|
||
# ---- Step4: 穷举 AES 模式与 IV/nonce 切分,命中 flag ----
|
||
def try_ctr(ct: bytes, key: bytes) -> List[Tuple[str, bytes]]:
|
||
outs = []
|
||
L = len(ct)
|
||
for split in range(4, min(24, L-4)):
|
||
iv = ct[:split]; body = ct[split:]
|
||
for nonce_len in range(0, min(15, split)+1):
|
||
nonce = iv[:nonce_len]; rem = iv[nonce_len:]
|
||
for name, init in (("zero", 0),
|
||
("rem_be", int.from_bytes(rem, "big") if rem else 0),
|
||
("rem_le", int.from_bytes(rem[::-1], "big") if rem else 0)):
|
||
try:
|
||
pt = AES.new(key, AES.MODE_CTR, nonce=nonce, initial_value=init).decrypt(body)
|
||
outs.append((f"CTR split={split} nonce={nonce_len} init={name}", pt))
|
||
except Exception:
|
||
pass
|
||
return outs
|
||
|
||
def try_stream_modes(ct: bytes, key: bytes) -> List[Tuple[str, bytes]]:
|
||
outs = []
|
||
L = len(ct)
|
||
# CFB/OFB 尝试 iv 在前/后
|
||
for mode_name, MODE in (("CFB", AES.MODE_CFB), ("OFB", AES.MODE_OFB)):
|
||
for ivpos in ("head","tail"):
|
||
if L <= 16: continue
|
||
iv, body = (ct[:16], ct[16:]) if ivpos=="head" else (ct[-16:], ct[:-16])
|
||
try:
|
||
pt = AES.new(key, MODE, iv=iv).decrypt(body)
|
||
outs.append((f"{mode_name} iv={ivpos}", pt))
|
||
except Exception:
|
||
pass
|
||
# CBC(尽管不太像)
|
||
for ivpos in ("head","tail"):
|
||
if L <= 16: continue
|
||
iv, body = (ct[:16], ct[16:]) if ivpos=="head" else (ct[-16:], ct[:-16])
|
||
body = body[:len(body)//16*16]
|
||
try:
|
||
raw = AES.new(key, AES.MODE_CBC, iv=iv).decrypt(body)
|
||
try: pt = unpad(raw, 16)
|
||
except Exception: pt = raw
|
||
outs.append((f"CBC iv={ivpos}", pt))
|
||
except Exception:
|
||
pass
|
||
return outs
|
||
|
||
def solve() -> None:
|
||
# 先打印你要的两个“中间产物”,便于复核
|
||
print("[MORSE] =", morse_text)
|
||
print("[B62 hex] =", b62_bytes.hex())
|
||
# 遍历所有密文候选 × 密钥候选 × 模式
|
||
for blob in a85_blobs:
|
||
for key in KEYS:
|
||
for tag, pt in try_ctr(blob, key) + try_stream_modes(blob, key):
|
||
s = pt.decode("utf-8", "ignore")
|
||
if "flag{" in s.lower():
|
||
print("[HIT]", tag, "key_len=", len(key))
|
||
print(s)
|
||
return
|
||
print("[X] 未命中。可再加:GCM(带不同 tag 长度)、XOR-探测、zlib 解压后再试。")
|
||
|
||
if __name__ == "__main__":
|
||
solve()
|