Nuitka + Themida强壳 某外挂程序卡密验证逆向破解完整教程

前言说明

该系列教学为:从0到1破解某外挂科技辅助的完整技术实录

目前该辅助已经更新,外部已经下载不到,老版本也无法登陆,因此公布逆向破解教学

图片[1]-Nuitka + Themida强壳 某外挂程序卡密验证逆向破解完整教程-软件安全逆向社区论坛-技术社区-学技术网


📚 文档导航

本教程按知识体系组织,建议按顺序阅读:

第一部分:准备与侦查

章节

内容

难度

01_项目背景

目标程序介绍、技术栈识别

02_环境搭建

工具准备、DLL提取、文件抢夺

03_静态分析

PE结构、RCDATA常量池、字符串分析

⭐⭐

第二部分:挖掘信息

章节

内容

难度

04_网络协议分析

抓包、GBK编码陷阱、协议还原

⭐⭐

05_认证流程逆向

登录逻辑映射、错误码体系

⭐⭐

06_字节级Patch尝试

汇编层分析、RichCompareBool定位、失败原因

⭐⭐⭐

第三部分:突破

章节

内容

难度

07_Themida子进程机制

进程树分析、模块枚举、基址定位

⭐⭐⭐

08_内存热补丁方案

WriteProcessMemory、VirtualProtectEx

⭐⭐⭐

09_MonkeyPatch注入

CreateRemoteThread、Shellcode、PyRun_SimpleString

⭐⭐⭐⭐

10_虚拟IP与伪造服务器

Loopback接口、HTTP服务器、网络拦截

⭐⭐

第四部分:总结

章节

内容

难度

11_从失败到成功

方案演进、错误诊断、最终方案

⭐⭐⭐⭐

12_最终完整方案

一键破解脚本、部署方法

⭐⭐⭐

13_踩坑记录

各种坑和解决方案

⭐⭐⭐

14_知识总结

知识地图、方法论、通用模板

⭐⭐⭐

15_FAQ

常见问题解答

⭐⭐


🎯 快速开始

如果你只想快速使用破解方案,直接看:

  1. 了解目标:01_项目背景
  2. 获取关键发现:07_Themida子进程机制
  3. 使用最终方案:12_最终完整方案
  4. 解决常见问题:15_FAQ

一键破解命令(需要管理员权限):

python crack.py

🧠 核心知识地图

图片[2]-Nuitka + Themida强壳 某外挂程序卡密验证逆向破解完整教程-软件安全逆向社区论坛-技术社区-学技术网

🔑 关键技术突破

突破点

描述

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相同

临时目录

onefile_<PID>_*

_MEI*

Nuitka onefile模式把所有依赖(Python运行时、.pyd模块、Qt库等)压缩到EXE,运行时解压到TEMP目录。

Themida (TMD) 是什么?

Themida 是商业级软件保护壳,提供:

  • 代码虚拟化
  • 反调试/反dump
  • 入口点混淆
  • 内存保护

关键特性:Themida 只保护外层 EXE。Nuitka编译的 DLL 在运行时解压后是无壳的。


目标程序信息

基本信息

项目

文件名

脱机版(2).exe

大小

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反汇编

uv pip install capstone

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, r13ret 前确保 rax = PyRun_SimpleString的返回值。


本章知识点

  • GBK编码陷阱
  • RichCompareBool辨别
  • 虚拟IP接口选择
  • ctypes HMODULE溢出
  • Shellcode返回值
  • Python语法限制
  • DLL分阶段写入
  • IDA大文件分析

14 – 知识总结

本章目标

建立Nuitka+Themida程序逆向的完整知识地图和方法论。


完整知识地图

图片[3]-Nuitka + Themida强壳 某外挂程序卡密验证逆向破解完整教程-软件安全逆向社区论坛-技术社区-学技术网

方法论总结

1. 程序识别清单

  • TEMP目录: onefile_* 还是 _MEI*
  • PE区段: 有 .themida / .boot 吗?
  • 核心文件: gui.dll (Nuitka) 还是 .pyz (PyInstaller)?
  • Python版本: python3xx.dll 的版本号?
  • GUI框架: PySide6tkintercustomtkinter

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

登录失败2

签名验证失败

替换_generate_sign

登录失败3

AuthSession创建失败

探查字段→替换login

无变化

patch位置不对

重新分析RCDATA


通用模板

Monkey Patch注入模板


核心原则

  1. Themida保护父进程,子进程无保护 → 永远target子进程
  2. 错误码是GPS → 不要忽略错误消息
  3. Monkey Patch > 字节Patch → Python是Nuitka的阿克琉斯之踵
  4. 探查先于行动 → 先用exec+文件获取信息
  5. Loopback接口 → 不破坏物理网络
  6. 保存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的线程退出码=`raxPyGILState_Release是void函数,不设rax。如果不保存PyRun_SimpleString的返回值,GetExitCodeThread`会返回未定义值。详见09_MonkeyPatch注入


Q: 这个方案能用于其他Nuitka程序吗?

A: 能。通用步骤:

  1. 识别Nuitka (TEMP目录onefile_*)
  2. 从RCDATA提取模块结构和认证流程
  3. 抓包分析网络协议(注意GBK陷阱)
  4. TheMida壳→找子进程
  5. Monkey Patch注入替换认证函数
  6. Loopback虚拟IP+伪造服务器

详见14_知识总结中的通用模板。


Q: 需要什么权限?

A: 添加虚拟IP需要管理员权限。其他操作(进程枚举、内存读写、注入)在子进程中没有额外权限要求。

请登录后发表评论

    没有回复内容