QCAD 3.32.9 逆向分析与补丁实战
免责声明: 本文仅用于技术研究和学习目的。所有分析均基于自行购买的正版软件。请支持正版软件,在购买授权后使用。
- 前言
- 环境准备
- QCAD 插件架构分析
- 许可验证机制解剖
- Qt5 vs Qt6 的关键差异
- isTrial 函数定位
- 补丁策略设计
- checkLicense 函数定位与补丁
- checkPluginLicense 函数定位与补丁
- 工具开发历程
- 性能优化:从 16 秒到 174 毫秒
- 常见问题与排错
- 总结与致谢
1. 前言
1.1 背景
QCAD 是一款开源的 2D CAD 制图软件,采用 GPL 协议发布。其商业版本 QCAD Professional 和 QCAD/CAM 提供了额外的功能模块,这些模块以 DLL 插件的形式加载,并带有试用期限制。
1.2 版本演进
| 版本 |
日期 |
Qt 版本 |
插件数量 |
目标插件数 |
| 3.32.2 |
2026-Q1 |
Qt 5.13.2 |
15 |
9 |
| 3.32.3 |
2026-Q2 |
Qt 5.13.2 |
15 |
9 |
| 3.32.9 |
2026-Q3 |
Qt 6.11.0 |
22 |
13 |
1.3 分析范围
本文涵盖从 QCAD 3.32.2 到 3.32.9 的完整逆向分析过程,包括:
- Qt5 和 Qt6 版本的二进制差异分析
- 插件许可验证机制的逆向工程
- 跨版本补丁工具的开发与演化
- 性能优化:从 16 秒到 174 毫秒
2. 环境准备
2.1 工具链
OS: Windows 11 24H2
Python: 3.14.5
IDA Pro: 9.1 (MCP 远程连接)
QCAD/CAM: 3.32.9 (Qt5 & Qt6 双版本分析)
2.2 分析目标
所有显示"试用版本:激活"的专有插件:
| # |
插件 ID |
文件 |
说明 |
自版本 |
| 1 |
DWG |
qcaddwg.dll |
DWG 格式支持 |
3.32.2 |
| 2 |
PDF |
qcadpdf.dll |
PDF 导入 |
3.32.2 |
| 3 |
POLYGON |
qcadpolygon.dll |
多边形布尔运算 |
3.32.2 |
| 4 |
PROJ |
qcadproj.dll |
地图投影 |
3.32.2 |
| 5 |
PROTOOLS |
qcadproscripts.dll |
专业版脚本工具 |
3.32.2 |
| 6 |
PROXIES |
qcadproxies.dll |
代理图形引擎 |
3.32.2 |
| 7 |
SHP |
qcadshp.dll |
Shape 文件导入 |
3.32.2 |
| 8 |
SPATIALINDEXPRO |
qcadspatialindexpro.dll |
空间索引 |
3.32.2 |
| 9 |
TRACE |
qcadtrace.dll |
位图矢量化 |
3.32.2 |
| 10 |
CAM |
qcadcamscripts.dll |
CAM 加工脚本 |
3.32.3 |
| 11 |
NEST |
qcadnest.dll |
排料优化 |
3.32.3 |
| 12 |
CAVALIER |
qcadcavalier.dll |
多段线偏移 |
3.32.9 新增 |
| 13 |
TEX |
qcadtex.dll |
LaTeX 数学对象 |
3.32.9 新增 |
3. QCAD 插件架构分析
3.1 三层架构
QCAD 的插件系统分为三个层级:
GPL (开源免费)
├── DXF (qcaddxf.dll) — DXF 文件导入导出
├── JSAPI (qcadjsapi.dll) — QJSEngine 绑定 (3.32.9 新增)
└── SCRIPTS (qcadscripts.dll) — QCAD 脚本引擎
Professional (专有, 需授权)
├── DWG (qcaddwg.dll) — DWG 格式支持
├── PDF (qcadpdf.dll) — PDF 导入
├── POLYGON (qcadpolygon.dll) — 多边形布尔运算
├── PROJ (qcadproj.dll) — 地图投影
├── PROTOOLS (qcadproscripts.dll) — 专业版工具
├── PROXIES (qcadproxies.dll) — 代理图形引擎
├── SHP (qcadshp.dll) — Shape 文件导入
├── SPATIALINDEXPRO (qcadspatialindexpro.dll) — 空间索引
├── TRACE (qcadtrace.dll) — 位图矢量化
├── CAVALIER (qcadcavalier.dll) — 多段线偏移 (3.32.9 新增)
├── TEX (qcadtex.dll) — LaTeX 数学对象 (3.32.9 新增)
├── TRIANGULATION (qcadtriangulation.dll) — 多边形三角化 (3.32.9 移除)
└── 辅助 DLL: HELP, NESTJSAPI, QCADPROJSAPI, TEXJSAPI
CAM (专有, 需授权)
├── CAM (qcadcamscripts.dll) — CAM 加工脚本
├── GLES3D (qcadgles3d.dll) — OpenGL ES 3D 显示
├── NEST (qcadnest.dll) — 排料优化
└── 辅助 DLL: GLES3DJSAPI
3.2 插件加载流程
QCAD 启动
└─ main() 入口
└─ RPluginLoader::loadPlugins()
└─ 遍历 plugins/ 目录加载所有 DLL
├─ QPluginLoader::load() — 加载 DLL
├─ QPluginLoader::instance() — 获取插件实例
├─ qobject_cast<RPluginBase*>() — 转换为基类指针
└─ RPluginLoader::checkPluginLicense()
└─ 调用插件自身的 RPluginBase::checkLicense()
├─ RPluginBase::checkLicenseFile() — 检查许可证文件
├─ RPluginBase::checkLicenseKey() — 检查许可证密钥
└─ 设置 RPluginBase::isTrial() 的返回值
4. 许可验证机制解剖
4.1 RPluginBase 基类结构
RPluginBase 是所有插件的基类,继承自 QObject。其关键成员变量布局如下:
class RPluginBase : public QObject {
Q_OBJECT
public:
// ... 虚函数表 ...
virtual bool isTrial() const; // 判断是否试用版
virtual bool checkLicense() const; // 检查许可证
virtual QString checkLicenseFile() const;
virtual QString checkLicenseKey() const;
// ...
protected:
// Qt5 偏移 0x28 处的一个字节标志位
// Qt6 偏移 0x48 处的一个字节标志位
bool _isTrial; // 0 = 已授权, 1 = 试用版
};
4.2 许可证验证链
所有 13 个目标插件共享完全相同的 isTrial() 函数实现。这是通过编译器的虚函数表机制实现的——每个 DLL 编译时都包含了 RPluginBase 的虚函数实现。
4.3 验证流程
QCAD 启动
│
▼
checkLicense() 被调用
│
├─ checkLicenseFile() — 检查 %APPDATA%/QCAD/ 下的许可证文件
│ ├─ 找到有效许可证文件 → _isTrial = 0 (已授权)
│ └─ 未找到 → 继续下一步
│
├─ checkLicenseKey() — 检查注册表或配置文件中的许可证密钥
│ ├─ 密钥有效 → _isTrial = 0 (已授权)
│ └─ 密钥无效/未找到 → _isTrial = 1 (试用版)
│
└─ 最终: isTrial() 返回 _isTrial 的值
5. Qt5 vs Qt6 的关键差异
5.1 核心发现
这是本文最重要的技术发现。QCAD 3.32.9 同时发布了 Qt5 和 Qt6 两个版本,它们的 RPluginBase 对象布局不同:
| 版本 |
isTrial 偏移 |
十六进制特征码 |
补丁策略 |
| Qt5 3.32.2 |
[rcx+0x28] |
80 79 28 00 0F 94 C0 C3 |
mov al, 0; nop; ret |
| Qt5 3.32.9 |
[rcx+0x28] |
80 79 28 00 0F 94 C0 C3 |
mov al, 0; nop; ret |
| Qt6 3.32.9 |
[rcx+0x48] |
80 79 48 00 0F 94 C0 C3 |
xor al, al; ret; nop×5 |
5.2 原因分析
Qt5 和 Qt6 的 QObject 基类大小不同。Qt6 的 QObject 比 Qt5 增加了约 32 字节,原因包括:
- QProperty 系统: Qt6 引入了新的 QProperty 绑定系统
- QObjectPrivate 扩展: Qt6 的
QObjectPrivate 增加了新字段
- QML 集成改进: 与 QML 引擎的集成更深
这导致 RPluginBase 中 _isTrial 标志位的偏移从 0x28 变为 0x48。
5.3 验证方法
通过 IDA Pro 分析 qcadproj.dll 的导出函数 ?isTrial@RPluginBase@@QEAA_NXZ:
Qt5 版本 (3.32.2 / 3.32.9):
.text:000000018000B200 80 79 28 00 cmp byte ptr [rcx+0x28], 0
.text:000000018000B204 0F 94 C0 setz al ; al=1 → 试用版
.text:000000018000B207 C3 ret
Qt6 版本 (3.32.9):
.text:000000018000BE00 80 79 48 00 cmp byte ptr [rcx+0x48], 0
.text:000000018000BE04 0F 94 C0 setz al ; al=1 → 试用版
.text:000000018000BE07 C3 ret
5.4 Qt 版本检测方法
工具自动检测 Qt 版本的逻辑非常简单可靠:
def detect_qt(install_dir: Path) -> str:
if (install_dir / "Qt6Core.dll").exists():
return "Qt6"
if (install_dir / "Qt5Core.dll").exists():
return "Qt5"
return "?"
6. isTrial 函数定位
6.1 方法一:通过导出表定位
3 个插件将 isTrial 和 checkLicense 作为 C++ 修饰导出函数:
| 插件 |
isTrial 导出 RVA |
Qt5 偏移 |
Qt6 偏移 |
| qcadproj.dll |
0xBE00 |
0x28 |
0x48 |
| qcadspatialindexpro.dll |
0x35E0 |
0x28 |
0x48 |
| qcadtrace.dll |
0x3570 |
0x28 |
0x48 |
导出函数名示例:
?isTrial@RPluginBase@@QEAA_NXZ
?checkLicense@RPluginBase@@QEAA_NXZ
6.2 方法二:通过特征码扫描
所有 13 个目标插件的 isTrial 函数具有完全相同的二进制模式。这意味着我们可以通过特征码扫描一次性定位所有文件,无需逐个分析。
6.3 方法三:IDA Pro MCP 批量分析
使用 IDA Pro 9.1 的 MCP 接口,可以远程批量分析 DLL:
# 通过 MCP 远程分析所有 DLL
for dll in target_dlls:
# 打开文件到 IDA
ida.open_file(dll_path)
# 等待自动分析完成
# 查找 isTrial 函数
# 导出反汇编结果
# 关闭数据库
7. 补丁策略设计
7.1 补丁原理
将 isTrial() 函数的返回值强制改为 0(非试用),使其始终返回"已授权"状态。
7.2 等长替换原则
这是最重要的工程原则。修改 PE 文件必须保持等长,否则会破坏:
- 所有后续代码的 RVA 地址偏移
- 导出表地址指向错误的偏移量
- 依赖该 DLL 的其他模块(如
qcadprojsapi.dll)加载失败
7.3 Qt5 版补丁 (偏移 0x28)
8 字节 → 8 字节 (等长替换):
原始: 80 79 28 00 0F 94 C0 C3
cmp byte ptr [rcx+0x28], 0
setz al ; al=1 表示试用
ret
替换: 80 79 28 00 B0 00 90 C3
cmp byte ptr [rcx+0x28], 0 ; 保留比较指令(不影响结果)
mov al, 0 ; 强制返回 0 (非试用)
nop ; 填充至等长
ret
7.4 Qt6 版补丁 (偏移 0x48)
8 字节 → 8 字节 (等长替换):
原始: 80 79 48 00 0F 94 C0 C3
cmp byte ptr [rcx+0x48], 0
setz al ; al=1 表示试用
ret
替换: 32 C0 C3 90 90 90 90 90
xor al, al ; al = 0 (非试用)
ret
nop × 5 ; 填充至等长
7.5 为什么不能缩短字节
惨痛教训: 最初我犯了一个错误——将 8 字节替换为 3 字节 (32 C0 C3)。
后果:
文件缩小 5 字节
└─ 所有后续代码的 RVA 地址偏移 -5
└─ 导出表地址全部错位
└─ qcadprojsapi.dll 加载失败
└─ "无法加载库" 错误
修复需要:
- 从备份恢复所有 13 个文件
- 重新设计为等长替换
- 重新验证所有导出表完整性
教训: 修改 PE 文件时,永远保持等长替换,除非你完全理解 PE 文件结构并能修复所有重定位表。
7.6 isTrial 补丁的局限性
重要发现: 仅修补 isTrial() 函数不足以彻底解决试用提示问题。
修补 isTrial() 后,QCAD 启动时不会再显示"试用版本"标记,但 15-30 分钟后仍然会弹出试用到期提示。这是因为 QCAD 的许可验证机制有三层:
JavaScript 定时器 (15-30 分钟)
└─ RPluginLoader::checkPluginLicenses() (qcadcore.dll)
└─ RPluginLoader::checkPluginLicense() (qcadcore.dll)
└─ RPluginBase::checkLicense() (每个插件 DLL)
├─ checkLicenseFile() — 检查许可证文件
├─ checkLicenseKey() — 检查许可证密钥
└─ 设置 _isTrial = 1 (重新激活试用模式)
即使 isTrial() 被修补为始终返回 0,checkLicense() 函数仍然会:
- 检查许可证文件是否存在且有效
- 检查许可证密钥是否有效
- 如果两者都无效,设置
_isTrial = 1
- 定时器触发时,JavaScript 代码调用
checkPluginLicenses()
- 该函数遍历所有插件,调用
checkLicense()
- 返回 false 的插件触发试用到期弹窗
因此,需要 三层补丁 才能彻底解决问题:
| 层级 |
目标 |
函数 |
作用 |
| 1 |
13 个插件 DLL |
isTrial() |
返回 false (非试用) |
| 2 |
13 个插件 DLL |
checkLicense() |
始终返回 true (已授权) |
| 3 |
qcadcore.dll |
checkPluginLicense() |
阻止插件引擎检查许可证 |
8. checkLicense 函数定位与补丁
8.1 问题背景
在修补 isTrial() 后,虽然插件不再显示"试用版本"标记,但 QCAD 内置的 JavaScript 定时器仍然会在 15-30 分钟后触发试用到期弹窗。经过深入分析,发现弹窗的触发路径并不经过 isTrial(),而是通过 checkLicense() 函数。
8.2 验证过程
通过字符串搜索在所有 DLL 中发现了关键线索:
TrialExpiredReason — 试用到期原因枚举
Trial — 试用状态标记
Disabled — 禁用状态标记
1uninitSlot() — Qt 定时器槽函数
LicenseFile — 许可证文件属性
License/LicenseKey — 许可证密钥属性
所有 13 个目标插件都包含 TrialExpired 字符串和 QTimer 引用,证实了定时器机制的存在。
8.3 checkLicense 函数定位
通过 pefile 库解析 DLL 的导出表,在 3 个插件中找到了 checkLicense 的导出 RVA:
| 插件 |
checkLicense RVA |
checkLicenseFile RVA |
checkLicenseKey RVA |
isTrial RVA |
| qcadproj.dll |
0xB0D0 |
0xB1E0 |
0xB7B0 |
0xBE00 |
| qcadspatialindexpro.dll |
0x28B0 |
0x29C0 |
0x2F90 |
0x35E0 |
| qcadtrace.dll |
0x2840 |
0x2950 |
0x2F20 |
0x3570 |
导出函数名(C++ 修饰名):
?checkLicense@RPluginBase@@UEAA_NXZ — virtual bool checkLicense()
?checkLicenseFile@RPluginBase@@QEAA_NXZ — bool checkLicenseFile()
?checkLicenseKey@RPluginBase@@QEAA_NXZ — bool checkLicenseKey()
?isTrial@RPluginBase@@QEAA_NXZ — bool isTrial()
注意:@UEAA 表示这是一个虚函数(U = virtual),@QEAA 表示非虚函数。
8.4 函数特征码分析
读取 qcadproj.dll 中 checkLicense 函数的代码(RVA 0xB0D0):
; checkLicense 函数前导 (12 字节)
48 89 5C 24 10 mov [rsp+0x10], rbx ; 保存 rbx
57 push rdi ; 保存 rdi
48 81 EC A0 00 00 00 sub rsp, 0xA0 ; 分配 160 字节栈空间
48 8B 05 ... mov rax, [rip+...] ; 加载安全 Cookie
48 33 C4 xor rax, rsp ; XOR 与栈指针
48 89 84 24 98 00 00 00 mov [rsp+0x98], rax ; 保存 Cookie
48 8B D9 mov rbx, rcx ; 保存 this 指针
48 8D 15 ... lea rdx, [rip+...] ; 加载字符串引用
48 8D 4C 24 40 lea rcx, [rsp+0x40] ; 结果字符串缓冲区
FF 15 ... call [rip+...] ; 调用 checkLicenseFile()
该函数具有以下特征:
- 安全 Cookie (GS): 函数使用
/GS 编译保护,栈上保存了安全 Cookie
- 虚函数调用: 通过
call [rip+...] 间接调用 checkLicenseFile() 和 checkLicenseKey()
- QString 返回值: 使用栈上缓冲区作为 QString 的返回参数
- 函数较长: 约 150+ 字节,远长于
isTrial 的 8 字节
8.5 在所有 DLL 中定位 checkLicense
通过搜索函数前导特征码 48 89 5C 24 10 57 48 81 EC A0 00 00 00,在所有 13 个目标 DLL 中找到了 checkLicense 函数:
| DLL |
文件偏移 |
上下文 |
| qcaddwg.dll |
0x55A0 |
有安全 Cookie + 虚函数调用 |
| qcadpdf.dll |
0x1D80 |
同上 |
| qcadpolygon.dll |
0xCC0 |
同上 |
| qcadproj.dll |
0xA4D0 |
同上 |
| qcadproscripts.dll |
0x22E0 |
同上 |
| qcadproxies.dll |
0xFA0 |
同上 |
| qcadshp.dll |
0x1050 |
同上 |
| qcadspatialindexpro.dll |
0x1CB0 |
同上 |
| qcadtrace.dll |
0x1C40 |
同上 |
| qcadcavalier.dll |
0xCC0 |
同上 |
| qcadtex.dll |
0x34300 |
同上 |
| qcadcamscripts.dll |
0x1CC0 |
同上 |
| qcadnest.dll |
0x44A00 |
同上 |
所有 DLL 中的 checkLicense 函数代码完全相同,因为它们都编译了同一个 RPluginBase::checkLicense() 实现。
8.6 checkLicense 补丁方案
将 checkLicense 函数的前 8 字节替换为 B0 01 C3 90 90 90 90 90:
原始: 48 89 5C 24 10 57 48 81 (8 字节)
mov [rsp+0x10], rbx
push rdi
sub rsp, 0xA0 (开始)
替换: B0 01 C3 90 90 90 90 90 (8 字节, 等长)
mov al, 1 ; 始终返回 true (已授权)
ret ; 立即返回
nop x 5 ; 填充至等长
为什么可以安全替换? 函数在 sub rsp, 0xA0 之前就返回,栈指针尚未被修改,ret 指令能正确弹出返回地址。安全 Cookie 检查被跳过,但由于函数未修改栈,不存在栈溢出风险。
9. checkPluginLicense 函数定位与补丁
9.1 函数定位
checkPluginLicense 是 RPluginLoader 类的静态方法,位于 qcadcore.dll 中。它是插件引擎检查许可证的入口点,被 JavaScript 代码调用。
通过 pefile 解析导出表找到:
qcadcore.dll:
?checkPluginLicense@RPluginLoader@@SA_NPEAVQObject@@@Z @ 0xF2C10
?checkPluginLicenses@RPluginLoader@@SA_NXZ @ 0xF2C40
?loadPlugin@RPluginLoader@@SAXPEAVQObject@@_NAEBVQString@@2@Z
?loadPlugins@RPluginLoader@@SAX_N@Z
9.2 函数代码分析
读取 checkPluginLicense 函数的代码(RVA 0xF2C10):
48 83 EC 28 sub rsp, 0x28 ; 分配栈空间
48 85 C9 test rcx, rcx ; 检查插件指针是否为空
74 20 jz short return_true ; 为空则返回 true
48 8B 01 mov rax, [rcx] ; 加载 vtable 指针
48 8D 15 ... lea rdx, [rip+...] ; 加载 RPluginBase 类名
FF 50 08 call [rax+0x08] ; 调用 qobject_cast (vtable+0x08)
48 8B C8 mov rcx, rax ; 转型结果
48 85 C0 test rax, rax ; 检查转型是否成功
74 0B jz short return_true ; 失败则返回 true
48 8B 00 mov rax, [rax] ; 加载 vtable
48 83 C4 28 add rsp, 0x28 ; 平衡栈
48 FF 60 38 jmp [rax+0x38] ; 尾调用 checkLicense() (vtable+0x38)
return_true:
B0 01 mov al, 1 ; 返回 true
48 83 C4 28 add rsp, 0x28 ; 平衡栈
C3 ret
关键发现: checkLicense() 虚函数位于 vtable 偏移 0x38 处。
函数流程:
checkPluginLicense(QObject* plugin)
├─ plugin == null → return true
├─ qobject_cast<RPluginBase*>(plugin)
│ ├─ 转型失败 → return true
│ └─ 转型成功 → tail call checkLicense() via vtable[0x38]
└─ checkLicense() 返回布尔值
9.3 checkPluginLicense 补丁方案
将 checkPluginLicense 函数的前 8 字节替换为 B0 01 C3 90 90 90 90 90:
原始: 48 83 EC 28 48 85 C9 74 20 (8 字节)
sub rsp, 0x28
test rcx, rcx
jz short +0x20
替换: B0 01 C3 90 90 90 90 90 (8 字节, 等长)
mov al, 1 ; 始终返回 true
ret ; 立即返回
nop x 5 ; 填充至等长
为什么可以安全替换? 函数在 sub rsp, 0x28 之前就返回,栈指针未被修改。ret 指令正确弹出返回地址。
10. 工具开发历程
13.1 版本时间线
v1.0 — 初版 (3.32.2)
│ 基于 52pojie 的 macOS 分析思路
│ 手动 IDA Pro 分析 Windows 3.32.2
│ 找到 isTrial @ 0x28
│
├─ v2.0 — 3.32.9 适配
│ QCAD 升级到 3.32.9, Qt5→Qt6
│ 所有偏移量失效, 重新分析
│ isTrial 从 0x28 变为 0x48
│
├─ v3.0 — 灾难性错误
│ 8→3 字节替换, 导出表损坏
│ qcadprojsapi.dll 加载失败
│ 13 个文件全部恢复备份
│
├─ v4.0 — 等长替换
│ 修复为 8→8 字节等长替换
│ 导出表完整保留
│ 支持 3.32.2 / 3.32.9 双版本
│
├─ v5.0 — Qt5/Qt6 双版本
│ 发现 Qt5/Qt6 偏移不同
│ 增加 Qt 版本自动检测
│
├─ v5.2 — GUI 工具
│ 增加 tkinter 图形界面
│ 支持扫描、修补、恢复、校验
│
├─ v5.3 — 性能优化
│
└─ v5.4 — 三层补丁 (当前)
发现 isTrial 补丁不足
新增 checkLicense + checkPluginLicense 补丁
三步彻底解决试用提示
扫描速度从 16 秒 → 174 毫秒
移除 pefile 依赖
92 倍性能提升
13.2 工具架构
patch_qcad.py (CLI) qcad_patch_gui.py (GUI)
┌──────────────────────┐ ┌──────────────────────┐
│ argparse 命令行解析 │ │ tkinter 图形界面 │
│ ├─ info │ │ ├─ 目录选择 │
│ ├─ scan │ │ ├─ 文件列表 │
│ ├─ patch │ │ ├─ 扫描/修补/恢复 │
│ └─ restore │ │ └─ 详细状态显示 │
├──────────────────────┤ ├──────────────────────┤
│ 核心引擎 (共享) │◄───────│ 核心引擎 (共享) │
│ ├─ detect_qt() │ │ ├─ detect_qt() │
│ ├─ get_patterns() │ │ ├─ get_patterns() │
│ ├─ scan_file() │ │ ├─ scan_directory() │
│ └─ apply_patches() │ │ └─ apply_patches() │
└──────────────────────┘ └──────────────────────┘
13.3 核心代码 (CLI 版本)
# Qt5 特征码: isTrial @ [rcx+0x28]
P_QT5 = bytes.fromhex("80 79 28 00 0F 94 C0 C3")
R_QT5 = bytes.fromhex("80 79 28 00 B0 00 90 C3")
# Qt6 特征码: isTrial @ [rcx+0x48]
P_QT6 = bytes.fromhex("80 79 48 00 0F 94 C0 C3")
R_QT6 = bytes.fromhex("32 C0 C3 90 90 90 90 90")
def detect_qt(install_dir: Path) -> str:
if (install_dir / "Qt6Core.dll").exists(): return "Qt6"
if (install_dir / "Qt5Core.dll").exists(): return "Qt5"
return "?"
def scan_file(data: bytes, qt_ver: str) -> list[tuple]:
"""快速扫描:只搜索与 Qt 版本匹配的模式"""
results = []
for find_pat, repl_pat, label in get_patterns(qt_ver):
offs = []
s = 0
while True:
o = data.find(find_pat, s)
if o < 0: break
offs.append(o)
s = o + 1
patched = data.find(repl_pat) >= 0
if offs or patched:
results.append((label, offs, patched, find_pat, repl_pat))
return results