| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- #!/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).")
|