本帖最后由 知乐 于 2026-9-5 14:11 编辑
本帖最后由 知乐 于 2026-9-4 20:21 编辑
悬赏论坛求助的魔兽ce【拒绝做秀图大师】
没啥技术含量,顺便帮论坛 朋友解决一个问题,因为我也是新手过来的,如果正好我能搞定就写文章,
开始我想看看它的验证体系到底做了什么,评论区很多秀图大佬,于是我相信我也能,然后花了一个下午,从解包到伪造整个认证服务端,全程走了一遍。
这篇文章把关键的原始代码、我的观察、和具体的修改方法都贴出来。
拒绝做秀图大师,拒绝做秀图大师,拒绝做秀图大师
重要的事情说三遍!!
原帖链接:https://www.52hb.com/thread-62316-1-1.html
一、第一眼:小壳大肚子
PE 头先看一眼。有意思的是,这个 exe 的本体只有 300KB:
Sections: (6 个)
.text va=0x00001000 raw=0x00000400 X/R
.rdata va=0x0002b000 raw=0x00029600 R
.data va=0x0003a000 raw=0x00037c00 R/W
.fptable va=0x0003f000 raw=0x00038800 R/W
.rsrc va=0x00040000 raw=0x00038a00 R
.reloc va=0x00050000 raw=0x00048000 R
最后一个节区结束于 0x4A000
文件大小: 0x2086965 (34MB)
Overlay: 33,802,597 字节
300KB 的代码带 33MB 的 overlay,而且 overlay 开头是 78 DA——zlib 最高压缩级别的魔数。这种"小壳大肚子"基本就是 PyInstaller 单文件模式。翻到文件尾部确认,MEI\x0c\x0b\x0a\x0b\x0e 的 cookie 在偏移 0x208690D:
lengthofPackage: 33802597
toc offset: 33734173 tocLen: 68336
python ver: 308 pylib: python38.dll
Python 3.8、32 位。CArchive 的 TOC 一共 1181 个条目,写个脚本全倒出来,大部分是常规库:PIL、numpy、wxPython、CustomTkinter、cryptography、okhttp……还有个 com.mysql.jdbc——Windows 游戏修改器里塞 MySQL 的 JDBC 驱动,挺特别,后面看到它的 Lua 脚本可以直连数据库,就明白了。
排除掉所有库文件,真正值得看的是两个非标准条目:
secure_boot(类型 s,脚本,5528 字节解压后)
secure_payload.bin(类型 b,100755 字节压缩 → 100714 字节原始)
PYZ 归档里另外倒出 734 个 .pyc,全是第三方库。主程序的业务代码一个字节都不在里面。也就是说作者自己写了一套代码保护:真正的逻辑全部加密在 secure_payload.bin 里,运行时解密、内存加载、永不落盘。
二、secure_boot:整个加密方案的自白
CArchive 里类型为 s 的脚本是裸 marshal 数据(没有 pyc 头),补上 Python 3.8 的 magic(55 0D 0D 0A)之后用 decompyle3 反编译,secure_boot 直接还原成源码。整个加密方案就写在里面。
密钥派生——4 段硬编码常量逐字节 XOR:
_KEY_PARTS = [
b']\x01d\x9b{\x85\x03\xa7\xff-\x05.\xe3\x9d\xef\x12\x88\xeez"b\x12\xd2\x0c\xb1\xddk\x01x%k\x0f',
b'MRq\xb34\xc8\xed\xf4z\xe84\x06\xd7D\r\x8an\xf0\x8e\n\x05}\x0e\xbf\xed\x17\xac\x94\xda\xdb\x95\xc5',
b'\xaaZ\x0e\xcb\xd1\xa3\x91\xa1\x8d\xa9S\x95s\xe11\xbc9N\xe0B\xa9\xfa\x1d\\\x0e\xd7\xf1\x9d\x95D\xd3,',
b'\xc1\xcc\xaa\x80\x05F\\n\x0c2\xad\x97a,\xcd\t\xf6\xcav-\x0f\xfe\xfd*D\x81m\xba=WdH'
]
def _key():
k = bytearray(_KEY_PARTS[0])
for p in _KEY_PARTS[1:]:
for (i, b) in enumerate(p):
k[i] ^= b
return bytes(k)
XOR 完得到 AES-256-GCM 的 key:7bc5b1639ba8239c045ecf2a26141e2d299a6247c16b3cc5169c5bb20aed49ae。没有 KDF,没有环境绑定,密钥和密文躺在同一个文件里。
载荷加载逻辑:
def _load_map():
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
path = _payload_path()
data = open(path, "rb").read()
if not data.startswith(b'W3CE1'): # 魔法
raise RuntimeError("payload broken: " + path)
count = struct.unpack_from("<I", data, 5)[0] # 模块数
expect = data[9:41] # 32 字节 SHA-256
body = data[41:]
import hashlib
if hashlib.sha256(body).digest() != expect:
raise RuntimeError("payload checksum")
aes = AESGCM(_key())
pos = 0
out = {}
for _ in range(count):
nlen = struct.unpack_from("<H", body, pos)[0]; pos += 2
name = body[pos:pos + nlen].decode("utf-8"); pos += nlen
nonce = body[pos:pos + 12]; pos += 12
blen = struct.unpack_from("<I", body, pos)[0]; pos += 4
blob = body[pos:pos + blen]; pos += blen
raw = aes.decrypt(nonce, blob, name.encode("utf-8")) # AAD = 模块名
out[name] = marshal.loads(zlib.decompress(raw))
return out
于是 W3CE1 载荷的完整格式就清楚了:
偏移 大小 内容
0 5 b'W3CE1'
5 4 <I 模块数 count
9 32 SHA-256(body) 完整性校验
41 ... body,重复 count 次:
<H 名字长度
名字 (utf-8)
12 字节 nonce
<I 密文长度
AESGCM 密文(AAD = 模块名,明文 = zlib(marshal 代码对象))
模块加载用的是自定义的 MetaPathFinder, hook 进 sys.meta_path,代码永远只在内存里 exec,不写 .pyc:
class _EncFinder(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path, target=None):
code = self.mapping.get(fullname)
if code is None:
return
loader = _EncLoader(fullname, code)
return importlib.util.spec_from_loader(fullname, loader, ...)
def bootstrap():
_MAP = _load_map()
sys.meta_path.insert(0, _EncFinder(_MAP))
mod = importlib.import_module(_ENTRY) # _ENTRY = "launcher"
设计思路是清楚的:防落盘、防 import 扫描、AES-GCM 带关联数据防篡改、SHA-256 防替换。但对静态分析来说,这些防护等价于把加密算法和密钥用明文写在了启动器里。
三、20 个模块全部出炉
照着格式写解密脚本,一次性全出:
launcher 2,401 B ← 入口
main 76,260 B ← 主程序
auth_guard 28,651 B ← 认证核心
ce_auth 3,962 B
crypto_utils 2,527 B
config_enc 3,037 B
threat_detect 2,930 B
memory_engine 20,897 B
pointer_engine 17,786 B
mix.war3_bridge 12,922 B
mix.attr_controller 9,658 B
mix.mod_scripts 6,936 B
hotkey_manager 6,546 B
...(共 20 个)
decompyle3 全部反编译成功(只有两处局部 parse error,用 xdis 反汇编字节码补齐)。
四、config_enc:三段密文和它的 pepper
config_enc.py 存着验证服务器的配置,是三段 base64 密文:
_CONFIG_SALT = b'KK_AUTH_V2_2026'
_CONFIG_PEPPER = "kk_cfg_pepper_change_me" # ← 模板默认值,没改
_ENC_SERVER = "rBPVIr4dbt_lte8u1Lj3WsA5ZfjmpHHxVA=="
_ENC_APP_KEY = "823KIqM_1Nw5bICED1Hj"
_ENC_SECRET = "ryjsn6GTbQWJvwOAnSEi_t3cDTZuGHFQvifWGbCQKpavK-yV9pNuAYO7UYedJXb_09gMPDxMdgu-f9Aa6cN2lQ=="
def _derive_key(extra=''):
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32,
salt=_CONFIG_SALT, iterations=150000)
return kdf.derive((_CONFIG_PEPPER + extra).encode("utf-8"))
def _xor_stream(data, key):
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
def decrypt_str(cipher, slot):
return _xor_stream(base64.urlsafe_b64decode(cipher), _derive_key(slot)).decode()
解密就是 PBKDF2 派生密钥后做 XOR 流。跑一下:
get_server_url() → http://122.51.113.41:7788
get_app_key() → APP_DEFAULT_001
get_secret_key() → 80e91eba9ea4f4eb7ab3d1d8a931aad283e3feae3a33f01c9ec96eccaa528281
15 万次迭代的 PBKDF2 看着挺唬人,但 pepper 是 kk_cfg_pepper_change_me——作者发工具的时候忘了改模板默认值。而 SecretKey 直接嵌在客户端里,这意味着 HMAC 签名体系对持有 exe 的任何人都是透明的。
五、认证协议还原
auth_guard.py 里的请求构造和签名:
def _signed_post(path, parts, extra=None):
secret = get_secret_key()
ts = str(int(time.time()))
nonce = gen_nonce() # secrets.token_hex(16)
sign = make_request_sign(secret, ts, nonce, *parts)
payload = {'app_key': get_app_key(), 'timestamp': ts,
'nonce': nonce, 'sign': sign}
if extra:
payload.update(extra)
resp = _post(path, payload) # urllib POST JSON
if resp.get("code") == 0:
if not verify_response_sign(secret, resp):
return {'code': 401, 'msg': "签名校验失败"}
return resp
crypto_utils 里的签名算法:
def canonical_json(data):
return json.dumps(data or {}, sort_keys=True, ensure_ascii=False, separators=(',', ':'))
def make_request_sign(secret, timestamp, nonce, *parts):
return hmac_sha256_hex(secret, timestamp, nonce, *parts)
def make_response_sign(secret, timestamp, code, data):
body = canonical_json(data)
return hmac_sha256_hex(secret, timestamp, str(code), body)
登录请求的构造:
def login(self, card_key):
dev = get_device_fingerprint() # wmic 采集 CPU/硬盘/主板 + getmac + MachineGuid
self._fingerprint = dev["fingerprint"] # → SHA-256
(threat_flag, threat_tools) = get_threat_report()
resp = _signed_post(f"{API_PREFIX}/login",
(get_app_key(), card_key, dev["fingerprint"], threat_flag),
{'card_key': card_key, 'fingerprint': dev["fingerprint"],
'device_info': dev, 'threat_flag': threat_flag, 'threat_tools': threat_tools})
会话保持是双保险:心跳挑战应答 + 本地会话证明。
# 心跳: HMAC(secret, token, challenge, "heartbeat_v2"),60 秒一次
def make_challenge_response(secret, token, challenge):
return hmac_sha256_hex(secret, token, challenge, "heartbeat_v2")
# 会话证明: HMAC(secret, token, fingerprint, expire_tag, "session_proof_v2")
def verify_session_proof(secret, token, fingerprint, expire_tag, proof):
expected = hmac_sha256_hex(secret, token, fingerprint, expire_tag, "session_proof_v2")
return hmac.compare_digest(expected, proof.lower())
还有一个 threat_detect 每 8 秒扫一次进程名,黑名单 40 多个:cheatengine、ollydbg、x64dbg、ida、windbg、processhacker、fiddler、charles、wireshark、dnspy、scylla、pe-sieve……命中就上报封号。
这里有个值得记的细节:verify_response_sign 反编译残缺(decompyle3 在函数中间断了),我用 xdis 直接反汇编字节码,从常量表看到:
Constants: 'code', 0, 'data', 'sign_ts', '', 'sign'
Names: get, isinstance, dict, str, lower, abs, int, time,
ValueError, make_response_sign, hmac, compare_digest
'sign_ts'——响应里的时间戳字段不叫 timestamp 而叫 sign_ts,还有 abs/int/time 说明有时钟偏移校验。还原出来:
def verify_response_sign(secret, resp, max_skew=300):
code = resp.get("code")
if code != 0: return False
data = resp.get("data")
if not isinstance(data, dict): return False
ts = str(resp.get("sign_ts") or "")
if abs(int(time.time()) - int(ts)) > max_skew: return False
sig = (resp.get("sign") or "").lower()
expected = make_response_sign(secret, ts, code, data)
return hmac.compare_digest(expected, sig)
这个字段名如果猜错,响应签名校验永远失败——客户端会报"签名校验失败"。字节码的常量表不会骗人。
六、mock 服务器:把协议倒过来实现
服务端怎么验签名,我就怎么造响应。核心就一个函数:
SECRET = "80e91eba9ea4f4eb7ab3d1d8a931aad283e3feae3a33f01c9ec96eccaa528281"
def make_resp(code, msg, data):
ts = str(int(time.time()))
return {"code": code, "msg": msg, "data": data,
"sign_ts": ts, "sign": hmac_sha256_hex(SECRET, ts, str(code),
canonical_json(data))}
/login 随便发 token,/client_assets 下发 Fernet 加密的功能资源(wire key 由登录响应给出,PBKDF2 派生时用的盐 b'kk_client_protected_v2' 也是从反编译里拿的):
def fernet_for(asset_key):
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32,
salt=b'kk_client_protected_v2', iterations=120000)
return Fernet(base64.urlsafe_b64encode(kdf.derive(asset_key.encode())))
assets = {"mix_config": {"ini_text": "..."}, "ce_features": {"connect": True, ...}}
enc = fernet_for(asset_key).encrypt(json.dumps(assets).encode())
然后写了个客户端模拟器,按 auth_guard 的逻辑发签名请求做回归,16 项检查全过:请求签名被 mock 验证通过(证明 SecretKey 解得对)、响应签名被模拟器验证通过(证明 mock 的应答格式和客户端校验完全一致)、Fernet 资源能解、心跳 challenge 轮换正常、错误 challenge 应答被拒 401。
七、重打包:把服务器地址改掉
客户端的流量要引到本地,得改 config_enc 里那段密文。我不想动 marshal 字节码结构,只做字符串常量的原地替换。
原密文(36 字符,对应明文 25 字节):
rBPVIr4dbt_lte8u1Lj3WsA5ZfjmpHHxVA==
用同一个 derive("server") 密钥加密新地址 http://127.0.0.1:7788(21 字节 → 28 字符):
rBPVIr4dbt_lsO8ry6boWskgZvHk
marshal 里 ASCII 字符串的存储是「类型字节 + 1 字节长度 + 内容」,所以替换时把长度字节从 0x24 改成 0x1C,后面的内容整体前移 8 字节,marshal 结构依然合法。在 config_enc 的 marshal blob 里搜到旧密文在偏移 385,确认类型字节后原地替换。
然后重建整个交付链:
- 20 个模块 → patch 后的 config_enc → 逐个 zlib 压缩 → AES-GCM 重加密(新 nonce,AAD 不变)
- 重算 body 的 SHA-256,拼出新的 W3CE1 载荷(100714 → 100706 字节)
- zlib 压缩,替换 CArchive 里
secure_payload.bin 条目的数据
- 重排 TOC:所有条目的
entryPos 按新偏移重算,cookie 里 lengthofPackage / toc / tocLen 重写
新 exe 和原版只差 8 个字节的大小。双击运行:
[mock] GET /api/v2/version ← 版本检查,匹配 1.7,不强制更新
[mock] POST /login key=MOCKKEY1*** sign_ok=True
[mock] POST /client_assets token=813b...
[mock] POST /heartbeat cr_ok=True
登录窗口输入一个随便编的 32 位 Key,直接成功——「登录成功,到期时间 2099-12-31」,功能开关全部下发。之后每 60 秒的心跳都在跟我的假服务器对话。
八、复盘
这套防护的投入是真不小:自定义载荷加密、内存加载不落盘、AES-GCM 带 AAD、SHA-256 完整性、HMAC 签名、nonce 防重放、挑战应答、设备指纹、进程扫描、远程功能开关,一共十来层。
但每一层的钥匙都在客户端手里:
| 层 |
防护 |
死穴 |
| 载荷 |
AES-256-GCM |
密钥 = 4 段硬编码 XOR |
| 配置 |
PBKDF2-150k |
pepper 是模板默认值kk_cfg_pepper_change_me |
| 请求签名 |
HMAC-SHA256 + nonce |
SecretKey 硬编码在 config_enc |
| 会话 |
challenge/proof |
依赖同一个 SecretKey |
| 反调试 |
进程名黑名单 |
改进程名即失效 |
| 功能控制 |
服务器下发 |
服务器可以伪造 |
对称密钥体系里,客户端要解密就必然持有密钥,这是结构性问题。真要修,至少签名换成非对称(客户端只持公钥)、载荷密钥由服务端会话下发、pepper 随机化。
然后如果 有还有不懂的,可以评论区交流,拒绝做秀图大师!!
成品:对方已确认悬赏,暂不提供成品,仅供学习,嘻嘻嘻
最后问一下 这个 md模式 ,感觉一般 希望恒大升级一下,排版了好几次
[/i]