完整逻辑演进:两周走了哪些路,为什么走到今天
阶段一:代理抓包 + 协议分析(7/6)
我们当时怎么想的:
"先别碰客户端,让数据自己说话。搭个中间人代理,看看客户端和服务器到底在聊什么。"
做了什么:
公网服务器搭 TCP 代理(:13000 → 150.158.48.18:13000)+ HTTP 更新服(:8888)
轻量版代理抓了 602 个包,31KB
验证了握手结构:7B → 25B → 23B → 加密
验证了加密是确定性的(重放客户端包 → 服务器返回相同密文)
收获了什么:
包 0/1 明文,包 2 是 23B 子密钥,之后全加密
子密钥首字节固定 0x12,但跨会话可变
穷举标准密码全失败(MD5/SHA/RC4/MT19937)
知道了有加密,但不知道加密在哪发生的
为什么停在这里:
代理只能看密文。看不到加密前的数据,就永远不知道协议字段长什么样。
认知升级:
这是正确的起点——先搞清楚"对手长什么样"。但这个阶段的结论也很明确:只看密文不够,必须深入客户端。
阶段二:neasec 补丁(7/6)— 死路
我们当时怎么想的:
"目录里有个 neasec.dll,导出 StartSecInput、SetSecVersion,名字带 sec,肯定就是加密器。废掉它就能拿到明文。"
做了什么:
StartSecInput 补丁(c3 ret 直接返回)→ 加密仍在
DllMain 补丁(mov eax,1; ret)→ 加密仍在
空壳 DLL 替换(4 个导出全 ret)→ 游戏正常启动,加密仍在
为什么这条路是死路:
三次补丁全部无效,学到一个事实:neasec 不是加密器。
它是什么?反作弊/HTTP 遥测。名字里的 "sec" 是 "security"(反外挂),不是 "security"(密码学)。我们被名字误导了。
为什么停在这里:
事实证明 neasec 和网络加密完全无关。继续折腾它是浪费生命。
认知升级:
第一次撞墙,但学到关键教训:文件名和导出名会骗人。唯一可信的是运行时行为。
阶段三:静态分析找加密入口(7/7)— 死路
我们当时怎么想的:
"neasec 不是加密器 → 加密在 my.exe 里 → 反汇编找。一个大加密函数肯定有巨型栈帧、多轮循环、状态机。"
做了什么:
capstone 反汇编 my.exe,找到大栈帧函数 0x1e6a7(632B 栈帧、8 状态机)
找到格式序列化入口 0x16974
找到调用点 0x406C10
为什么这条路是死路:
全部零触发。这些地址在运行时不走。
为什么停在这里:
静态分析能定位"看起来像加密"的函数,但没法告诉我们"游戏实际走不走这里"。
认知升级:
第二次撞墙,学到第二个关键教训:静态分析和运行时是两回事。 一个函数"看起来像加密"不等于"游戏调用它"。静态分析给的是可能性,不是事实。
阶段四:Frida 真机 Hook — 系统化探索(7/7-7/9)— 转折点
我们当时怎么想的:
"不猜了。让 CPU 自己告诉我们调用链。Hook 网络发送的底层 API,反查谁调用了它。"
做了什么:
模块枚举 → 发现 xymain.dll 两个进程都不加载
这直接推翻了之前所有基于 xymain 的分析方案
ws2_32.send Hook → 稳定抓密文 ✅
证明我们能抓到真实的网络数据
Thread.backtrace → 拿到调用链:
send ← c9485 ← c953c ← c9cc1 ← 69df6逐层 Hook 验证:
函数 触发频率 数据内容 结论
send_wrapper (c9485) 每 send 1 次 密文 唯一可靠的 send 入口
caller_2 (c953c) 每 send 272 次 未知参数 热函数,高频触发
caller_3 (c9cc1) 每 send 448 次 未知参数 热函数,高频触发
py_bridge (69df6) 零触发 — 调用链不完整
encrypt 函数 Hook 成功但零触发 — rotor 只解密 NPK,不走网络
xygame Hook 成功但零触发 — 处理渲染,不走网络
PyRun_SimpleString 直接调 crash — 时机/上下文不对
收获了什么:
send_wrapper @ 0xc9485 是唯一可靠的 send 入口(1:1 对应)
c953c 和 c9cc1 是 send 路径上的热函数
69df6 零触发意味着 backtrace 采样不完整,调用链更深处还有东西
c953c/c9cc1 的高频触发不是噪音——这正是逐字段构建数据包的行为
为什么当时没继续这条线:
我们被"找到加密函数"的目标框住了。c953c/c9cc1 是 272 次/448 次的触发频率,在当时看来是"调试噪音",我们过滤掉了它,继续去追想象中的"加密函数"。
认知升级(回头看才知道的):
这是整个探索最大的教训。我们已经踩到了明文字段构建的入口,但认知框架让我们把它当噪音丢弃了。 如果当时 hook 了这两个函数 dump 参数,协议破解可能当天就完成了。
阶段五:KVM Windows VM(7/7-7/8)— 死路
我们当时怎么想的:
"需要一个 Windows 环境自动化 Frida 抓密钥。用 Docker 起 KVM,预配好所有东西,一键跑。"
做了什么:
dockurr/windows 起 KVM
磁盘预配置(Python + Frida + 游戏 + 自启动脚本)
迭代调试循环(停→挂盘→查日志→改脚本→重启,每轮 7 分钟)
为什么这条路是死路:
KVM 在 Intel N100 4核12G 上太勉强
Python 3.12 兼容性问题
pip 在 VM 里可能回退到源码编译卡死
为什么停在这里:
资源太重 + 你有 Windows 真机可以直接跑 Frida,完全不需要 VM 绕路。
认知升级:
第三次撞墙。技术方案的选择要考虑执行环境的约束。N100 跑 KVM 就是小马拉大车,每轮迭代 7 分钟的反馈周期太慢,直接拖死生产力。
阶段六:NPK 解密 → PyRun_SimpleString 自举(7/8)— 进行中
我们当时怎么想的:
"NPK 加密是 rotor,key 已知(j2h56ogodh3se),算法已复现。卡在自定义 opcode 153。与其猜 opcode,不如让游戏 Python 引擎自己跑解密。用 PyRun_SimpleString 注入代码,把解密后的 Python 源码写到文件。"
做了什么:
v1:PyEval hook 内调 PyRun_SimpleString,输出到 C:\frida_pyout.txt
v2:修 tasklist 解析(正则)、修触发时机(setTimeout 8s 替代计数 300)、加 try/except
现状:
v2 脚本已经写好,等你跑结果。
如果跑通意味着什么:
一次性拿到所有 Python 源码、协议定义、密钥。社区说的"python里都有"——NPK 是加密的 Python 脚本包,解密 NPK = 拿到全部协议逻辑。
这条路的风险:
PyRun_SimpleString 可能 crash(v1 就崩了)
时机难把握(v2 用 setTimeout 8s 延迟触发)
输出重定向可能失败
但这个方向是对的——攻陷 Python 层可以一次性解所有问题,不用在 C 层和加密函数死磕。
逻辑主线总结
想抓协议明文
│
├─→ 代理抓密文 ✓(知道有加密,不知道在哪)
│
├─→ neasec 补丁 ✗(证明非加密器)
│ └─ 教训:名字会骗人,只有运行时行为可信
│
├─→ 静态分析找加密入口 ✗(运行时不走)
│ └─ 教训:静态分析和运行时是两回事
│
├─→ Frida 动态探索 ✓(找到调用链,但追加密函数卡住)
│ └─ 最大教训:c953c/c9cc1 是明文字段构建入口,我们当时当噪音过滤掉了
│
├─→ KVM 自动化 ✗(太重)
│ └─ 教训:执行环境约束决定方案可行性
│
└─→ PyRun_SimpleString 自举 ⏳(正确方向——攻陷 Python 层一次性解所有问题)核心教训
"python里都有"这句话从头就在那,我们花了快两周才真正理解它的分量。
整个探索从 C 层逆向开始,在加密函数上绕了最远的路。neasec 不是加密器、静态分析零触发、VM 太重——这些死路的共同根源是一个认知框架问题:我们默认加密发生在 C 层,默认需要逆向才能拿到协议定义。
但事实是:
加密是 Python 层调 C 层做的
协议定义在 Python 源码里
Python 源码就在 NPK 里
NPK 解密需要的 rotor key 我们已经有了
如果第一周就全力攻 PyRun_SimpleString 自举,可能早就拿到源码了。
评论 (0)