前言说明
该系列教学为:从0到1破解某外挂科技辅助的完整技术实录
目前该辅助已经更新,外部已经下载不到,老版本也无法登陆,因此公布逆向破解教学
![图片[1]-Nuitka + Themida强壳 某外挂程序卡密验证逆向破解完整教程-软件安全逆向社区论坛-技术社区-学技术网](https://img.naixiai.cn/2026/07/13/Snipaste_2026-07-13_11-40-50.png)
📚 文档导航
本教程按知识体系组织,建议按顺序阅读:
第一部分:准备与侦查
|
章节 |
内容 |
难度 |
|
目标程序介绍、技术栈识别 |
⭐ |
|
|
工具准备、DLL提取、文件抢夺 |
⭐ |
|
|
PE结构、RCDATA常量池、字符串分析 |
⭐⭐ |
第二部分:挖掘信息
|
章节 |
内容 |
难度 |
|
抓包、GBK编码陷阱、协议还原 |
⭐⭐ |
|
|
登录逻辑映射、错误码体系 |
⭐⭐ |
|
|
汇编层分析、RichCompareBool定位、失败原因 |
⭐⭐⭐ |
第三部分:突破
|
章节 |
内容 |
难度 |
|
进程树分析、模块枚举、基址定位 |
⭐⭐⭐ |
|
|
WriteProcessMemory、VirtualProtectEx |
⭐⭐⭐ |
|
|
CreateRemoteThread、Shellcode、PyRun_SimpleString |
⭐⭐⭐⭐ |
|
|
Loopback接口、HTTP服务器、网络拦截 |
⭐⭐ |
第四部分:总结
|
章节 |
内容 |
难度 |
|
方案演进、错误诊断、最终方案 |
⭐⭐⭐⭐ |
|
|
一键破解脚本、部署方法 |
⭐⭐⭐ |
|
|
各种坑和解决方案 |
⭐⭐⭐ |
|
|
知识地图、方法论、通用模板 |
⭐⭐⭐ |
|
|
常见问题解答 |
⭐⭐ |
🎯 快速开始
如果你只想快速使用破解方案,直接看:
- 了解目标:01_项目背景
- 获取关键发现:07_Themida子进程机制
- 使用最终方案:12_最终完整方案
- 解决常见问题:15_FAQ
一键破解命令(需要管理员权限):
python crack.py
🧠 核心知识地图
![图片[2]-Nuitka + Themida强壳 某外挂程序卡密验证逆向破解完整教程-软件安全逆向社区论坛-技术社区-学技术网](https://img.naixiai.cn/2026/07/13/converted-svg3.png)
🔑 关键技术突破
|
突破点 |
描述 |
|
GBK编码陷阱 |
服务器返回的是明文GBK,不是加密数据 |
|
Themida子进程 |
子进程加载无壳gui.dll,可完整访问内存 |
|
Monkey Patch |
Python层面替换函数,比汇编patch更精准 |
|
Loopback虚拟IP |
不破坏物理网络的内网拦截方案 |
|
Shellcode保存返回值 |
不保存会导致GetExitCodeThread误判失败 |
📖 适合人群
- 有一定Python和C/C++基础
- 会使用IDA Pro/x64dbg进行基础操作
- 想学习Nuitka编译程序的逆向方法
- 想了解Themida壳+Nuitka组合的破解思路
- 逆向经验不足但愿意系统学习
⚙️ 技术栈
|
层级 |
技术 |
|
加壳 |
Themida (TMD) |
|
编译 |
Nuitka (Python→C→x64原生) |
|
GUI |
PySide6 (Qt6) + qfluentwidgets |
|
网络 |
requests (HTTP), hashlib (MD5签名) |
|
Python |
3.11 (python311.dll) |
|
打包 |
Nuitka onefile (自解压模式) |
01 – 项目背景
本章目标
了解目标程序的基本信息、技术栈构成,建立对整个逆向项目的全局认知。
[理论] 技术栈概述
Nuitka 是什么?
Nuitka 是一个 Python → C → 原生代码的编译器。与 PyInstaller(打包.pyc字节码)不同:
|
特性 |
Nuitka |
PyInstaller |
|
编译方式 |
Python→C→原生x64 |
打包.pyc字节码 |
|
反编译 |
❌ 无法还原Python源码 |
✅ 可反编译 |
|
性能 |
接近原生C |
与CPython相同 |
|
临时目录 |
|
|
Nuitka onefile模式把所有依赖(Python运行时、.pyd模块、Qt库等)压缩到EXE,运行时解压到TEMP目录。
Themida (TMD) 是什么?
Themida 是商业级软件保护壳,提供:
- 代码虚拟化
- 反调试/反dump
- 入口点混淆
- 内存保护
关键特性:Themida 只保护外层 EXE。Nuitka编译的 DLL 在运行时解压后是无壳的。
目标程序信息
基本信息
|
项目 |
值 |
|
文件名 |
|
|
大小 |
62,349,312 字节 (59.5 MB) |
|
用途 |
赛尔号游戏辅助工具 |
|
开发者 |
乌皇科技 |
|
验证方式 |
网络卡密验证 |
技术栈识别
方法1:通过TEMP目录识别
运行程序后检查 %TEMP% 目录:
onefile_15876_134276261909760904/
├── gui.dll ← Nuitka编译的核心 (43MB)
├── python311.dll ← Python运行时 (5.6MB)
├── python3.dll
├── PySide6/ ← Qt6 GUI框架
├── numpy/ ← 科学计算
├── cryptography/ ← 加密库
├── qt6core.dll ← Qt6运行时
├── qt6gui.dll
├── qt6widgets.dll
└── ... (56个文件)
方法2:通过RCDATA常量池识别
在gui.dll的资源段中搜索特征字符串:
Auth.auth_client ← 认证客户端模块
Auth.auth_models ← 认证数据模型
Auth.heartbeat_worker ← 心跳维持
SecretConfig ← 密钥配置
Gui.gui_pages.login_page ← 登录UI
Handlers.login_handler ← 登录处理
Utils.login_helper ← 登录辅助
qfluentwidgets ← Fluent Design UI库
方法3:通过PE区段识别
脱机版(2).exe 区段表:
.themida EXEC|READ|WRITE (RawSize=0 ← 虚拟区段!)
.boot EXEC|READ (熵=7.95 ← 加密)
.idata READ|WRITE
.rsrc READ
.themida 区段的 RawSize=0 说明这是Themida创建的虚拟区段,实际数据在Themida内部处理。
模块结构
从RCDATA常量池恢复的Python模块结构:
├── Auth/ ← 认证模块
│ ├── auth_client.py (AuthClient) ← 认证客户端,包含所有API路径
│ ├── auth_gate.py ← 验证门卫(判断是否通过)
│ ├── auth_models.py (AuthSession, AuthError)
│ ├── auth_worker.py ← 认证工作器
│ ├── heartbeat_worker.py ← 心跳维持
│ ├── legacy_auth.py ← 旧版认证全局状态
│ └── machine_code.py ← 机器码生成
├── SecretConfig ← 服务器配置+密钥
├── Gui/
│ ├── gui_pages/login_page.py ← 登录UI
│ └── gui_window.py
├── Handlers/
│ ├── login_handler.py ← 登录处理
│ └── ...
├── Protocol/
│ └── loginProtocol.py
├── Task/
│ └── login_task.py
├── Utils/
│ ├── login_helper.py
│ ├── network_trace.py
│ └── packet.py
└── Constants/
└── web.py (URL常量)
认证流程概览
用户输入卡密
↓
get_machine_code() → sha256(磁盘序列号 + MAC地址)
↓
encrypt_machine_code() → MD5(机器码)
↓
POST http://111.229.9.92:6666/9g2f7g4r4j6v6h4l
CardPwd=<卡密>&Mac=<加密机器码>&LgCity=None
↓
服务器返回 GBK 编码文本
↓
response.split("|||")
↓
成功: expire|||token|||remote_data|||timestamp|||server_sign
失败: 单字段错误消息 (如 "卡密不存在!")
↓
_generate_sign(expire+token+remote_data+timestamp+secret)
↓
client_sign == server_sign ?
↓ 通过
AuthSession → 心跳维持 → 主程序
↓ 失败
"登录失败,卡密不正确或者已过期"
关键发现:服务器返回的 ¿¨Ü²»´?£¡ 不是加密,而是GBK编码的 卡密不存在!
本章知识点
- ✅ Nuitka onefile 打包识别
- ✅ Themida 壳识别
- ✅ TEMP目录文件分析
- ✅ RCDATA常量池分析
- ✅ 模块结构还原
- ✅ 认证流程梳理
02 – 环境搭建
工具清单
|
工具 |
用途 |
必需 |
|
Python 3.x |
运行分析脚本 |
✅ |
|
capstone |
x64反汇编 |
✅ |
|
IDA Pro / Ghidra |
深度分析(可选) |
推荐 |
|
Fiddler/Charles |
抓包分析 |
✅ |
|
Process Monitor |
文件监控 |
推荐 |
提取gui.dll
import subprocess, shutil, glob, time, os
proc = subprocess.Popen([exe_path])
time.sleep(5)
onefile_dirs = glob.glob(r"%TEMP%\onefile_*")
# 等待文件大小稳定
prev_size = 0
while True:
size = os.path.getsize(gui_dll)
if size == prev_size and size > 1024*1024:
break
prev_size = size
time.sleep(0.5)
shutil.copy2(gui_dll, destination)
⚠ gui.dll分阶段写入(先15.6MB后43.2MB),必须等大小稳定再复制。
03 – 静态分析 / 04 – 网络协议 / 05 – 认证流程 / 08 – 内存热补丁 / 12 – 最终完整方案
这5章核心内容已融入其他章节,此处提供快速索引
03 静态分析 → 见 01_项目背景
PE结构:
- ImageBase:
0x180000000 - .text段: VAddr=`0x1000
, RawSize=0x1c46600` (29MB) - .rsrc段: RCDATA常量池 (14MB)
关键字符串搜索:
CardPwd, Mac, LgCity → 请求参数
AuthClient, SecretConfig → 认证模块
111.229.9.92 → 服务器IP
_generate_sign → 签名函数
登录失败/登录失败2/登录失败3 → 错误码映射
04 网络协议 → 见 10_虚拟IP
登录请求:
POST http://111.229.9.92:6666/9g2f7g4r4j6v6h4l
CardPwd=<卡密>&Mac=<MD5机器码>&LgCity=None
成功响应 (GBK编码):
expire|||token|||remote_data|||timestamp|||server_sign
GBK陷阱: ¿¨Ü²»´攚£¡ = 卡密不存在!
05 认证流程 → 见 01_项目背景 + 11_从失败到成功
用户输入卡密 → get_machine_code() → POST → split("|||")
→ _generate_sign → [登录失败2] → get_remote_mark
→ AuthSession → [登录失败3] → 主程序
AuthSession字段: card, token, expire, remote, timestamp, rm, mac_code
08 内存热补丁 → 见 07_Themida子进程 + 06_字节Patch
子进程补丁流程:
找子进程PID → EnumProcessModulesEx → gui.dll基址
→ base+RVA=patch地址 → VirtualProtectEx → WriteProcessMemory
注意: RichCompareBool patch可能是字段分派而非签名验证。
12 最终完整方案
见 crack.py 源码和 09_MonkeyPatch注入。
一键运行: python crack.py (需要管理员权限)
06 – 字节级Patch尝试(失败分析与教训)
本章目标
理解为什么字节级Patch在本次逆向中失败了,掌握如何辨别 签名验证 和 字段分派,避免同样的错误。
[理论] PyObject_RichCompareBool 的两种用途
Python 中 == 比较会被 Nuitka 编译为 PyObject_RichCompareBool 调用。但同一API在不同上下文中含义不同:
用途1:签名验证(我们要patch的)
if client_sign == server_sign: # ← 签名比较
return success()
else:
return fail()
汇编特征:
call [PyObject_RichCompareBool]
cmp eax, -1 ; 错误检查
je error
cmp eax, 1 ; 相等?
jne fail_path ; ← 我们想patch这个
; success_path
用途2:字段分派(千万不能patch的)
if field_name == "expire": # ← 检查字段名
self.expire = value
elif field_name == "token": # ← 检查字段名
self.token = value
elif field_name == "remote":
self.remote = value
else:
self.unknown = value
汇编特征:
mov r8d, 2 ; Py_EQ
call [PyObject_RichCompareBool]
cmp eax, -1
je error
cmp eax, 1
jne next_elif ; ← patch它 → 跳过字段设置!
; 设置字段1
分析过程
第一步:定位 RichCompareBool
在gui.dll的IAT中找到 PyObject_RichCompareBool 地址 0x181c48430,扫描.text段的 FF 15 指令:
找到4个调用点:
VA=0x181c067d8 → 调用点#1
VA=0x181c06822 → 调用点#2
VA=0x181c0686c → 调用点#3
VA=0x181c068b2 → 调用点#4
第二步:分析调用模式
每个调用后都是:
call [PyObject_RichCompareBool]
83 f8 ff ; cmp eax, -1 (错误检查)
0f 84 / 74 xx ; je (跳到错误处理)
83 f8 01 ; cmp eax, 1 (相等检查)
75 xx ; jne (不相等跳转) ← 4个patch点
第三步:错误的判断
当时认为4个jne都是签名验证,全部patch为 jne→jmp。这是致命错误。
第四步:Patch后测试
Patch全部成功(子进程内存中4个字节都变为0xEB),但程序仍报”登录失败2″。
为什么失败了?
反汇编分析
; 函数: some_dispatch(obj, value, data)
0x181c067b0: mov rdi, rdx ; rdi = value
0x181c067c2: mov rbx, r8 ; rbx = data
; 检查1: value == 常量1?
0x181c067cf: mov rcx, rdi
0x181c067d2: mov r8d, 2 ; Py_EQ
0x181c067d8: call RichCompareBool
0x181c067ea: jne 0x181c06812 ; ← PATCH点1
0x181c067ec: global_var1 = rbx ; 设置字段1
0x181c0680b: return
; 检查2: value == 常量2?
0x181c06812: ...
0x181c06834: jne 0x181c0685c ; ← PATCH点2
0x181c06836: global_var2 = rbx ; 设置字段2
0x181c06855: return
; 检查3: value == 常量3?
0x181c0685c: ...
0x181c0687a: jne 0x181c068a2 ; ← PATCH点3
0x181c0687c: global_var3 = rbx ; 设置字段3
0x181c0689b: return
; 检查4: value == 常量4?
0x181c068a2: ...
0x181c068c0: jne 0x181c068c9 ; ← PATCH点4
0x181c068c2: global_var4 = rbx ; 设置字段4
0x181c068c9: ... ; 默认路径
0x181c068e1: return
Patch后的效果:所有4个字段都被跳过,只走默认路径。字段解析逻辑被彻底破坏,导致后续签名验证因为没有正确数据而失败。
如何区分签名验证 vs 字段分派?
|
特征 |
签名验证 |
字段分派 |
|
常量来源 |
计算值 |
.data段全局变量 |
|
副作用 |
跳转分支 |
写入不同全局变量 |
|
分支数 |
2(成功/失败) |
4+(多值分派) |
|
jne目标 |
失败路径 |
下一个elif |
|
常量内容 |
签名MD5 |
字符串/数字 |
|
RCDATA上下文 |
_generate_sign |
expire/token/remote |
经验法则:如果 jne 跳向的是 下一个比较 而不是 失败处理,那就是字段分派。
本章知识点
- ✅ PyObject_RichCompareBool 的双重用途
- ✅ 字段分派 vs 签名验证的汇编特征差异
- ✅ 不该盲目patch所有RichCompareBool
- ✅ 通过RCDATA字符串和汇编结构判断函数用途
07 – Themida 子进程机制
本章目标
理解Themida加壳程序的真实运行结构——父进程保护壳、子进程无保护运行代码,这直接决定了破解策略的切换。
[理论] Themida的运行模式
两种保护层次
┌──────────────────────────────────────┐
│ 外层EXE (Themida壳) │
│ ├── 反调试/反dump │
│ ├── 代码虚拟化 │
│ ├── 内存保护 │
│ └── 创建子进程 ─────────────────────┐ │
└──────────────────────────────────────┘ │
▼
┌──────────────────────────────────────┐
│ 子进程 (无保护!) │
│ ├── 加载gui.dll (完整可见) │
│ ├── 加载python311.dll │
│ └── 执行Python代码 │
└──────────────────────────────────────┘
进程树实际结构
PID=17116 (hermes启动器)
└── PID=15876 (脱机版2.exe ← Themida壳进程)
├── 模块数: 22 (只有系统DLL)
├── gui.dll: ✗ 不在列表
└── PID=5768 (脱机版2.exe ← 子进程,无保护)
├── 模块数: 144 (完整!)
├── gui.dll: ✓ 0x7ffc0c150000
└── python311.dll: ✓ 0x7ffc3e410000
关键发现:父进程 vs 子进程
父进程(PID=15876)
- 模块数:22(只有系统DLL + 脱机版2.exe自身)
- gui.dll 不在模块列表中
- 可执行内存:30MB(gui.dll的.text段就有29MB)
- 4个patch模式全部找不到
# 父进程内存扫描结果
扫描27个区域, 30.5MB
patch模式: 0/4 找到 ← Themida隐藏了gui.dll的内存
子进程(PID=5768)
- 模块数:144
- gui.dll 完整可见:
0x7ffc0c150000 - 所有patch点可读写:
patch#1 @ 0x7ffc0dd567ea: 0x75 → 0xEB ✓
patch#2 @ 0x7ffc0dd56834: 0x75 → 0xEB ✓
patch#3 @ 0x7ffc0dd5687a: 0x75 → 0xEB ✓
patch#4 @ 0x7ffc0dd568c0: 0x75 → 0xEB ✓
找子进程的正确方法
方法1:通过父进程PID枚举子进程
# wmic 获取子进程
r = subprocess.run(['wmic', 'process', 'where',
f'parentprocessid={parent_pid}', 'get', 'processid'], ...)
# EnumProcessModulesEx 检查是否加载了gui.dll
for child_pid in child_pids:
h = kernel32.OpenProcess(0x0410, False, child_pid)
# ... 枚举模块 ...
if 'gui.dll' found:
return child_pid
方法2:等待子进程出现
# 子进程在父进程启动后几秒才出现
WAIT_CHILD = 12 # 等待12秒
for wait in range(WAIT_CHILD):
time.sleep(1)
child_pids = find_child_processes(parent_pid)
for pid in child_pids:
if has_gui_dll(pid):
return pid
⚠️ ctypes 64位 HMODULE 溢出
问题
# ❌ 在64位Windows上会抛 OverflowError
hModules = (wintypes.HMODULE * 2048)()
psapi.GetModuleBaseNameW.argtypes = [..., wintypes.HMODULE, ...]
# OverflowError: int too long to convert
原因
64位Windows的模块句柄值超过32位有符号整数范围(如 0x7ffc3e410000),wintypes.HMODULE 默认按32位整数处理。
修复
# ✅ 全部改用 ctypes.c_void_p
hModules = (ctypes.c_void_p * 2048)()
psapi.EnumProcessModulesEx.argtypes = [..., ctypes.POINTER(ctypes.c_void_p), ...]
psapi.GetModuleBaseNameW.argtypes = [wintypes.HANDLE, ctypes.c_void_p, ...]
psapi.GetModuleFileNameExW.argtypes = [wintypes.HANDLE, ctypes.c_void_p, ...]
本章知识点
- ✅ Themida的父/子进程结构
- ✅ wmic枚举子进程
- ✅ EnumProcessModulesEx枚举模块
- ✅ gui.dll基址定位
- ✅ ctypes 64位HMODULE溢出修复
- ✅ 父进程 vs 子进程内存可访问性差异
09 – Monkey Patch 注入(核心突破)
13 – 踩坑记录
本章目标
汇总本次逆向过程中遇到的所有坑,帮助后来者避免重蹈覆辙。
坑1:GBK编码误判为加密
现象
服务器返回 ¿¨Ü²»´攚£¡,看起来像加密数据。
真相
b'\xbf\xa8\xc3\xdc\xb2\xbb\xb4\xe6\xd4\xda\xa3\xa1'.decode('gbk')
# → "卡密不存在!"
12字节的乱码 + 中文服务器 = 高概率是GBK明文。
避坑方法
- 用多种编码解析可疑数据(GBK, UTF-8, Latin-1, Shift-JIS)
- 如果字节数短且前缀特征明显(如
\xbf),优先考虑中文编码 - 用
decode('gbk')验证,看是否输出有意义的中文
坑2:RichCompareBool不都是签名验证
现象
4个RichCompareBool调用,全部patch后仍然”登录失败2″。
真相
这4个调用是字段分派函数(if field_name == "expire"),不是签名验证。
避坑方法
|
检查项 |
签名验证 |
字段分派 |
|
jne目标 |
失败处理 |
下一个elif |
|
常量来源 |
计算值 |
.data全局变量 |
|
副作用 |
跳转 |
写全局变量 |
坑3:物理网卡添加虚拟IP破坏网络
现象
添加虚拟IP后整个电脑断网,ping 8.8.8.8失败。
原因
在物理以太网接口添加IP会破坏DHCP。
解决
只用Loopback接口(索引1):
netsh interface ipv4 add address 1 <IP> 255.255.255.255
坑4:ctypes HMODULE 64位溢出
现象
OverflowError: int too long to convert
原因
64位模块句柄值超过32位有符号整数范围。
解决
所有HMODULE相关argtypes改用 ctypes.c_void_p。
坑5:Shellcode未保存PyRun_SimpleString返回值
现象
注入后 GetExitCodeThread 返回 0x1,即使代码实际执行成功。
原因
rax 在被 PyGILState_Release 覆盖后变成未定义值。
解决
call PyRun_SimpleString ; rax = 返回值
mov r13, rax ; ★ 保存
call PyGILState_Release
mov rax, r13 ; ★ 恢复
坑6:def语句不能跟分号
现象
SyntaxError at monolithic Python injection
原因
# ❌ 语法错误
"import x; def f(): pass"
# ✅ 正确
"import x\ndef f():\n pass"
Python语法中 def/class/if/for/while 等复合语句必须在逻辑行开头。
坑7:Python背景进程输出被缓冲
现象
python -u script.py & 在background模式下看不到输出。
解决
PYTHONUNBUFFERED=1 python script.py > output.log 2>&1 &
cat output.log # 稍后查看
坑8:gui.dll分阶段写入TEMP
现象
程序启动后gui.dll先以15.6MB出现,然后增长到43.2MB。
原因
Nuitka onefile的解压是分阶段写入的。
解决
必须等文件大小稳定后再操作:
while size != EXPECTED_SIZE:
time.sleep(0.1)
size = os.path.getsize(path)
坑9:IDA idalib_open分析43MB DLL超时
现象
idalib_open(gui.dll, run_auto_analysis=True) 300秒超时。
解决
- 用
run_auto_analysis=False - 或用
capstone做定向反汇编 uv pip install capstone
坑10:Thread exit code ≠ PyRun_SimpleString return value
现象
GetExitCodeThread 返回非零值,但Python代码实际成功。
原因
CreateRemoteThread的线程函数最后执行的是 ret,如果 rax 没有被正确设置,线程退出码就是未定义值。
解决
Shellcode中 mov rax, r13 在 ret 前确保 rax = PyRun_SimpleString的返回值。
本章知识点
- ✅ GBK编码陷阱
- ✅ RichCompareBool辨别
- ✅ 虚拟IP接口选择
- ✅ ctypes HMODULE溢出
- ✅ Shellcode返回值
- ✅ Python语法限制
- ✅ DLL分阶段写入
- ✅ IDA大文件分析
14 – 知识总结
本章目标
建立Nuitka+Themida程序逆向的完整知识地图和方法论。
完整知识地图
![图片[3]-Nuitka + Themida强壳 某外挂程序卡密验证逆向破解完整教程-软件安全逆向社区论坛-技术社区-学技术网](https://img.naixiai.cn/2026/07/13/converted-svg1.png)
方法论总结
1. 程序识别清单
- TEMP目录:
onefile_*还是_MEI*? - PE区段: 有
.themida/.boot吗? - 核心文件:
gui.dll(Nuitka) 还是.pyz(PyInstaller)? - Python版本:
python3xx.dll的版本号? - GUI框架:
PySide6?tkinter?customtkinter?
2. 信息挖掘清单
- RCDATA常量池中有哪些模块名?
- 认证相关字符串在哪里?
- 错误消息映射(
登录失败→登录失败2→登录失败3) - 服务器IP和端口?
- 响应编码格式(GBK? UTF-8? 加密?)
3. 方案选择决策树
有Python C API访问权限?
├── 是 → Monkey Patch注入 (最优)
│ ├── 找到目标函数?
│ │ ├── 是 → 替换函数
│ │ └── 否 → 探查__annotations__再替换
│ └── 替换login方法是最可靠的最终方案
└── 否 → 字节级Patch
├── 确认是签名验证(非字段分派)?
│ ├── 是 → patch RichCompareBool
│ └── 否 → 不要patch! 找其他方法
└── 备选: Frida hook
4. 错误诊断法
|
错误 |
含义 |
下一步 |
|
程序崩溃 |
patch破坏了逻辑 |
撤销patch |
|
|
签名验证失败 |
替换_generate_sign |
|
|
AuthSession创建失败 |
探查字段→替换login |
|
无变化 |
patch位置不对 |
重新分析RCDATA |
通用模板
Monkey Patch注入模板
核心原则
- Themida保护父进程,子进程无保护 → 永远target子进程
- 错误码是GPS → 不要忽略错误消息
- Monkey Patch > 字节Patch → Python是Nuitka的阿克琉斯之踵
- 探查先于行动 → 先用exec+文件获取信息
- Loopback接口 → 不破坏物理网络
- 保存Shellcode返回值 → 否则误判失败
本章知识点
- ✅ 完整知识地图
- ✅ 识别清单
- ✅ 决策树
- ✅ 错误诊断法
- ✅ 通用模板
- ✅ 核心原则
15 – FAQ
Q: 为什么不能直接修改EXE?
A: 脱机版(2).exe 被Themida加壳,所有内嵌数据(包括gui.dll)被加密。搜索”onefile”、”gui.dll”、zstd魔数均未找到,无法直接修改。
Q: 为什么DLL替换方案不可靠?
A: Nuitka onefile每次运行创建新TEMP目录,且在解压后立即加载DLL。替换DLL有时序竞争(必须在解压后、加载前完成),且gui.dll分阶段写入(先15.6MB后43.2MB),时机把握困难。
Q: 为什么byte级patch了4个jne还是”登录失败2″?
A: 因为那4个jne不是签名验证,而是字段分派函数(if field_name == "expire")。Patch它们破坏了字段解析逻辑,但签名验证未被绕过。详见06_字节级Patch尝试。
Q: “登录失败2″和”登录失败3″有什么区别?
A:
登录失败2=_generate_sign签名验证未通过登录失败3= 签名验证通过,但 AuthSession 创建失败(字段缺失)
详见11_从失败到成功。
Q: 为什么不在父进程中做内存补丁?
A: Themida在父进程中隐藏了gui.dll的内存区域。ReactProcessMemory扫描105.7MB可读内存,4个patch模式0/4找到。但子进程中gui.dll完全可见。详见07_Themida子进程机制。
Q: Monkey Patch有没有风险?
A: 极低风险。因为:
- 不修改任何文件或内存字节
- 只是Python层面的函数引用替换
- 程序退出后效果消失
- Themida不保护子进程
Q: 为什么要用Loopback接口而不是物理网卡?
A: 在物理以太网接口添加虚拟IP会破坏DHCP,导致整个电脑断网。Loopback接口不影响物理网络。详见10_虚拟IP与伪造服务器。
Q: 如何获取AuthSession需要的字段?
A: 用Monkey Patch注入探查代码:
import Auth.auth_models
print(Auth.auth_models.AuthSession.__annotations__)
Q: Shellcode中为什么必须保存PyRun_SimpleString的返回值?
A: CreateRemoteThread的线程退出码=`rax。PyGILState_Release是void函数,不设rax。如果不保存PyRun_SimpleString的返回值,GetExitCodeThread`会返回未定义值。详见09_MonkeyPatch注入。
Q: 这个方案能用于其他Nuitka程序吗?
A: 能。通用步骤:
- 识别Nuitka (TEMP目录onefile_*)
- 从RCDATA提取模块结构和认证流程
- 抓包分析网络协议(注意GBK陷阱)
- TheMida壳→找子进程
- Monkey Patch注入替换认证函数
- Loopback虚拟IP+伪造服务器
详见14_知识总结中的通用模板。
Q: 需要什么权限?
A: 添加虚拟IP需要管理员权限。其他操作(进程枚举、内存读写、注入)在子进程中没有额外权限要求。




