WeChall - Snake

Challenge

WeChall Snake(by Gizmore and lazer, 2010):Java Applet 贪吃蛇游戏,目标 "reach exactly 300000 points"。没有答案提交框——玩家在 Applet 里玩完游戏,客户端把成绩加密提交到 CGI_Highscore.php,完成判定完全靠服务端校验提交构型是否"真实可达成"。

1
https://www.wechall.net/en/challenge/snake/index.php

Solution

CGI_Highscore.php 发送加密的 HT_RATE + HT_INSERT 请求,INSERT 一个完全自洽的 300000 分构型:

  1. GET 挑战页(记录游戏开始时间)
  2. 等待 ≥ 300 秒
  3. HT_RATE score=300000(返回 rank,写 trixx.txt)
  4. HT_INSERT score=300000(7 秒内):
    • K=10(字母轮数)→ M = 1.0 + 0.5K = 6.00
    • B=250, C=250, H=0 → L = 14 + 4B + 6C - 2H + 5K = 2564
    • md5 = MD5("250:250:0:2564:300000").upper()
    • name=<你的用户名> & sessid=

分析过程

1. 协议逆向

Applet(snake.jar)与 CGI_Highscore.php 的通信:

  • body = encrypted=URLEncode(GWF_Crypt(payload, "Snake$Poors!"))
  • GWF_Crypt:out[i] = key[ki % len] ^ c ^ 101;key 索引按递增步长循环(每轮 wrap 后步长 +1,越过 keylen 复位为 1)
  • payload:cmd=HT_INSERT&score=&name=&length=&hemps=&cherries=&bananas=&multi=&md5=&sessid=
  • md5 = MD5("bananas:cherries:hemps:length:score").toUpperCase()
  • 命令:HT_RATE(评分+写 trixx)、HT_INSERT(写榜)、HT_HOTD(英雄日)、HT_UNREQUEST

2. 服务端规则

snake.gizmore.org 原站 PHP 短标签未开启 → 源码直接泄露,可与线上行为交叉验证。

1
2
3
HT_Rate():  rank = 分数在榜排名;rank ≤ 100 时把 "time:score" 写入 trixx.txt
HT_InsertAllowed(): INSERT 的 score 必须 7 秒内被 RATE 过(完全相同的分数)
→ RATE 和 INSERT 必须成对、同分、间隔 < 7 秒

时间检测:除 RATE/INSERT 窗口外,还有最小游戏时长检查——从 HT_HOTDHT_RATE 的间隔必须达到一定时长,且随分数增大

3. 计分规则(从 applet 字节码逆向)

  • banana=250 分/+4 长、cherry=750/+6、hemp=500/-2、字母=100/+1
  • multiplier = 1.0 + 0.5×K(K = 集齐 5 个字母食物的轮数)
  • 字母分合计 A(K) = 500K + 125K(K-1)
  • 死亡动画 effectDie() 每帧 score += 10×L
  • 蛇长 L = 14 + 4B + 6C − 2H + 5K
  • 得分可达区间 [10L + A + Σ, 10L + A + M×Σ],Σ = 250B + 750C + 500H

线上版 CGI 比仓库版多了反作弊:分数 ≥ 300000 的记录会被标记 user_thief.png(Cheater)图标,且不会触发 solved。检测本质是 "golden ratio" 比例检查(M 与 K 自洽、计数合理性),不查 10 整除性、不做严格代数校验。被标记的构型共同点:

  • M 与 K 不自洽(如 K=46 但提交 M=20.00,应为 24.0)
  • 食物计数离谱(H=394、C=319 —— 真实游戏不可能)
  • score 超出构型可达区间(失败构型的共同点;服务端给检测留了 margin,但偏离太多照样标)

Script

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
#!/usr/bin/env python3
import hashlib, time, urllib.parse, urllib.request

NAME = "<your_wechall_username>" # 你的 WeChall 用户名
SESS = "<your_session_cookie>" # WC cookie 值
BASE = "https://www.wechall.net/en/challenge/snake"
UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120 Safari/537.36"

def gwf_crypt(data, key):
"""GWF_Crypt: out[i] = key[ki % len] ^ c ^ 101;key 索引按递增步长循环(wrap 后步长 +1)"""
keylen = len(key); out = []; step = 1; ki = -1
for c in data:
ki += step
if ki >= keylen: ki = 0; step += 1
if step >= keylen: step = 1
out.append(chr(ord(key[ki % keylen]) ^ ord(c) ^ 101))
return "".join(out)

def post(msg):
"""POST 加密 payload 到 CGI_Highscore.php"""
body = "encrypted=" + urllib.parse.quote_plus(gwf_crypt(msg, "Snake$Poors!"))
req = urllib.request.Request(BASE + "/CGI_Highscore.php", data=body.encode(),
headers={"User-Agent": UA, "Content-Type": "application/x-www-form-urlencoded",
"Referer": BASE + "/index.php", "Cookie": "WC=" + SESS})
with urllib.request.urlopen(req, timeout=25) as res:
return res.status, res.read().decode("utf-8", "replace")

def get(url):
"""GET 页面(带 session cookie)"""
req = urllib.request.Request(url, headers={"User-Agent": UA, "Cookie": "WC=" + SESS})
with urllib.request.urlopen(req, timeout=25) as res:
return res.read().decode("utf-8", "replace")

K = 10 # 集齐 5 个字母食物的轮数
B, C, H = 250, 250, 0 # banana / cherry / hemp 数量
SC = 300000
L = 14 + 4*B + 6*C - 2*H + 5*K # 蛇长
M = "%.2f" % (1.0 + 0.5*K) # multiplier
A = 500*K + 125*K*(K-1) # 字母分合计
SIGMA = 250*B + 750*C + 500*H # 食物分合计
mn, mx = 10*L + A + SIGMA, 10*L + A + (1.0+0.5*K)*SIGMA
assert mn <= SC <= mx, f"score {SC} not in [{mn}, {mx}]"
md5 = hashlib.md5(f"{B}:{C}:{H}:{L}:{SC}".encode()).hexdigest().upper()

print("step 1: GET 挑战页(会话计时开始)")
get(BASE + "/index.php")
print("step 2: 等待 300 秒(90 秒实验失败过,建议 ≥5 分钟)")
time.sleep(300)
print("step 3: HT_RATE(写 trixx.txt,7 秒窗口起点)")
r = post(f"cmd=HT_RATE&score={SC}&sessid={SESS}")
print("RATE:", r[0], repr(r[1][:30]))
time.sleep(2)
print("step 4: HT_INSERT(同分,7 秒内)")
r = post(f"cmd=HT_INSERT&score={SC}&name={NAME}&length={L}&hemps={H}"
f"&cherries={C}&bananas={B}&multi={M}&md5={md5}&sessid={SESS}")
print("INSERT:", r[0], repr(r[1][:80]))
if "Very well done" in r[1]:
print("SUCCESS - challenge solved!")
else:
print("NOT solved yet - response above")