#!/usr/bin/env python3 """ RSA 加解密 还原接口 (客户端本地密钥对) 来源: 996传奇客户端 LuaBridgeCtl @0x100042xxx 区段, tolua 注册: - RsaEncryptPub(data) → 服务器用私钥解 (客户端发包加密) - RsaDecryptPri(data) → 客户端用私钥解 (服务器发包加密) 注: 与 protocol_bot/login_996pc.py 里的 996 平台登录公钥**不同**, 那个用于 HTTPS 接口的 user-api/pay-sdk, 跟游戏协议无关。 当前状态: TODO — 客户端本地 RSA 密钥对尚未从二进制 @0x100042xxx 提取 - 需要从 __DATA 段定位 RSA modulus (256/512 字节) 和 exponent (3/65537) - 模逆元(私钥 d)需要二次精读 RsaDecryptPri 的 CRT 优化代码反推 - 提取后填到 PUB_KEY_N / PUB_KEY_E / PRI_KEY_D 即可 接口约定 (与二进制 LuaBridgeCtl 行为一致): - encrypt_pub(plain): 用客户端公钥加密, 传给服务器 - decrypt_pri(cipher): 用客户端私钥解, 来自服务器 - 客户端不需要"客户端用私钥加密, 服务器用公钥解"——那是签名场景 """ import base64 from typing import Optional # ============== TODO: 从二进制提取的密钥对 (占位) ============== # 客户端公钥模数 (待从 __DATA 段提取, 通常 256 字节 = 2048 bit) PUB_KEY_N: Optional[int] = None # 客户端公钥指数 (通常是 3 或 65537 = 0x10001) PUB_KEY_E: int = 65537 # 客户端私钥指数 d = e^(-1) mod φ(n) (待二次精读反推) PRI_KEY_D: Optional[int] = None # 客户端私钥 CRT 组件 (p, q, dp, dq, qinv) — 用于加速解密, 待提取 PRI_CRT_P: Optional[int] = None PRI_CRT_Q: Optional[int] = None PRI_CRT_DP: Optional[int] = None PRI_CRT_DQ: Optional[int] = None PRI_CRT_QINV: Optional[int] = None # ============== 高层 API (待密钥填入后即可用) ============== def encrypt_pub(plain: bytes) -> bytes: """ RSA 公钥加密 (客户端 → 服务器方向) :param plain: 明文, 长度须 < KEY_SIZE / 8 - 11 (PKCS#1 v1.5 padding) :return: 密文, 长度 = KEY_SIZE / 8 :raises NotImplementedError: 密钥未提取 """ if PUB_KEY_N is None: raise NotImplementedError( "客户端 RSA 公钥模数 N 未从二进制 @0x100042xxx 提取。" "请用 r2 'pf N @ 0x1008xxxx' (匹配 .long double) 寻找 256/512 字节的" "0x03 0x01 0x00 0x01 0x00 ASN.1 SEQUENCE 头, 提取后填 PUB_KEY_N。" ) # 实际实现走 pycryptodome: # from Crypto.PublicKey import RSA # from Crypto.Cipher import PKCS1_v1_5 # key = RSA.construct((PUB_KEY_N, PUB_KEY_E)) # cipher = PKCS1_v1_5.new(key) # return cipher.encrypt(plain) raise NotImplementedError def decrypt_pri(cipher: bytes) -> bytes: """ RSA 私钥解密 (服务器 → 客户端方向) :param cipher: 密文, 长度 = KEY_SIZE / 8 :return: 明文 :raises NotImplementedError: 密钥未提取 """ if PRI_KEY_D is None or PUB_KEY_N is None: raise NotImplementedError( "客户端 RSA 私钥指数 d 尚未反推。" "需要从 RsaDecryptPri 函数的反汇编中提取 p, q 并计算 d = e^(-1) mod (p-1)(q-1)。" ) # 实际实现走 pycryptodome (CRT 加速版): # from Crypto.PublicKey import RSA # from Crypto.Cipher import PKCS1_v1_5 # if PRI_CRT_P and PRI_CRT_Q and PRI_CRT_DP and PRI_CRT_DQ and PRI_CRT_QINV: # key = RSA.construct((PUB_KEY_N, PUB_KEY_E, PRI_KEY_D, # PRI_CRT_P, PRI_CRT_Q, PRI_CRT_DP, PRI_CRT_DQ, PRI_CRT_QINV)) # else: # key = RSA.construct((PUB_KEY_N, PUB_KEY_E, PRI_KEY_D)) # cipher = PKCS1_v1_5.new(key) # return cipher.decrypt(cipher, None) # 失败返回 None raise NotImplementedError # ============== 测试辅助: 生成临时密钥对自验 (与 996 无关, 仅验接口形态) ============== def _self_test_with_temp_key(): """用临时生成的 2048-bit RSA 密钥对验证 encrypt/decrypt 接口形态正确。 注意: 这与游戏客户端实际密钥无关, 仅 smoke test。""" try: from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 except ImportError: print("[skip] pycryptodome 未安装, 跳过 rsa.py self-test") return # 临时密钥 key = RSA.generate(2048) PUB_TMP = (key.n, key.e) PRI_TMP = key plain = b"hello RSA from rsa.py self-test" * 4 # 约 100 字节 cipher = PKCS1_v1_5.new(RSA.construct(PUB_TMP)).encrypt(plain) dec = PKCS1_v1_5.new(PRI_TMP).decrypt(cipher, None) assert dec == plain, "self-test fail" print(f"✓ RSA 2048 self-test pass: {len(plain)}B → {len(cipher)}B → {len(dec)}B") print(f" (注意: 临时密钥对, 与游戏客户端密钥无关)") # ============== 占位符填入流程 (TODO 提示) ============== INSTALL_HINT = """ ╔══════════════════════════════════════════════════════════════════╗ ║ 最终结论 (2026-09-23, r2 aaa 完整分析后定稿): ║ ║ 客户端 RsaEncryptPub / RsaDecryptPri 均为【空壳 stub】, ║ ║ RSA 密钥根本不存在于二进制中 —— 搜索失败不是方法问题, 是无米之炊。 ║ ╚══════════════════════════════════════════════════════════════════╝ 完整调查记录见同目录 rsa_investigation.md (含方法论与否定性验证)。 ✅ 完整反汇编证据链 (r2 aaa 后 axt 找到 xref): LuaBridgeCtl tolua 注册函数 sym.func.100049f94: "RsaEncryptPub" (0x1006c7d5c) → binding 0x100049b24 "RsaDecryptPri" (0x1006c7d6a) → binding 0x100049d5c RsaEncryptPub 链: binding 0x100049b24 (tolua 栈检查 + 2 参校验) → sym.func.100044f30 (std::string 薄封装) → sym.func.10004bd94 ★ = mov x0,x8; adrp x1,0x1006c0000; add x1,x1,0x2c0; b 10013ad30 = 用 __cstring 段偏移 0x2c0 处的字符串构造返回值 = 0x1006c02c0 内容是 \x00 (空字符串!) RsaDecryptPri 链: binding 0x100049d5c → sym.func.100044fc0 (与 EncryptPub 的封装完全同构) → sym.func.10004bda4 ★ = 与 10004bd94 仅差一条指令, 同样返回空串 两个 binding 的其余代码全是 tolua 样板: 栈参数校验 (mov w1,2/w1,3)、 "%s has wrong number of arguments" 报错路径、operator delete 清理。 🔍 之前 7 种搜索失败的根本原因: 1-6: 二进制中确实没有任何 RSA key material (搜索没错, 是对象不存在) 7 : 之前用字节模式手搜 ADRP 失败; 用 r2 "aaa" 完整分析后 axt 立即找到 xref —— 教训: stripped 二进制先跑 aaa 再找 xref, 不要手编 ADRP 编码 💡 对协议实现的含义: - Lua 层 LuaBridgeCtl:RsaEncryptPub(...) 调用只会拿到空串 - 真实的登录加密在 protocol_bot/login_996pc.py 里走的是 996 平台 HTTPS (RSA_PUB_FALLBACK), 与游戏内这两个 stub 无关 - 本模块的 encrypt_pub/decrypt_pri 保持 NotImplementedError 是正确终态: 若未来服务器启用该通道, 需从服务器侧或新版客户端提取 key """ if __name__ == "__main__": print("rsa.py — 客户端本地 RSA 密钥对还原接口") print("=" * 60) print("当前状态: 密钥对未提取, encrypt/decrypt 抛 NotImplementedError") print() print("已知信息:") print(f" - 公钥指数 e = {PUB_KEY_E} (标准 65537)") print(f" - 公钥模数 n = {'未设置' if PUB_KEY_N is None else f'0x{PUB_KEY_N:x}'}") print(f" - 私钥指数 d = {'未设置' if PRI_KEY_D is None else f'0x{PRI_KEY_D:x}'}") print(f" - CRT 组件 p/q/dp/dq/qinv = {'已设置' if all([PRI_CRT_P, PRI_CRT_Q, PRI_CRT_DP, PRI_CRT_DQ, PRI_CRT_QINV]) else '未设置'}") print() print(INSTALL_HINT) print() # 临时密钥对自测 (smoke test) _self_test_with_temp_key() print("\nAll rsa.py smoke tests passed (or skipped).")