- SignalDesk2小时前
很久以前 crack 过 Esko Studio Toolkit,又翻出来再写一篇 废话不多说上代码,请将脚本放到 Esko\ 目录下 (点击了解更多详细信息) Studio Toolkit 26.07 逆向实录:从零定位许可证检查 第一次启动 Studio Toolkit,程序没有直接进入编辑界面,而是先显示许可证选择窗口:产品密钥、许可证服务器、Esko ID 登录,以及无许可启动。 选择无许可启动后,主界面能够打开,但“新包装袋”“添加标签”“添加封套”等功能仍然是灰色。到这里可以先作出一个判断:启动许可与功能许可不是同一层检查。关闭欢迎窗口只让程序进入主界面,并没有让核心功能获得 capability。 这篇文章从一份未经修改的安装目录开始,按实际逆向顺序定位许可证边界。目标不是修改按钮文本,也不是简单调用 setEnabled(true) ,而是找出 UI、命令层、授权组件和业务对象之间的完整调用链。 第一次运行:只记录,不下断点 第一次启动不使用调试器,完整记录正常行为: 启动程序; 记录许可证选择窗口的所有选项; 选择无许可启动; 进入主界面; 展开工具栏和菜单; 记录灰色按钮及菜单中的“未许可”文本; 正常退出程序。 这一轮要回答三个问题: 程序是否允许无许可进入主界面? 无许可状态只影响 UI,还是也影响文档创建? 许可证窗口在每次启动都出现,还是状态会持久化? 不要在这一阶段修改 UI。按钮灰色本身就是重要证据,它说明某个状态已经从授权层传播到表现层。 3. 观察启动期间的文件和注册表访问 使用 Process Monitor,过滤进程名: Process Name is StudioToolkit.exe 保留以下操作: Load Image CreateFile ReadFile RegOpenKey RegQueryValue TCP Connect 重点寻找: 名称中包含 license、FNP、FlexNet、Esko 的文件 程序目录中的私有 DLL ProgramData 下的许可证数据 用户目录中的配置文件 许可证服务器名称 登录或订阅相关 URL 不要只搜索界面上看到的中文“未许可”。本地化文本往往在资源文件中,而真正的判断可能发生在完全不同的本地组件里。 同时保存运行时模块列表,模块列表通常比字符串搜索更快地暴露授权边界。 4. 从模块列表定位 libFNP 启动过程中可以观察到一个与主程序同名的私有组件: StudioToolkit.exe_libFNP.dll FNP 、特殊节区名称以及库内版本字符串都指向 FlexNet Publisher。对主程序执行: rabin2 -I '.\working\StudioToolkit.exe' rabin2 -S '.\working\StudioToolkit.exe' rabin2 -i '.\working\StudioToolkit.exe' 关键 PE 信息包括: PE32+ AMD64 Windows GUI NX enabled ASLR/PIC enabled signed image 节区表中如果出现: .fnp_dir .fnp_mar 就可以进一步确认 FlexNet 包装边界。授权不是 Qt 内部的单个布尔变量,而是主程序与本地许可组件共同完成的一套状态查询。 5. 不要先搜“license”,先看导入与导出 字符串搜索会产生大量噪声:错误文本、翻译资源、调试符号、日志模板和无关配置都会出现 license 。更有效的方法是先看二进制接口。 读取 libFNP 的导出表: rabin2 -E '.\working\StudioToolkit.exe_libFNP.dll' 这里会发现对外接口没有可靠函数名,而是通过 ordinal 暴露。样本中重点出现两个入口: ordinal 2 ordinal 3 Windows Loader 按 ordinal 查找函数时,内部索引为: index = requestedOrdinal - ExportDirectory.Base functionRva = AddressOfFunctions[index] 因此分析工具显示的 Ordinal_2 只是标签,不能把它当作原始函数名。真正重要的是:谁调用了 ordinal 2、谁调用了 ordinal 3、参数是什么、返回值如何被使用。 6. 先确认 DLL 搜索路径 在进入调试器前,先确认程序实际加载的是哪一个 DLL。相同文件名可能同时存在于: 程序目录 系统目录 PATH 中的第三方目录 旧版本安装目录 当前工作目录 使用 Process Monitor 的 Load Image 事件记录完整路径。如果 DLL 路径不符合预期,后续断点和静态地址都会失去意义。 还要确认 Qt 依赖来自同一套发行目录: Qt6Core.dll Qt6Gui.dll Qt6Widgets.dll plugins\platforms\qwindows.dll Qt 版本混用可能造成无 UI、资源缺失或启动即崩溃,这些现象不能直接归因于许可证。 7. 在加载器边界设置第一组断点 使用 x64dbg 或 WinDbg 启动程序,先设置: kernel32!LoadLibraryA kernel32!LoadLibraryW kernel32!LoadLibraryExW kernel32!GetProcAddress 目标不是停住所有 DLL,而是捕获: libFNP 何时被加载 哪个模块请求它 GetProcAddress 查询了名称还是 ordinal 查询结果保存到哪里 命中 GetProcAddress 时,在 Windows x64 ABI 下: RCX = HMODULE RDX = 函数名指针或整数 ordinal 如果 RDX 的高位为零且值很小,例如 2 或 3 ,通常就是按 ordinal 查询。函数返回后,RAX 是入口地址。记录模块基址并换算 RVA: RVA = FunctionVA - ModuleBase 以后应以“模块 + RVA”记录断点,不要长期保存某次运行中的绝对地址,因为 ASLR 会改变模块基址。 8. 在 ordinal 入口建立第二组断点 定位到 ordinal 2 和 ordinal 3 后,在两个入口设置断点。每次命中记录: 线程 ID RCX/RDX/R8/R9 RSP 栈上的后续参数 调用者返回地址 调用前 LastError 返回后的 RAX Windows x64 中,前四个整数或指针参数通常位于: RCX RDX R8 R9 调用者还会预留 32 字节 shadow space,并在 call 前保持 16 字节栈对齐。浮点参数一般使用 XMM0-XMM3 。 仅凭一次调用不能确定函数原型。至少要比较: 启动许可证窗口时的调用 选择无许可启动时的调用 主界面创建时的调用 展开功能菜单时的调用 点击功能时的调用 保存或导出时的调用 如果同一个入口在不同阶段被调用,它可能不是简单的 isLicensed() ,而是通用消息分派或状态查询接口。 9. 用调用者代码解释返回值 不能看到 RAX=0 就直接写“未许可”。返回值的意义必须从调用者代码恢复。 例如: call ordinal_2 test eax, eax je failure_path 这里零值进入失败路径。 但如果调用者是: call ordinal_2 cmp eax, 4 jne success_path 返回值就是状态码或枚举, 0 未必代表失败。 每次调用后继续单步,记录: 比较指令 条件跳转 写入的对象字段 调用的错误构造函数 最终到达的 UI 或命令分支 真正有价值的证据不是“函数返回了 1”,而是: 某次 ordinal 调用返回某值 -> 调用者执行某个条件跳转 -> 某对象的 capability 字段被写入 -> QAction 状态发生变化 10. 为什么需要透明代理 调试器适合还原少量关键调用,但授权查询可能发生几十次。为了获得完整时间序列,可以构造一个行为透明的同名代理 DLL。 代理结构: StudioToolkit.exe -> 同名代理 DLL -> 加载重命名后的原始 DLL -> 解析原始 ordinal -> 原样转发 -> 记录参数和返回值 代理不能在还未确认原型时随意改返回值。第一版只做观测。 函数指针可以先按调试结果写成最保守的指针参数形式: typedef intptr_t (__fastcall ordinal2_fn)( void , void , void , void , void ); typedef intptr_t (__fastcall ordinal3_fn)(void ); 转发函数保持“调用真实函数、记录、返回真实结果”的顺序: intptr_t result = real_ordinal2(a1, a2, a3, a4, a5); log_call("ordinal2", args, 5, result); return result; 对应 ordinal 的导出可以通过 .def 文件保持: LIBRARY "StudioToolkit.exe_libFNP.dll" EXPORTS FnpOrdinal2 @2 NONAME FnpOrdinal3 @3 NONAME NONAME 表示调用方通过 ordinal 解析,而不是依赖自定义函数名。 11. 透明代理必须保持哪些行为 代理不仅要保持返回值,还要尽量保持所有可观察状态: 调用约定 参数宽度 返回类型 LastError 异常行为 线程安全 加载和卸载顺序 日志函数应保存并恢复 LastError: DWORD saved = GetLastError(); log_call(...); SetLastError(saved); 否则文件操作可能改变线程错误状态,导致代理本身制造行为差异。 每条日志建议包含: 高精度时间戳 进程 ID 线程 ID 调用序号 ordinal 调用者返回地址 真实函数地址 参数寄存器摘要 返回值 调用前后 LastError 12. 避免在 Loader Lock 中做复杂工作 DllMain(DLL_PROCESS_ATTACH) 在 Loader Lock 下运行。这里不适合执行: 复杂 LoadLibrary 链 等待线程 初始化 Qt 网络请求 弹出窗口 长时间文件操作 较稳妥的结构是: DllMain -> 保存自身 HMODULE -> DisableThreadLibraryCalls 第一次 ordinal 调用 -> InitOnceExecuteOnce -> 构造原始 DLL 的绝对路径 -> LoadLibraryEx -> GetProcAddress -> 转发 如果代理版本表现为偶尔启动、偶尔没有 UI,先检查 Loader Lock 和初始化竞争,不要立即推断程序存在反调试。 13. 从日志中划分授权阶段 完成透明转发后,按固定顺序操作一次: 启动程序; 等待许可证窗口; 选择无许可启动; 打开主界面; 展开相关菜单; 创建空白文档; 尝试触发目标功能; 保存文档; 退出程序。 在日志中插入手工时间标记,将 ordinal 调用分成: 进程初始化 许可证来源选择 产品枚举 主窗口初始化 菜单状态刷新 命令执行 保存/导出 进程退出 如果某个返回值只在菜单初始化时影响按钮,它可能属于显示 capability。如果保存时又出现另一组查询,就说明执行和持久化存在二次检查。 14. 从灰色按钮反向追踪 目标按钮灰色时,先找谁调用了: QAction::setEnabled(false) QWidget::setEnabled(false) 在对应调用处查看上层逻辑。常见结构类似: bool enabled = documentReady && selectionValid && featureAvailable; action->setEnabled(enabled); 此时灰色不一定全部来自许可证。还可能受以下状态影响: 没有打开文档 对象类型不匹配 当前选择为空 处于只读模式 插件初始化失败 缺少许可证 capability 因此要分别改变文档和选择状态,观察最终布尔值中哪一项保持为 false。只有当其他条件均满足、许可相关条件仍然为 false,才能把它归因于授权。 15. 不要把 UI 点亮当作结果 直接调用: action->setEnabled(true); 只能验证按钮后面是否连接了 handler,不能证明功能已经可用。 完整链路应当是: QAction triggered -> Qt slot -> command/controller -> capability query -> 创建业务对象 -> 更新 undo stack -> 标记文档 dirty -> 保存或导出 -> 重新打开并验证 对每个目标功能记录: 功能 UI 可用 handler 命中 capability 查询 对象创建 保存成功 重开成功 新包装袋 添加标签 添加封套 只有最后几列成立,功能链才算真正走通。 16. 区分网络许可、本地许可和登录订阅 许可证窗口提供多个入口,说明程序支持多个 provider。它们不能混成一个“是否许可”布尔值。 需要分别测试: 未配置许可证服务器 服务器地址无效 服务器不可达 服务器可达但没有产品 服务器返回合法产品 本地离线许可 Esko ID 登录 无许可模式 当界面提示: 许可服务器 localhost 上的 Studio 没有可用许可 它只证明当前路径查询了 localhost ,不能证明所有运行模式都强制依赖网络许可管理器。 记录实际网络连接: Get-NetTCPConnection | Where-Object OwningProcess -eq $StudioToolkitPid 并与 ordinal 日志、窗口操作时间、模块路径对齐。这样才能判断失败
- 情报分类:服务器与云资源
- 分类依据:内容涉及服务器、云资源或网络线路
- 信息来源:服务器 / LINUX DO - 最新话题
- 发布时间:2026/9/25 18:05:47
- 暂无回复