Hello Navi

Tech, Security & Personal Notes

trytodecrypt.com

Text 19

5F70017FDD92B75AA6668648B404223663157787B35686FA165A8193E5075777F

与 Text 16 类似,每 4 位 hex 一组(偏移 + 编码字符)。前 13 位 hex(8 字节)是前缀/校验,有效数据从第 14 位开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
CHARSET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! "

# ============================================================
# Text 19 — Too Much 1
# Known solution: R2D2:C3PO:BB8
# Structure: each char encoded as 5 hex chars
# First n hex chars = separators (1 per char)
def decode_text19(ct):
result = ""
if len(ct) == 65:
data = ct[13:]
else:
raise ValueError(f"Unexpected ciphertext length: {len(ct)}")

for i in range(0, len(data), 4):
pair = data[i : i + 4]
if len(pair) < 4:
break
offset = int(pair[0:2], 16)
enc = int(pair[2:4], 16)
diff = (enc - offset) % len(CHARSET)
if 0 <= diff < len(CHARSET):
result += CHARSET[diff]
else:
result += "?"

# Fixed version has 2 key chars prefix
if len(ct) == 76 and len(result) > 2:
return result[2:]
return result
R2D2:C3PO:BB8

Text 20

8221E4F2173368D6B6B6E5050935D986A8C4CA764CF8A8C4B734E99807140B19DB691998095CC4E3D6C60D6E91

结构

目标密文长度是 90 hex,明文长度应为 18 字符。API 加密满足:

1
len(encrypt(text)) == 5 * len(text)

每 5 hex token 可以按下面方式观察:

1
[prefix 1 hex][a 2 hex][b 2 hex]

目标密文按这个 layout 拆开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pos group  p   a    b    (b-a)%71
0 8221E 8 34 30 67
1 4F217 4 242 23 65
2 3368D 3 54 141 16
3 6B6B6 6 182 182 0
4 E5050 E 80 80 0
5 935D9 9 53 217 22
6 86A8C 8 106 140 34
7 4CA76 4 202 118 58
8 4CF8A 4 207 138 2
9 8C4B7 8 196 183 58
10 34E99 3 78 153 4
11 80714 8 7 20 13
12 0B19D 0 177 157 51
13 B6919 B 105 25 62
14 98095 9 128 149 21
15 CC4E3 C 196 227 31
16 D6C60 D 108 96 59
17 D6E91 D 110 145 35

这题是 randomized encryption:同一 plaintext 每次加密都不同。简单把 b-aa-bxorprefix 当 key 都不成立。

1
2
3
prefixes = ct[:n]
pairs = ct[n:]
item_i = (prefixes[i], pairs[4*i:4*i+2], pairs[4*i+2:4*i+4])

在这个 layout 下,相邻 token 的 transition 有强信号:

1
delta_i = b_i - a_{i+1} mod 71

对 one-hot plaintext(例如 aaaaaaaaaaaaaaaaaawwwwwwwwwwwwwwwwww)采样时,前 13 个 transition 的 delta_i 对字符有明显泄露。它不是完美映射,会有错字和缺位,但不是随机噪声。把目标密文的前 13 个 transition 丢进这个映射,得到:

1
Par!2Lan6aaND

这个结果已经足够说明几件事:

1
2
3
Par      -> 很像 Paris 的开头
Lan -> 很像 London 的中段/开头线索
aaND -> 有明显的 N/D 大写结构,像 NewYork 这类拼接地名的残片

也就是说,算法至少泄露出“城市名串”的轮廓。再结合 Text 20 明文长度必须是 18 字符,最自然的补全是:

1
ParisLondonNewYork

用 solve API 验证:

1
solve?id=20&solution=ParisLondonNewYork -> 1
ParisLondonNewYork

已排除的方向

这些方向已经用 oracle 样本和 held-out 测试排除,不值得无新假设地重复:

1
2
3
4
5
6
7
8
exact 5-hex token dictionary
inline/front/split-half/first-byte key 布局
(b-a)%71, (a-b)%71, +/- prefix, xor, raw byte
first K hex as global key/nonce
affine forms mod 71/72/128/256
per-position naive Bayes / local feature classifier
Text20 -> Text23 子块投票迁移
solve API 低置信候选枚举

尤其是统计模型很容易在 constant corpus 上过拟合。用 random plaintext 做 5-fold held-out 后,top5 基本等于随机基线:

1
2
3
4
5
6
fold0 top1=0.0139 top5=0.0736
fold1 top1=0.0181 top5=0.0833
fold2 top1=0.0181 top5=0.0806
fold3 top1=0.0097 top5=0.0542
fold4 top1=0.0194 top5=0.0833
random_top5 baseline = 5/71 = 0.0704

一个有价值但尚未破解的结构

把密文看成 front layout:前 n 个 hex 是 prefix,后面每字符 4 hex 是两个 byte。这个视角下,前 13 个相邻 transition 有明显结构:

1
delta_i = b_i - a_{i+1} mod 256

对 repeated char 样本,前 13 个 transition 很稳定:

1
2
3
w -> w  基本总是 0
0 -> 0 主要是 163 / 162
a -> a 主要是 114 / 115

但从 pos13 之后,这个 transition 会退化成近随机。也就是说,算法里可能存在一段链式状态,长度或边界不是简单的 18 字符全程一致。

尝试把 target 的 transition 当成普通 pair dictionary 解路径失败:高分候选都不是合理明文,少量提交也返回 0。因此这里的结构更像某种 state/nonce/PRNG relation,而不是 F(ch_i, ch_{i+1}) 这种直接查表。

当前结论

Text 20 的答案已通过 solve API 验证。它的核心不是 per-token decode,也不是纯靠猜;而是:

1
2
3
4
5
1. 通过长度确认明文 18 字符。
2. 改用 front layout 拆出 prefix 和 byte pairs。
3. 从相邻 transition 中恢复出前 13 位附近的带噪声骨架:Par!2Lan6aaND。
4. 根据骨架和 18 字符长度,提出城市名串候选 ParisLondonNewYork。
5. 用 solve API 验证,返回 1。

所以这里确实有语义猜测,但不是 blind guess。更准确地说,它是“结构泄露 + 人类模式识别 + oracle 验证”。后 5 位没有找到独立稳定 channel,因此没有把完整公式还原出来。

Text 21

333131353156333131323231305230363135315631333151342F3430313131323154342F

每字符加密为 4 位 hex(2 ASCII 字节),固定替换表。密文 hex 解码后每 2 字节对应一个明文字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/usr/bin/env python3
"""Build character mapping from trytodecrypt.com encrypt API.

Usage: trytodecrypt_get_char_map.py <text_id> <api_key>

Encrypts each character in the charset via the API, maps
full response -> character. Parallel requests for speed.
Outputs a sorted Python dict literal.
"""
import sys
import urllib.parse
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed

C = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! "
URL = "http://api.trytodecrypt.com/encrypt?key={key}&id={id}&text={text}"


def encrypt(ch, text_id, api_key):
url = URL.format(key=api_key, id=text_id, text=urllib.parse.quote(ch))
try:
with urllib.request.urlopen(url, timeout=10) as resp:
return resp.read().decode().strip()
except Exception as e:
return None


if __name__ == "__main__":
text_id = sys.argv[1]
api_key = sys.argv[2]

mapping = {}
total = len(C)
with ThreadPoolExecutor(max_workers=12) as pool:
fut_map = {pool.submit(encrypt, ch, text_id, api_key): ch for ch in C}
for i, fut in enumerate(as_completed(fut_map), 1):
ch = fut_map[fut]
enc = fut.result()
if enc:
mapping[enc] = ch
print(f"\r [{i}/{total}] {repr(ch)} -> {enc or 'FAIL'}" + " " * 10,
end="", file=sys.stderr, flush=True)
print(file=sys.stderr)

if mapping:
print({k: mapping[k] for k in sorted(mapping.keys())})
else:
print("{}")
sys.exit(1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# ============================================================
# Text 21 — Too Much 3
# Known solution: TryToDecrypt! now!
# Encryption: each plaintext char maps to 4 fixed hex chars
# (step=4, simple substitution)
# ============================================================
def decode_text21(ct):
"""Decode Text 21 — fixed 4-hex substitution."""
# The known mapping (built from encrypt oracle):
# Each char -> exactly 4 hex chars
mapping = {
'0': '2F54',
'1': '2F55',
'2': '2F56',
'3': '302D',
'4': '302E',
'5': '302F',
'6': '3030',
'7': '3031',
'8': '3032',
'9': '3033',
'a': '3034',
'b': '3035',
'c': '3036',
'd': '3051',
'e': '3052',
'f': '3053',
'g': '3054',
'h': '3055',
'i': '3056',
'j': '312D',
'k': '312E',
'l': '312F',
'm': '3130',
'n': '3131',
'o': '3132',
'p': '3133',
'q': '3134',
'r': '3135',
's': '3136',
't': '3151',
'u': '3152',
'v': '3153',
'w': '3154',
'x': '3155',
'y': '3156',
'z': '322D',
'A': '322E',
'B': '322F',
'C': '3230',
'D': '3231',
'E': '3232',
'F': '3233',
'G': '3234',
'H': '3235',
'I': '3236',
'J': '3251',
'K': '3252',
'L': '3253',
'M': '3254',
'N': '3255',
'O': '3256',
'P': '332D',
'Q': '332E',
'R': '332F',
'S': '3330',
'T': '3331',
'U': '3332',
'V': '3333',
'W': '3334',
'X': '3335',
'Y': '3336',
'Z': '3351',
'-': '3352',
'_': '3353',
'.': '3354',
',': '3355',
';': '3356',
':': '342D',
'?': '342E',
'!': '342F',
' ': '3430',
}
reverse_mapping = {v: k for k, v in mapping.items()}

result = ""
for i in range(0, len(ct), 4):
chunk = ct[i : i + 4]
if chunk in reverse_mapping:
result += reverse_mapping[chunk]
else:
result += "?"
return result
TryToDecrypt! now!

Text 22

00100401400A0120A101C0310F503706004E05B0870A00880D80ED0BE1262890FD16816A1453453721963ED1D11F04624D9

结构分析

99 hex,每字符加密为 9 hex3 组 × 3 hex),共 11 字符。

加密是确定性的——同一输入永远返回同一输出——但算法是位置相关的:同一个字符在不同位置产生完全不同的密文。

加密 0(charset index 0)和 a(index 10)在不同位置的输出:

1
2
位置 0: '0' → [001, 003, 008]   'a' → [001, 003, 050]   仅 group2 变化
位置 1: '0' → [00A, 00F, 01F] 'a' → [00A, 012, 01C] group1/2 都变

关键观察:

  • group0 只与位置有关,与字符无关(同一位置所有字符共享同一个 group0,如位置 0 永远是 001、位置 1 永远是 00A)
  • group1 + group2 共同编码字符,但公式复杂且各位置不同
  • 不存在简单的线性公式——尝试过 (b2-b1) % 71(b1+b2-K) % 71(b1 XOR b2) % 71 等均不成立

解法:progressive guessing(渐进猜解)

不需要理解加密公式也能解密——只要能调用加密工具,就可以暴力猜解。

核心依赖密文的前缀保持性质

1
2
3
encrypt("a")     → 001003050          (9 hex)
encrypt("ab") → 00100305000A01201F (18 hex,前 9 hex 与 "a" 一致)
encrypt("abc") → 00100305000A01201F... (前 18 hex 与 "ab" 一致)

密文的前 N×9 hex 完全由明文的前 N 个字符决定。后续字符不影响前面的密文段。

算法:

  1. 从空字符串开始
  2. 对位置 i,已有正确前缀 guess(前 i 个字符已破解)
  3. 遍历 charset 中全部 71 个候选字符 c
  4. 通过 API 加密 guess + c
  5. 如果返回的密文以目标密文的前 (i+1)×9 hex 开头,则 c 就是第 i 位字符
  6. 重复至 11 位全部破解

最坏 11 × 71 = 781 次 API 调用,几分钟跑完。

关于反爬

最初尝试用 web 端加密(POST https://www.trytodecrypt.com/decrypt.php)做猜解,但非浏览器请求被服务端 bot 检测拦截,始终返回 503 Service Unavailable。即使带上 PHPSESSID cookie 和 User-Agent 也无济于事。

切换到独立 API 接口即解决:

1
2
3
4
5
6
# 有反爬(503)
POST https://www.trytodecrypt.com/decrypt.php?id=22
body: text=a&encrypt=Encrypt

# 无反爬(正常)
GET http://api.trytodecrypt.com/encrypt?key=KEY&id=22&text=a

API 端用 key 做身份认证,不做 bot 检测。key 在登录后从 API 页面 获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import urllib.request, urllib.parse
API_KEY = 'YOUR_KEY_HERE'
CHARSET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! '
target = '00100401400A0120A101C0310F503706004E05B0870A00880D80ED0BE1262890FD16816A1453453721963ED1D11F04624D9'
guess = ''
for pos in range(11):
for ch in CHARSET:
test = guess + ch
url = f'http://api.trytodecrypt.com/encrypt?key={API_KEY}&id=22&text={urllib.parse.quote(test)}'
resp = urllib.request.urlopen(url).read().decode().strip()
if resp.upper().startswith(target[:len(test)*9]):
guess = test
break
print(f' [{pos+1}/11] {guess}')
"

运行过程:

1
2
3
4
5
6
7
8
9
10
11
[1/11] m
[2/11] mi
[3/11] mis
[4/11] miss
[5/11] missi
[6/11] missis
[7/11] mississ
[8/11] mississi
[9/11] mississip
[10/11] mississipp
[11/11] mississippi
mississippi

Text 23

E3F59F001361B62958E551B9702F2C6B25F9E3FC350062295A1A20182041493C447BA0767A393A1F278DB14268565F51575C65212A8386494B383F7375676845472F30494C737A406890988B8D50577A835960476B6F73686E6367668B787A494C33357EA4555E191C18216A6F353A173E2026474A8A8C3F481416759D

这题最后的关键不是 PRNG,也不是统计分类,而是 递归套壳:Text 23 的整段 250-hex ciphertext 可以先按 Text19/Text20 那种 front-prefix layout 解出一层 50-hex 中间密文;这个中间密文再用同一个规则解一次,得到真正 plaintext。

第一层:把 250 hex 当成 50 个 5-hex token

目标密文长度是 250 hex。把它整体看成 50 个 5-hex token。

沿用前面 Text 19 / Text 20 里反复出现的 layout:

1
2
3
4
prefixes = ct[:n]
pairs = ct[n:]
item_i = (prefixes[i], pairs[4*i:4*i+2], pairs[4*i+2:4*i+4])
plain_i = CHARSET[(b_i - a_i) % 71]

对 Text 23,n = 250 / 5 = 50。第一层解出来不是最终明文,而是一个仍然全为 hex 的 50 字符串:

1
6888B418AC9699327212137E82797A464B232C93955D63292E

这一点非常反直觉,因为 API 行为确实显示:

1
len(encrypt(text)) == 25 * len(text)

所以目标明文长度看起来应是 10 字符。但真正结构是:外层把「内层 50-hex 密文」再包了一次。

第二层:对 50-hex 中间密文再解一次

中间密文长度 50 hex,同样可看成 10 个 5-hex token。再跑同一个 decode:

完整复现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CHARSET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! '


def decode_once(ct):
n = len(ct) // 5
prefixes = ct[:n]
pairs = ct[n:]

out = ''
for i in range(n):
a = int(pairs[4*i:4*i+2], 16)
b = int(pairs[4*i+2:4*i+4], 16)
out += CHARSET[(b - a) % len(CHARSET)]
return out


target = 'E3F59F001361B62958E551B9702F2C6B25F9E3FC350062295A1A20182041493C447BA0767A393A1F278DB14268565F51575C65212A8386494B383F7375676845472F30494C737A406890988B8D50577A835960476B6F73686E6367668B787A494C33357EA4555E191C18216A6F353A173E2026474A8A8C3F481416759D'

mid = decode_once(target)
plain = decode_once(mid)

print(mid)
print(plain)
3.14159265

Challenge

It seems that the simple substitution ciphers are too easy for you. From my own experience I can tell that transposition ciphers are more difficult to attack. However, in this training challenge you should have not much problems to reveal the plaintext.

oWdnreuf.lY uoc nar ae dht eemssga eaw yebttrew eh nht eelttre sra enic roertco drre . Ihtni koy uowlu dilekt oes eoyrup sawsro don:we raphbmnmld.s

A transposition cipher where adjacent character pairs are swapped. The message includes: "Wonderful. You can read the message way better when the letters are in correct order. I think you would like to see your password now: XXXXXXXX"

Solution

Swap each adjacent pair of characters:

1
2
3
4
5
6
ct = "oWdnreuf.lY uoc nar ae dht eemssga eaw yebttrew eh nht eelttre sr..."
chars = list(ct)
for i in range(0, len(chars) - 1, 2):
chars[i], chars[i+1] = chars[i+1], chars[i]
result = ''.join(chars)
# result: "Wonderful. You can read the message way better when the letters are in correct order..."

Extract the password after "password now: ".

earhpmbmndls

Challenge

I guess you are done with Caesar I, aren't you? The big problem with caesar is that it does not allow digits or other characters. I have fixed this, and now I can use any ascii character in the plaintext. The keyspace has increased from 26 to 128 too. /

Enjoy!

Caesar cipher with full ASCII range (0-127). The keyspace has increased from 26 to 128.

Solution

The cipher bytes are displayed as hex values. Decode by trying all 128 shifts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
hex_text="""53 7B 7B 70 20 76 7B 6E 38 20 05 7B 01 20 7F 7B
78 02 71 70 20 7B 7A 71 20 79 7B 7E 71 20 6F 74
6D 78 78 71 7A 73 71 20 75 7A 20 05 7B 01 7E 20
76 7B 01 7E 7A 71 05 3A 20 60 74 75 7F 20 7B 7A
71 20 03 6D 7F 20 72 6D 75 7E 78 05 20 71 6D 7F
05 20 00 7B 20 6F 7E 6D 6F 77 3A 20 63 6D 7F 7A
33 00 20 75 00 4B 20 3D 3E 44 20 77 71 05 7F 20
75 7F 20 6D 20 7D 01 75 00 71 20 7F 79 6D 78 78
20 77 71 05 7F 7C 6D 6F 71 38 20 7F 7B 20 75 00
20 7F 74 7B 01 78 70 7A 33 00 20 74 6D 02 71 20
00 6D 77 71 7A 20 05 7B 01 20 00 7B 7B 20 78 7B
7A 73 20 00 7B 20 70 71 6F 7E 05 7C 00 20 00 74
75 7F 20 79 71 7F 7F 6D 73 71 3A 20 63 71 78 78
20 70 7B 7A 71 38 20 05 7B 01 7E 20 7F 7B 78 01
00 75 7B 7A 20 75 7F 20 6F 7E 73 6D 6E 7C 7F 7B
6D 7C 73 79 3A"""
ct_bytes = [int(x, 16) for x in hex_text.split()]
for K in range(128):
decoded = ''.join(chr((b - K) % 128) for b in ct_bytes)
if 'is' in decoded:
print(f"K={K}: {decoded}")

The space character (0x20) is part of the shift and will appear as a different character after decoding. Replace the most frequent non-alpha character with space to read the message.

crgabpsoapgm

Challenge

A 4×4 pixel BMP image with hidden data. The challenge title says "This is the most basic image stegano I can think of."

Solution

Download the BMP file and read the pixel data directly — the message is stored as plaintext in the pixel bytes, not hidden with LSB steganography.

1
2
$ curl -O https://www.wechall.net/en/challenge/training/stegano1/stegano1.bmp
$ xxd stegano1.bmp | head -5

The pixel data at offset 54 contains the ASCII message:

1
2
3
4c 6f 6f 6b 20 77 68 61 74 20 74 68 65 20 68 65
78 2d 65 64 69 74 20 72 65 76 65 61 6c 65 64 3a
20 70 61 73 73 77 64 3a 73 74 65 67 61 6e 6f 49

Decoded: "Look what the hex-editor revealed: passwd:steganoI"

steganoI

Challenge

The page displays a string of ASCII numeric codes:

1
84, 104, 101, 32, 115, 111, 108, 117, 116, 105, 111, 110, 32, 105, 115, 58, 32, 111, 114, 100, 97, 101, 110, 112, 114, 115, 115, 105, 112

Solution

将 ASCII 码数字转换为字符:

1
2
nums = [84, 104, 101, 32, 115, 111, 108, 117, 116, 105, 111, 110, 32, 105, 115, 58, 32, 111, 114, 100, 97, 101, 110, 112, 114, 115, 115, 105, 112]
print(''.join(chr(n) for n in nums))
ordaenprssip

Challenge

Understanding the robots.txt file.

Solution

访问目标网站的 /robots.txt,查看禁止爬取的路径,T0PS3CR3T 目录就是答案。

1
2
3
4
$ curl https://www.wechall.net/robots.txt
User-agent: *
Disallow: /challenge/training/www/robots/T0PS3CR3T
...

然后访问 https://www.wechall.net/challenge/training/www/robots/T0PS3CR3T/ 确认。

T0PS3CR3T

Challenge

A standard Caesar cipher with random shift per session. The plaintext is: "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG OF CAESAR AND YOUR UNIQUE SOLUTION IS XXXXXXXX"

Solution

Brute force all 26 shifts and look for "YOUR UNIQUE SOLUTION" in the decoded text.

1
2
3
4
5
6
7
8
9
10
11
12
13
ciphertext = "JXU GKYSA RHEMD VEN ZKCFI ELUH JXU BQPO TEW EV SQUIQH QDT OEKH KDYGKU IEBKJYED YI DFUWHHDUYVBF"

for shift in range(26):
result = ''
for c in ciphertext:
if c.isalpha():
base = ord('A')
result += chr((ord(c) - base - shift) % 26 + base)
else:
result += c
if "YOUR UNIQUE SOLUTION IS" in result:
solution = result.split("IS ")[-1].split()[0]
print(f"Solution: {solution}")

The last word after "IS" is the per-session solution.

You can also use CyberChef to decode the ciphertext:

1
2
ROT13_Brute_Force(true,true,false,100,0,true,'')
input: JXU GKYSA RHEMD VEN ZKCFI ELUH JXU BQPO TEW EV SQUIQH QDT OEKH KDYGKU IEBKJYED YI DFUWHHDUYVBF
NPEGRRNEIFLP

tryptodecrypt.com

Hard 级别的密钥嵌入在密文结构本身。

Text 13

59656A6B6F9F656A67746767

首字节 0x59 (89) 为全局 key。后续每字节减 key 得 charset 位置。

1
2
3
4
5
def decode_text13(ct):
"""首字节为全局 key"""
key = int(ct[:2], 16)
data = [int(ct[i:i+2], 16) for i in range(2, len(ct), 2)]
return "".join(C[b - key] for b in data)
chim cheree

Text 14

6F5657A6606B7D9C7480649D7A6B757D9C70816B6CB4

前 3 字节 [0x6F, 0x56, 0x57] 为旋转 key。后续每字节依次减去对应 key。

1
2
3
4
5
def decode_text14(ct):
"""前 3 字节为旋转 key"""
keys = [int(ct[i:i+2], 16) for i in range(0, 6, 2)]
data = [int(ct[i:i+2], 16) for i in range(6, len(ct), 2)]
return "".join(C[data[i] - keys[i % 3]] for i in range(len(data)))
Take the blue pill!

Text 15

574168755997984F7A7E76AD6954A662538F764F7A5C4F876544

前 6 位 hex 是三个 2 位子密钥 (57, 41, 68)。子密钥交替加密后续字符(密钥 1 加密第 4/7/10...个字符)。字符加密 = 子密钥 + 字符专用偏移量。

第一层:静态替换表

每个字符硬编码一个唯一的偏移量,跟字符集索引无关:

1
2
3
a→0x27  b→0x0b  c→0x41  d→0x45  e→0x0e ...
A→0x1e B→0x13
空格→0x12 句号→0x03

这就是个查表替换,只不过替换结果不是另外一个字符,是一个数字。

第二层:类维吉尼亚加密

拿替换后的数字,加上循环密钥(57→41→68→57→…),得到最终密文:

1
2
3
4
5
A  → 查表得 0x1e  → +密钥 0x57 → 0x75
l → 查表得 0x18 → +密钥 0x41 → 0x59
i → 查表得 0x2f → +密钥 0x68 → 0x97
c → 查表得 0x41 → +密钥 0x57 → 0x98
...

加密工具虽然每请求随机化(Hard 特性),但是同一个字符在同一个位置的加密结果每次都一样。所以:

  1. 加密 "aaaaaa" → 看密文的重复规律,发现每 3 个字节一个周期 → 密钥长度 3
  2. 加密 "aaaaaaaaaaaa" → 同一位置加密 "a" 多次 → 确认 "a" 的密文总是 0x7e(当密钥=0x57时)
  3. 加密 "ba"、"ca"、"da"… → 算出每个字符的偏移量
  4. 密钥值本身:从密文前 6 位直接读出来的(这就是为什么说密钥嵌在密文结构里)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CHAR_OFF = {
'a': 0x27, 'b': 0x0b, 'c': 0x41, 'd': 0x45, 'e': 0x0e,
'f': 0x10, 'g': 0x11, 'h': 0x05, 'i': 0x2f, 'j': 0x1c,
'k': 0x16, 'l': 0x18, 'm': 0x04, 'n': 0x35, 'o': 0x3e,
'p': 0x37, 'q': 0x1d, 'r': 0x1f, 's': 0x15, 't': 0x21,
'u': 0x1a, 'v': 0x23, 'w': 0x00, 'x': 0x0c, 'y': 0x3b,
'z': 0x30, '.': 0x03, ' ': 0x12, 'A': 0x1e, 'B': 0x13,
}
# 反转: 偏移 -> char
OFF_CHAR = {v: k for k, v in CHAR_OFF.items()}

def decode_text15(ct):
"""前 6 hex 为 3 子密钥, data - key[i%3] 得偏移查表"""
keys = [int(ct[i:i+2], 16) for i in range(0, 6, 2)]
data = [int(ct[i:i+2], 16) for i in range(6, len(ct), 2)]
plain = ""
for i, d in enumerate(data):
off = d - keys[i % 3]
plain += OFF_CHAR.get(off, "?")
return plain
Alice and Bob are here.

Text 16

32632E3149844B82115794BA78AD87C36DA01148707080C65459255C2C6487B02851

每 4 位 hex 一组(2 位偏移 + 2 位编码字符)。编码字符 - 偏移 = 字符集索引。

编码:0=00, 1=01, ..., 9=09, a=0A, ..., z=23, A=24, ..., Z=3D, space=46, fullstop=40

1
2
3
4
5
6
7
8
9
def decode_text16(ct):
"""4-hex 一组 (2偏移 + 2编码)"""
pt = ""
for i in range(0, len(ct), 4):
off = int(ct[i:i+2], 16)
enc = int(ct[i+2:i+4], 16)
cp = enc - off
pt += C[cp] if 0 <= cp < len(C) else "?"
return pt
N3XT CRYPT0 5TUFF

Text 17

5D2EAF346C9271B7489BBA3A52326752248C2255826771378D741E48205A

密文前半为密钥,后半为编码数据。密钥 - 编码数据 = 字符集索引。

1
2
3
4
5
6
7
def decode_text17(ct, reverse=False):
"""前半 key 后半数据, key - 数据"""
half = len(ct) // 2
keys = [int(ct[i:i+2], 16) for i in range(0, half, 2)]
data = [int(ct[i:i+2], 16) for i in range(half, len(ct), 2)]
pt = "".join(C[keys[i] - data[i]] for i in range(len(data)))
return pt[::-1] if reverse else pt
bazinga he said

Text 18

35445FA0D18F47618981AE5D3A98A5138EAE2A303A5D688B6C4461703B902F308F5F125F7725

与 Text 17 相同算法,但明文在加密前被反转了。

1
2
3
def decode_text18(ct):
"""同 Text 17 但结果反转"""
return decode_text17(ct, reverse=True)
5TL1 9aKu p03z U2a5

tryptodecrypt.com

字符集 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?!(71 个字符)。

Text 7

21052F151200271512413E35101A152F3511

固定 2-hex 替换表(步长 2)。

same script as easy level

1
2
3
❯ python decrypt_cipher.py 7 2
密文: 21052F151200271512413E35101A152F3511
******************
this was confusing

Text 8

eaidagdagenpmgodlceijmgoefodlceijcnllonmgodlcfilfgamgodnnflgfgafilmgofildihdagmgoefodlccnlcnledddagmgoedddagfobdagedd

3 字符一组的替换密码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/usr/bin/env python3
import re
import subprocess
import sys

C = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! "


def encrypt(text_id, text, cookie=""):
cmd = ["curl", "-s"]
if cookie:
cmd += ["-b", cookie]
cmd += [
f"https://www.trytodecrypt.com/decrypt.php?id={text_id}",
"-d",
f"text={text}&encrypt=Encrypt",
]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
# m = re.findall(r"panel-body[^>]>([0-9a-fA-F]+)</div>", r.stdout)
m = re.findall(r"panel-body[^>]*>([0-9a-zA-Z]+)</div>", r.stdout)
return m[1] if len(m) >= 2 else None


def build_mapping(text_id, step, cookie=""):
mapping = {}
for i in range(0, len(C), 20): # 每次 20 字符,多数服务端限制 50
batch = C[i : i + 20]
enc = encrypt(text_id, batch, cookie)
if enc:
for j, ch in enumerate(batch):
mapping[enc[j * step : (j + 1) * step]] = ch
return mapping


def decode(ct, step, mapping):
return "".join(mapping.get(ct[i : i + step], "?") for i in range(0, len(ct), step))


if __name__ == "__main__":
if len(sys.argv) < 3:
print(f"用法: {sys.argv[0]} <text_id> <step>")
sys.exit(1)

text_id = int(sys.argv[1])
step = int(sys.argv[2])

ct = sys.stdin.read().strip() if not sys.stdin.isatty() else input("密文: ").strip()

mapping = build_mapping(text_id, step)
if not mapping:
print("ERROR: 映射表为空,检查 text_id / step / 网络连接")
sys.exit(1)

print(decode(ct, step, mapping))
1
2
3
❯ python decrypt_cipher_alpha.py 8 3
密文: eaidagdagenpmgodlceijmgoefodlceijcnllonmgodlcfilfgamgodnnflgfgafilmgofildihdagmgoefodlccnlcnledddagmgoedddagfobdagedd
***************************************
keep in mind: its just the middle level

Text 9

6224F12C1C3FAA5AA54836B3C446D6415E74

反转输入后,每字符映射到固定 3-hex 码。解码时反向操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/usr/bin/env python3
import re
import subprocess
import sys

C = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! "


def encrypt(text_id, text, cookie=""):
cmd = ["curl", "-s"]
if cookie:
cmd += ["-b", cookie]
cmd += [
f"https://www.trytodecrypt.com/decrypt.php?id={text_id}",
"-d",
f"text={text}&encrypt=Encrypt",
]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
m = re.findall(r"panel-body[^>]*>([0-9a-zA-Z]+)</div>", r.stdout)
return m[1] if len(m) >= 2 else None


def build_mapping(text_id, step, cookie=""):
mapping = {}
# 或者把加密块大小改成 1
for i in range(0, len(C), 20): # 每次 20 字符,多数服务端限制 50
batch = C[i : i + 20]
enc = encrypt(text_id, batch, cookie)
if enc:
for j, ch in enumerate(batch):
# mapping[enc[j * step : (j + 1) * step]] = ch
mapping[enc[-(j + 1) * step : len(enc) - j * step]] = ch
return mapping


def decode(ct, step, mapping):
return "".join(mapping.get(ct[i : i + step], "?") for i in range(0, len(ct), step))


if __name__ == "__main__":
if len(sys.argv) < 3:
print(f"用法: {sys.argv[0]} <text_id> <step>")
sys.exit(1)

text_id = int(sys.argv[1])
step = int(sys.argv[2])

ct = sys.stdin.read().strip() if not sys.stdin.isatty() else input("密文: ").strip()

mapping = build_mapping(text_id, step)
if not mapping:
print("ERROR: 映射表为空,检查 text_id / step / 网络连接")
sys.exit(1)

print(decode(ct, step, mapping))
1
2
3
echo "6224F12C1C3FAA5AA54836B3C446D6415E74" | python trytodecrypt_cipher_reverse.py 9 3 | rev

************
fireball 123

Text 10

261129152E152B

7 -> 9 单表替换 每个字符固定映射到一个密文块,不依赖位置

10 -> 12 多表替换 映射关系随位置变化(Vigenère 风格)

步长 2,偏移量 [16, 17, 18] 循环。enc = charset_pos + offset[pos % 3]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#!/usr/bin/env python3
"""trytodecrypt CPA solver — 基于网站加密工具建映射表解码

三种模式:
simple 固定映射(Text 1-8)
reverse 先反转再加密(Text 9)
vigenere 逐位置映射(Text 10-12)
derive 自动推导公式,少量请求解码(Text 10-12 推荐)

用法:
python trytodecrypt_solve.py <text_id> [options]
echo '密文' | python trytodecrypt_solve.py <text_id> [options]

选项:
-s, --step 步长(默认 auto)
-r, --reverse 反转模式(Text 9)
-v, --vigenere Vigenere 模式(逐位置建表,请求多)
-d, --derive 推导模式(少量请求算公式,Text 10-12 推荐)
-c, --cookie PHPSESSID
-q, --quiet 静默模式
"""

import argparse
import re
import subprocess
import sys
import time

C = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! "


def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)


def encrypt(text_id, text, cookie="", retry=3):
for attempt in range(retry):
cmd = ["curl", "-s", "--max-time", "15", "--retry", "2"]
if cookie:
cmd += ["-b", cookie]
cmd += [
f"https://www.trytodecrypt.com/decrypt.php?id={text_id}",
"-d", f"text={text}&encrypt=Encrypt",
]
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=20)
m = re.findall(r"panel-body[^>]*>([0-9a-zA-Z]+)</div>", r.stdout)
if len(m) >= 2:
return m[1]
except subprocess.TimeoutExpired:
pass
if attempt < retry - 1:
time.sleep(1)
return None


def detect_step(text_id, cookie=""):
for step in [2, 3, 4]:
enc = encrypt(text_id, "0" * 10, cookie)
if enc and len(enc) == step * 10:
return step
enc = encrypt(text_id, "0", cookie)
if enc:
return len(enc)
return 2


def build_mapping_simple(text_id, step, cookie="", batch=20, reverse=False):
mapping = {}
for i in range(0, len(C), batch):
plain = C[i:i + batch]
enc = encrypt(text_id, plain, cookie)
if not enc:
continue
groups = [enc[j * step:(j + 1) * step] for j in range(len(plain))]
if reverse:
groups.reverse()
for j, ch in enumerate(plain):
mapping[groups[j]] = ch
return mapping


def build_mapping_vigenere(text_id, step, ct_len, cookie="", quiet=False):
mapping = [{} for _ in range(ct_len)]
total = ct_len * len(C)
done = 0
t0 = time.time()
for pos in range(ct_len):
prefix = "0" * pos
for ch in C:
plain = prefix + ch
enc = encrypt(text_id, plain, cookie)
if enc:
key = enc[pos * step:(pos + 1) * step]
mapping[pos][key] = ch
done += 1
if not quiet and done % 50 == 0:
elapsed = time.time() - t0
eprint(f" [{done}/{total}] {done/total*100:.0f}% eta {elapsed/done*(total-done):.0f}s")
if not quiet:
eprint(f" [{total}/{total}] 100% ({time.time()-t0:.0f}s)")
return mapping


def derive_vigenere(text_id, step, ct_len, cookie=""):
"""推导公式模式:少量请求算出 key,直接解码"""
# 加密 "0"*n 获取每个位置的基值
enc_zeros = encrypt(text_id, "0" * ct_len, cookie)
if not enc_zeros:
return None
bases = [int(enc_zeros[p * step:(p + 1) * step], 16) for p in range(ct_len)]

# 加密 "1"*n 获取梯子
enc_ones = encrypt(text_id, "1" * ct_len, cookie)
if enc_ones:
steps = [int(enc_ones[p * step:(p + 1) * step], 16) - bases[p] for p in range(ct_len)]
else:
steps = [0] * ct_len

return bases, steps


def decode_with_key(ct, step, bases, steps):
"""用推导出的 key 解码"""
pt = ""
for i in range(len(ct) // step):
val = int(ct[i * step:(i + 1) * step], 16)
# enc = base + cp * step
if steps[i] != 0:
cp = round((val - bases[i]) / steps[i])
else:
cp = val - bases[i]
if 0 <= cp < len(C):
pt += C[cp]
else:
pt += "?"
return pt


def decode_simple(ct, step, mapping):
return "".join(mapping.get(ct[i:i + step], "?") for i in range(0, len(ct), step))


def decode_vigenere(ct, step, mapping):
return "".join(
mapping[i].get(ct[i * step:(i + 1) * step], "?")
for i in range(len(mapping))
)


def main():
ap = argparse.ArgumentParser(description="trytodecrypt CPA solver")
ap.add_argument("text_id", type=int, help="题目 ID")
ap.add_argument("-s", "--step", type=int, default=0, help="步长(默认 auto)")
ap.add_argument("-r", "--reverse", action="store_true", help="反转模式(Text 9)")
ap.add_argument("-v", "--vigenere", action="store_true", help="Vigenere 模式(逐位置建表)")
ap.add_argument("-d", "--derive", action="store_true", help="推导模式(少量请求算公式)")
ap.add_argument("-c", "--cookie", default="", help="PHPSESSID")
ap.add_argument("-q", "--quiet", action="store_true", help="静默模式")
ap.add_argument("--batch", type=int, default=20, help="每批字符数(默认 20,simple 专用)")
args = ap.parse_args()

ct = sys.stdin.read().strip() if not sys.stdin.isatty() else input("密文: ").strip()
if not ct:
eprint("ERROR: 未提供密文")
sys.exit(1)

step = args.step or detect_step(args.text_id, args.cookie)
if not args.quiet:
eprint(f"[*] 步长: {step}")

if args.derive:
ct_len = len(ct) // step
if not args.quiet:
eprint(f"[*] 推导模式: {ct_len} 个位置")
result = derive_vigenere(args.text_id, step, ct_len, args.cookie)
if result is None:
eprint("ERROR: 推导失败")
sys.exit(1)
bases, steps = result
if not args.quiet:
eprint(f"[*] bases: {bases}")
eprint(f"[*] steps: {steps}")
print(decode_with_key(ct, step, bases, steps))

elif args.vigenere:
ct_len = len(ct) // step
if not args.quiet:
eprint(f"[*] Vigenere: {ct_len} × {len(C)} = {ct_len * len(C)} 次请求")
mapping = build_mapping_vigenere(args.text_id, step, ct_len, args.cookie, args.quiet)
if not mapping[0]:
eprint("ERROR: 映射表为空")
sys.exit(1)
print(decode_vigenere(ct, step, mapping))

else:
mode = "reverse" if args.reverse else "simple"
if not args.quiet:
eprint(f"[*] 模式: {mode}")
mapping = build_mapping_simple(args.text_id, step, args.cookie, args.batch, args.reverse)
if not mapping:
eprint("ERROR: 映射表为空")
sys.exit(1)
print(decode_simple(ct, step, mapping))


if __name__ == "__main__":
main()
1
2
3
4
5
6
7
❯ python trytodecrypt_solve.py -v 10 -c "8rubc5nmtqala9gvdj8t7cigqn" -s 2 -d
密文: 261129152E152B
[*] 步长: 2
[*] 推导模式: 7 个位置
[*] bases: [16, 17, 18, 16, 17, 18, 16]
[*] steps: [1, 1, 1, 1, 1, 1, 1]
*******
m0n5t3r

Text 11

3785824AD56B2531A7150DF44C21434A61E63F040A42F2012BC2F43F0AD535D24D46013213866D7E0

步长 3 hex,6 位循环。基值 [168, 282, 567, 57, 245, 180],乘数 [12, 47, 21, 19, 35, 9]

密文值 = 基值[位置] + 字符位置 × 倍率[位置]

same as Text 10

1
2
3
4
5
6
7
❯ python trytodecrypt_solve.py -v 11 -c "8rubc5nmtqala9gvdj8t7cigqn" -s 3 -d
密文: 3785824AD56B2531A7150DF44C21434A61E63F040A42F2012BC2F43F0AD535D24D46013213866D7E0
[*] 步长: 3
[*] 推导模式: 27 个位置
[*] bases: [168, 282, 567, 57, 245, 180, 168, 282, 567, 57, 245, 180, 168, 282, 567, 57, 245, 180, 168, 282, 567, 57, 245, 180, 168, 282, 567]
[*] steps: [12, 47, 21, 19, 35, 9, 12, 47, 21, 19, 35, 9, 12, 47, 21, 19, 35, 9, 12, 47, 21, 19, 35, 9, 12, 47, 21]
***************************
You are very good. Respect!

Text 12

00D02703603C0450461340870A50B50EA10A0BD133

基值为三角数 T(pos+1) = (pos+1)(pos+2)/2,步长 (pos+2)/2。

1
2
3
4
C = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! "
ct = "00D02703603C0450461340870A50B50EA10A0BD133"
vals = [int(ct[i*3:(i+1)*3], 16) for i in range(14)]
print("".join(C[round((v - (i+1)*(i+2)//2) / ((i+2)/2))] for i, v in enumerate(vals)))
cookie monster

tryptodecrypt.com

字符集 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?!(71 个字符)。

easy 都是简单替换密码。

网站给一个加密工具,输入明文返回密文。脚本就利用这个 oracle 做 Chosen-Plaintext Attack。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import subprocess, re, sys
C = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! "

def encrypt(text_id, text, cookie=""):
cmd = ["curl", "-s"]
if cookie:
cmd += ["-b", cookie]
cmd += [f"https://www.trytodecrypt.com/decrypt.php?id={text_id}",
"-d", f"text={text}&encrypt=Encrypt"]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
m = re.findall(r"panel-body[^>]*>([0-9a-fA-F]+)</div>", r.stdout)
return m[1] if len(m) >= 2 else None

def build_mapping(text_id, step, cookie=""):
mapping = {}
for i in range(0, len(C), 20):
batch = C[i:i+20]
enc = encrypt(text_id, batch, cookie)
if enc:
for j, ch in enumerate(batch):
mapping[enc[j*step:(j+1)*step]] = ch
return mapping

def decode(ct, step, mapping):
return "".join(mapping.get(ct[i:i+step], "?") for i in range(0, len(ct), step))

if __name__ == "__main__":
text_id, step = int(sys.argv[1]), int(sys.argv[2])
ct = sys.stdin.read().strip() if not sys.stdin.isatty() else input("密文: ").strip()
mapping = build_mapping(text_id, step)
print(decode(ct, step, mapping))

Text 1

131017171A48221A1D170F

偏移 2。

1
2
3
4
5
6
7
C = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! "
OFF = 2
def dec(s):
r = []
for i in range(0, len(s), 2):
r.append(C[int(s[i:i+2], 16) - OFF])
return "".join(r)
hello world

Text 2

4A3E374A4973483F3D3E4A

偏移 42(ASCII 表偏移)。

1
2
3
4
5
6
C = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! "
def dec(s, OFF=42):
r = []
for i in range(0, len(s), 2):
r.append(C[int(s[i:i+2], 16) - OFF])
return "".join(r)
thats right

Text 3

0A0B1339150B1139070A0B13390510

偏移 -13(58 mod 71)。hex 值小于 13 时直接减会负索引,需要 % 71

1
2
3
4
5
6
C = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! "
def dec(s, OFF=-13):
r = []
for i in range(0, len(s), 2):
r.append(C[(int(s[i:i+2], 16) - OFF) % len(C)])
return "".join(r)
now you know it

Text 4

0C02D8010D0C02D8010606D8101402FCD80F0603D8FC0600DA

enc = (30 - charset_pos) % 256

1
2
3
$ python script.py 4 2
密文: 0C02D8010D0C02D8010606D8101402FCD80F0603D8FC0600DA
****
is this too easy for you?

Text 5

90DE633F425148DE51546CDE725466DE3F2A6936DE4263CCDEAB362A3372DE39545DDE633F36DE51366F63DE545136D8

enc = charset_pos * 3 + 12

1
2
3
$ python script.py 5 2
密文: 90DE633F425148DE51546CDE725466DE3F2A6936DE4263CCDEAB362A3372DE39545DDE633F36DE51366F63DE545136D8
************************************************
I think now you have it. Ready for the next one?

Text 6

4D586CFC2DB449D47B0CF99C3BC46CFC7B0C

固定 4-hex 替换表。

1
2
3
$ python script.py 6 4
密文: 4D586CFC2DB449D47B0CF99C3BC46CFC7B0C
*********
lucky guy
+ + +
SYSTEM STATUS: ACTIVE ENCRYPTED SECTOR 7 PRTS_TERMINAL_V2.0 PROTOCOL: 0x2A ENCRYPTED DATA STREAM SYSTEM: ONLINE