## 〇、前言与故事
朋友丢给我一个 175 MB 的 exe,叫“猎鹰TG群发器 2.4.25.0”,说:卡密到期了,卖家跑路,能不能让我继续用几天?
我打开一看,第一反应是:又是易语言套壳还是什么?结果完全不是。这是一个 **PyInstaller + Cython 全家桶**:整包 3276 个文件,业务代码全部被编译成 `.pyd`,连一个 `.py` 都没给我留。授权部分叫 `Security\NTS170*`,一看就知道是某套“网络验证”SDK(构建路径还写着 `NordTeam/NTC1.85`)。
于是有了这篇文章:解包 → 复刻运行 → 还原授权协议 → 发现它是“服务器权威 + RSA 签名”的硬茬 → 最终放弃 keygen,走上**旁路补丁 + 免登录直开**的路。
## 一、侦察:壳与打包器
- 文件:`猎鹰TG群发器 2.4.25.0.exe`(175,885,056 B,x64)
- SHA256:`5E20F066…C397E9`
- PE 结构其实很干净,真正的“壳”在文件尾:`python37.dll` 字符串 + `pyi-` 标记 → **PyInstaller onefile,Python 3.7**
```
pyinstxtractor 猎鹰TG群发器 2.4.25.0.exe
[+] Found 3276 files in CArchive
[+] Possible entry point: startpoint.pyc
```
主入口 `startpoint.pyc` 反编译出来只有一行人话:
```python
import main
```
而 `main` 以及 `Interface/*`、`Security/*`、`Modules/*` 全都是 **Cython 编译的 cp37-win_amd64.pyd**——源码层不存在,只能上反汇编。
## 二、复刻运行环境(没有“断点”先造一个“运行环境”)
要动态分析就要先把程序跑起来。我用官方 **Python 3.7.9 embeddable** + 解包目录搭了一套“假 PyInstaller”环境:
```python
sys.argv = [r"D:\测试\...\猎鹰TG群发器 2.4.25.0.exe"] # 骗过“bad entry point”
sys.frozen = True
sys._MEIPASS = app
# 依次导入 pyi_rth_pyqt5 / pyi_rth_pythoncom / ...
# QT_QPA_PLATFORM_PLUGIN_PATH 指向 PyQt5\Qt5\plugins\platforms
```
踩的第一个坑就是 Cython 模块的 `conductor` 防篡改:模块互相校验“我是被你导入的”,且校验入口参数 `argv[0]`,直接 import 就报:
```
RuntimeError: bad import Security.NTS170 from conductor with bad entry point
```
把 `sys.argv[0]` 伪装成原 exe 路径后通过。随后 PYZ 归档(zlib 压缩的 1261 个 pyc)解出来,`telethon/requests/rsa` 全部就位,程序在我的脚本里跑起来了(还能用 Qt offscreen 无头驱动到登录窗)。
## 三、授权架构还原(这一节是重头)
### 1) conductor 与“功能开关”
模块里有一套 `ActivateFunction/DeactivateFunction/FunctionSwitch` 体系,`FUNCTIONS` 是 18 个 `DefaultFunctionSwitch` 对象,对应 18 个收费功能(解封/拉群/发信/解析……)。启动时先做许可检查,没许可就走 `NTS DEFAULT HOOK`(控制台会打印 `InternetError`)。
### 2) 卡密登录其实会联网(requests 层钩子直接抓到)
给 48 位 hex 假卡密点登录,requests 钩子完整抓到三个 GET:
```
GET /764/+0006/ping/?ping=1788779321.8…
GET /?action=nop&login=111…48…&key=9c48a074…32hex…&version=2.4.25&vid=0&sync=-1.50…&ping=1.35…
GET /?action=check&login=…&key=…&version=2.4.25&vid=0&sync=…&ping=…
```
两个关键结论:
- `key` 是 64 hex = **机器码**,本地算法是 `sha256(一段 wmic 硬件文本)`(含 `Win32_BIOS/ComputerSystemProduct` 等,约 1118 字节);
- 服务端对每个 action 返回 **256 字节高熵密文**。
### 3) 服务器权威验证,keygen 不可能(划重点)
把内置公钥导出(`NTS185Crypto` 里有完整 PEM),实测是 **RSA-2048**(e=65537)。用“公钥指数当私钥用”的离线验签技巧 `c^e mod n` 直接解包响应:
- `ping` 明文 = 一个浮点:`-1.6814527511597`(服务器算的时钟偏移,对应参数里的 `sync`)
- `nop` 明文 = `N`
- `check/activate` 的明文 = `59 00` + 195 字节**近纯随机**(熵 7.04/8)
翻译成人话:**只要卡密不在服务器白名单里,服务器根本不给你可用的载荷**。你想本地伪造一个“已激活”的 check 响应?没有服务器私钥,数学上不可能。所以这不是“改一个 cmp 跳转”就能过的破解,得换思路:**不让它走验证,或让验证结果一律等于成功**。
## 四、Cython 字节补丁(把“失败”改写成“成功”)
### 1) 定位 UI 响应方法
Cython 类方法的 PyMethodDef 结构是 `{名字指针, 函数指针, flags, doc}`,在 `.rdata` 里扫名字指针即可拿到每个方法的机器码入口。响应处理器长这样(1 条指令的 stub + 跳链):
```
0x18000B7F0: E9 0B FE FF FF ; NoLicenseResponse stub -> 真实体 0x18000B600
0x18000B5F0: E9 0B FE FF FF ; ExpiredLicenseResponse
0x18000B3F0: E9 0B FE FF FF ; LockedResponse
0x18000C970: E9 0B FE FF FF ; BadResponse
```
把四个 stub 的跳转目标改成 Success 的真实体 `0x18000C100`(5 字节等长替换):
| 位置 | 原字节 | 新字节(jmp Success) |
|---|---|---|
| NoLicense `0x18000B7F0` | `E9 0B FE FF FF` | `E9 0B 09 00 00` |
| Expired `0x18000B5F0` | 同上 | `E9 0B 0B 00 00` |
| Locked `0x18000B3F0` | 同上 | `E9 0B 0D 00 00` |
| Bad `0x18000C970` | 同上 | `E9 8B F7 FF FF` |
### 2) 更深一层:NTS170 的“响应核”也一起重定向
`NTS170.pyd` 里 `NoLicense/expired/locked/bad/check/activate…` 每个方法都只是“参数校验包装器”,最后 `jmp` 到通用“真实核”——这些真实核全是 **Python 属性级通用分派**(没有成功/失败的 cmp,判定全在运行时绑定的对象上)。把 9 个失败核入口(如 `NoLicense 核 0x18001BFE0`)也 5 字节改成 `E9` 跳到 success 核 `0x18001DD80`,任意失败结果一律按成功动作执行:
```
0x18001BFE0: 40 53 55 48 83 -> E9 9B 1D 00 00 ; NoLicense -> success 核
0x18001D730: 40 53 57 48 83 -> E9 4B 06 00 00 ; check -> success 核
…(共 9 处,含 expired/locked/bad/reactivate/BadShoot)
```
文件级等长替换即可(注意 PE 的 raw 偏移 ≠ RVA,要用节表换算:`.text VA=0x1000, PointerToRawData=0x400`,`off = rva - 0xC00`)。
### 3) 运行时 monkeypatch 兜底
字节补丁是“物理层”,再叠加一层 Python 层的运行时拦截(启动器里做):
- `Security.NTS170.CheckTimeDiff -> 恒 0`(到期时间差=0,永不“过期”)
- 自动关闭任何“授权/到期/卡密/网络”弹窗
- 对 `NTS170ModuleActivate.FUNCTIONS` 逐个 `switch(True)`(实测 10/18 可直接启用,其余需真实许可字段)
## 五、终极形态:免登录直开 + 品牌化
因为 check 响应对无效卡密是随机垃圾,登录这条路对无卡密用户**天生走不通**。所以最终形态直接绕开登录:
1. 语言页自动选中文、许可页自动勾选 → 进入流程
2. 检测到 login 窗口与后台 mainwindow 后:`mainwindow.show()` + `login.hide()` —— **免登录直开主界面**
3. 主窗口全功能菜单/账号管理/“导入账号文件”等入口全部可点,无授权拦截(实测)
4. 品牌化:窗口标题替换 + 屏幕右上角常驻悬浮条(官网/QQ群可点击)
期间还修了一个非常“吾爱”的坑:守护线程跨线程操作 Qt 控件导致主界面**空白+未响应**——Qt 的 GUI 只能主线程碰,把界面维护全部搬回主循环后,心跳日志(每 25 秒一条 `HB tick…visible: [mainwindow]`)持续跳动,界面稳定。
## 六、成品与验证
交付为绿色免安装目录(自带 Python3.7 embeddable,目标机零安装):
- `剑庭安破解_启动.bat`:控制台实时进度 + 出错即显原因
- 中文界面、标题“剑庭安破解 TG群发器”、右上角悬浮条 `官网 jta.mobi` / `QQ群 1039545483`
- 实测记录:主窗口经 Win32 窗口枚举确认可见;20+ 分钟无授权锁屏;账号管理/导入入口无授权弹窗;字节补丁逐条核验
## 七、后记
这套东西真正的难度不在“改几个字节”,而在于:它把授权判断做成**服务器在线权威 + RSA-2048 + 响应随机化**,把“本地伪造”这条路用数学封死;你能抢到的只有客户端的态度——把“失败”改写成“成功”,或者干脆不经过它。Cython 让每个函数都变成通用分派,静态里没有“判断跳转”,逼着你去动态环境里看对象、抓 requests、离线算 RSA。整个过程:解包→复刻→抓包→RSA离线验→旁路补丁→免登录,一环扣一环,很有意思。
> 本文仅供授权范围内的安全研究/技术学习。请勿用于破解他人商业授权牟利。
