某站用了 PoW(Proof of Work)机制来防止自动签到。本来想着简单的 HTTP 请求就能搞定,结果发现还要算哈希。研究了下它的实现,用 Python 复现了一遍,记录一下过程。
背景
PoW 的核心思想很简单:服务器给你一道难题,你必须做一定的计算工作才能解决。计算成本提高了,自动化脚本就不划算了。
常见的 PoW 实现(比如比特币)要求哈希结果有特定数量的前导零。这个站也是这么干的。
抓包分析
打开浏览器开发者工具,观察签到流程:
1
| GET /api/pow/challenge?tier=4&hps=3
|
响应:
1 2 3 4 5 6 7
| { "challengeId": "d10938eb-0c02-49c4-a5ac-c67c5faf860a", "challenge": "8414fee4-8f21-40dc-b503-73a9f9205c96", "difficulty": 9, "targetBits": 9, "targetSeconds": 200 }
|
提交的时候:
1 2 3 4 5 6
| POST /api/pow/submit { "challengeId": "d10938eb-0c02-49c4-a5ac-c67c5faf860a", "nonce": 223, "tier": 4 }
|
问题来了:这个 nonce 是怎么算出来的?
WASM 文件
在 Network 面板里看到一个 pow.wasm 文件,下载下来看看。WASM 是 WebAssembly,一种可以在浏览器里运行的高效字节码。
用 wabt 工具反编译(或者直接看 JS 里的调用),发现导出了几个函数:
1 2 3 4
| memory: 线性内存 alloc(size): 分配内存 dealloc(ptr, size): 释放内存 hash_with_nonce(challenge_ptr, challenge_len, nonce, out_ptr): 计算哈希
|
核心就是 hash_with_nonce,输入 challenge 和 nonce,输出哈希结果的前导零数量。
PoW 原理
先说清楚几个概念:
Challenge(挑战)
服务器每次生成的随机字符串:
1
| "challenge": "8414fee4-8f21-40dc-b503-73a9f9205c96"
|
每次签到都不一样,这是关键。如果 challenge 不变,算一次就能复用,那就没意义了。
Difficulty(难度)
要求哈希结果有多少个前导零:
SHA-256 输出 64 个十六进制字符。十六进制每个字符是 0-f(16种可能),是 0 的概率是 1/16。
要得到 n 个前导零,概率是 (1/16)^n:
| 难度 |
预期尝试次数 |
| 1 |
~16 次 |
| 2 |
~256 次 |
| 9 |
~68.7 亿次 |
| 16 |
~2^64 次 |
所以 difficulty 每增加 1,计算量翻倍 16 倍。
Nonce(随机数)
客户端要找到一个数值,使得 SHA256(challenge + nonce) 的结果至少有 difficulty 个前导零。
只能暴力尝试:
1 2 3 4
| nonce=0: SHA256("8414fee4...0") = "a1b2c3d4..." → leading=0 ❌ nonce=1: SHA256("8414fee4...1") = "f7e8d9c0..." → leading=0 ❌ ... nonce=223: SHA256("8414fee4...223") = "000000001abc123..." → leading=9 ✅
|
找到 nonce=223 后提交给服务器验证。
为什么每次都要重新算?
这是很多人容易误解的点。
虽然 difficulty 是固定的(比如 9),但 challenge 每次都不同,所以需要的 nonce 也不同:
| 签到 |
Challenge |
Nonce |
| 第1次 |
8414fee4-... |
223 |
| 第2次 |
a7f3d2e1-... |
512 |
| 第3次 |
f9e8d7c6-... |
89 |
同样的 nonce=223,在不同 challenge 下哈希结果完全不同:
1 2
| SHA256("8414fee4..." + "223") = "005866b7..." → leading=2 SHA256("a7f3d2e1..." + "223") = "d4c3b2a1..." → leading=0
|
这就是 PoW 的核心设计:每次都要付出计算工作,无法缓存或复用。
Python 实现
加载 WASM
用 wasmtime 库加载 WASM 模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| from wasmtime import Engine, Store, Module, Instance, Memory, Func
wasm_bytes = pathlib.Path("pow.wasm").read_bytes() engine = Engine() store = Store(engine) module = Module(engine, wasm_bytes) instance = Instance(store, module, [])
exports = instance.exports(store) memory = exports.get("memory") alloc_func = exports.get("alloc") dealloc_func = exports.get("dealloc") hash_func = exports.get("hash_with_nonce")
|
内存管理
WASM 用线性内存,需要手动管理:
1 2 3 4 5 6 7 8 9 10 11 12 13
| def write_to_memory(data: bytes, ptr: int): """写入数据到WASM内存""" mem = memory.data_ptr(store) for i in range(len(data)): mem[ptr + i] = data[i]
def read_from_memory(ptr: int, size: int) -> bytes: """从WASM内存读取数据""" mem = memory.data_ptr(store) result = bytearray(size) for i in range(size): result[i] = mem[ptr + i] return bytes(result)
|
计算哈希
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| def hash_with_nonce(challenge: str, nonce: int) -> Tuple[int, str]: """计算 SHA-256 哈希,返回 (前导零数量, 哈希值)""" challenge_bytes = challenge.encode('utf-8') challenge_ptr = alloc_func(store, len(challenge_bytes)) out_ptr = alloc_func(store, 32)
try: write_to_memory(challenge_bytes, challenge_ptr)
leading = hash_func(store, challenge_ptr, len(challenge_bytes), nonce, out_ptr)
hash_bytes = read_from_memory(out_ptr, 32) hash_hex = hash_bytes.hex()
return leading, hash_hex finally: dealloc_func(store, challenge_ptr, len(challenge_bytes)) dealloc_func(store, out_ptr, 32)
|
暴力搜索 Nonce
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| def find_nonce(challenge: str, difficulty: int) -> Optional[int]: """查找满足条件的 nonce""" challenge_bytes = challenge.encode('utf-8') challenge_ptr = alloc_func(store, len(challenge_bytes)) out_ptr = alloc_func(store, 32)
try: write_to_memory(challenge_bytes, challenge_ptr)
nonce = 0 while True: leading = hash_func(store, challenge_ptr, len(challenge_bytes), nonce, out_ptr)
if leading >= difficulty: return nonce
nonce += 1
if nonce % 10000 == 0: time.sleep(0.001) finally: dealloc_func(store, challenge_ptr, len(challenge_bytes)) dealloc_func(store, out_ptr, 32)
|
算力校准
服务器要求提供设备算力(H/s,Hashes per second),用来调整难度:
1 2 3 4 5 6 7 8 9 10 11 12
| def benchmark(duration_ms: int = 1200) -> int: """测速,返回 H/s""" start_time = time.perf_counter() nonce = 0
while (time.perf_counter() - start_time) * 1000 < duration_ms: hash_func(store, challenge_ptr, len(challenge_bytes), nonce, out_ptr) nonce += 1
elapsed_ms = (time.perf_counter() - start_time) * 1000 hps = int((nonce / elapsed_ms) * 1000) return hps
|
完整签到流程
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
| import requests
class PowSignInClient: def __init__(self, base_url: str, session: requests.Session): self.base_url = base_url.rstrip('/') self.session = session
def get_challenge(self, tier_id: int, hps: int): """获取 PoW 挑战""" params = f"tier={tier_id}&hps={hps}" url = f"{self.base_url}/api/pow/challenge?{params}" return self.session.get(url).json()
def submit_pow(self, challenge_id: str, nonce: int, tier: int): """提交签到""" payload = { "challengeId": challenge_id, "nonce": nonce, "tier": tier } url = f"{self.base_url}/api/pow/submit" return self.session.post(url, json=payload).json()
session = requests.Session() session.cookies.set("sid", "your-cookie-here")
client = PowSignInClient("https://target.com", session)
hps = benchmark(duration_ms=600) print(f"设备算力: {hps} H/s")
challenge_data = client.get_challenge(tier_id=4, hps=hps) print(f"Challenge: {challenge_data['challenge']}") print(f"Difficulty: {challenge_data['difficulty']}")
nonce = find_nonce(challenge_data['challenge'], challenge_data['difficulty']) print(f"找到 Nonce: {nonce}")
result = client.submit_pow(challenge_data['challengeId'], nonce, 4) print(f"签到结果: {result}")
|
运行效果
1 2 3 4 5 6 7 8
| $ python pow_sign.py auto --cookie "sid=xxx" [Auto] 设备算力: 150,000 H/s [Auto] Challenge: 8414fee4-8f21-40dc-b503-73a9f9205c96 [Auto] Difficulty: 9 [Auto] 预计耗时: ~4.5s [Auto] 找到 Nonce: 223 [Auto] Hash: 00000000123456789abcdef012345678... [Auto] 签到成功! 奖励: 17.56
|
总结
PoW 机制确实是防止自动化的有效手段:
- 计算成本高 - 每次签到需要几秒甚至几十秒的哈希计算
- 无法复用 - challenge 每次随机,nonce 必须重新计算
- 容易验证 - 服务器收到 nonce 后算一次哈希就能验证
但也给了逆向的空间:
- WASM 可以分析 - 导出函数清晰,逻辑透明
- 可以用 Python 复现 - wasmtime 完美支持 WASM
- 算力可以优化 - 多线程、GPU 加速都能提速
安全性和可逆性之间,永远是博弈。
Reference