知乐 发表于 2026-9-4 20:07

悬赏论坛求助的魔兽ce【拒绝做秀图大师】

本帖最后由 知乐 于 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=0x00001000raw=0x00000400X/R
.rdata   va=0x0002b000raw=0x00029600R
.data      va=0x0003a000raw=0x00037c00R/W
.fptable   va=0x0003f000raw=0x00038800R/W
.rsrc      va=0x00040000raw=0x00038a00R
.reloc   va=0x00050000raw=0x00048000R
最后一个节区结束于 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:

```python
_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)
    for p in _KEY_PARTS:
      for (i, b) in enumerate(p):
            k ^= b
    return bytes(k)
```

XOR 完得到 AES-256-GCM 的 key:`7bc5b1639ba8239c045ecf2a26141e2d299a6247c16b3cc5169c5bb20aed49ae`。没有 KDF,没有环境绑定,密钥和密文躺在同一个文件里。

载荷加载逻辑:

```python
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)         # 模块数
    expect = data                                    # 32 字节 SHA-256
    body = data
    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); pos += 2
      name= body.decode("utf-8");pos += nlen
      nonce = body;                  pos += 12
      blen= struct.unpack_from("<I", body, pos); pos += 4
      blob= body;                  pos += blen
      raw = aes.decrypt(nonce, blob, name.encode("utf-8"))   # AAD = 模块名
      out = 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:

```python
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 密文:

```python
_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 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` 里的请求构造和签名:

```python
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` 里的签名算法:

```python
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)
```

登录请求的构造:

```python
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})
```

会话保持是双保险:心跳挑战应答 + 本地会话证明。

```python
# 心跳: 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` 说明有时钟偏移校验。还原出来:

```python
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 服务器:把协议倒过来实现

服务端怎么验签名,我就怎么造响应。核心就一个函数:

```python
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'` 也是从反编译里拿的):

```python
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,确认类型字节后原地替换。

然后重建整个交付链:

1. 20 个模块 → patch 后的 config_enc → 逐个 zlib 压缩 → AES-GCM 重加密(新 nonce,AAD 不变)
2. 重算 body 的 SHA-256,拼出新的 W3CE1 载荷(100714 → 100706 字节)
3. zlib 压缩,替换 CArchive 里 `secure_payload.bin` 条目的数据
4. 重排 TOC:所有条目的 `entryPos` 按新偏移重算,cookie 里 `lengthofPackage` / `toc` / `tocLen` 重写

新 exe 和原版只差 8 个字节的大小。双击运行:

```
GET/api/v2/version          ← 版本检查,匹配 1.7,不强制更新
POST /loginkey=MOCKKEY1***sign_ok=True
POST /client_assetstoken=813b...
POST /heartbeatcr_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模式 ,感觉一般 希望恒大升级一下,排版了好几次

页: [1]
查看完整版本: 悬赏论坛求助的魔兽ce【拒绝做秀图大师】