吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 765|回复: 1
收起左侧

[MacOS逆向] Mole许可证破解教程

[复制链接]
HackXK 发表于 2026-5-15 12:34

0x00 前言

Mole 是一款 macOS 系统清理/优化工具,作者 tw93,使用 SwiftUI 开发。本文记录对其许可证验证机制的逆向分析与破解过程,仅供学习交流。

目标版本:Mole 1.3.0 (Build 5)
架构:Universal Binary (x86_64 + arm64)
工具:IDA Pro 、Python3
难度:中等  


0x01 基本信息收集

字段
Bundle ID com.tw93.MoleApp
最低系统 macOS 14.0
语言 Swift (SwiftUI)
签名 Developer ID + Hardened Runtime
许可证服务 DodoPayments
自动更新 Sparkle 框架

通过 lipo -detailed_info 确认 Fat Binary 结构:

architecture x86_64
    offset 16384
    size 3263952
architecture arm64
    offset 3293184      ← 0x324000
    size 3062992

0x02 许可证系统架构分析

关键类与枚举

从 ObjC metadata 提取到的许可证相关类:

_TtC4Mole14LicenseManager      ← 许可证管理核心
_TtC4Mole11LicenseGate          ← 许可证门控 UI

LicenseStatus 枚举

通过逆向 StatusPill 的分支逻辑,还原出枚举的 tag 值:

Tag 状态 说明
0 active 已激活
1 invalid 无效
2 trial 试用
3 expired 已过期

API 端点

服务器基址:https://live.dodopayments.com

路径 方法 用途
/licenses/activate POST 激活密钥
/licenses/validate POST 验证密钥
/licenses/deactivate POST 停用设备

持久化存储

  • UserDefaults domain: com.tw93.MoleApp
  • licenseLastValidated: 上次验证时间戳
  • _cachedLicenseKey: 缓存的密钥
  • license_key_instance_id: 设备指纹

0x03 逆向分析过程

3.1 定位许可证状态初始化

IDA 加载 arm64 slice,从字符串入手。搜索 licenseLastValidated 定位到激活成功路径,再向上追溯找到状态设置逻辑。

关键函数 sub_1000DE6B8(启动时的许可证检查)流程:

App 启动
    │
    ▼
读取缓存的 LicenseKey (UserDefaults)
    │
    ├── 有 key + 有 instanceId → 发起 validate 请求验证
    │
    └── 无 key 或无 instanceId → 设置 _status = trial ← 我们要改这里

0x1000DE834 处找到"无缓存 key"的分支:

1000de834  LDR X19, [X22,#0x100]     ; 加载 status 值指针
1000de838  LDR X20, [X22,#0xD8]      ; 加载 LicenseManager 实例
1000de83c  ADRL X0, unk_1002A7F68    ; LicenseStatus 类型元数据
1000de844  ADRL X1, unk_100211010
1000de84c  BL sub_10000AF58          ; 获取类型元数据
1000de850  MOV X3, X0
1000de854  LDUR X8, [X0,#-8]
1000de858  LDR X8, [X8,#0x38]        ; 获取 storeEnumTag 函数指针
1000de85c  MOV X0, X19               ; status 值指针
1000de860  MOV W1, #2                ; ★ tag = 2 (trial)
1000de864  MOV W2, #4                ; numCases = 4
1000de868  BLR X8                    ; 调用 storeEnumTag
1000de86c  MOV X0, X19
1000de870  BL sub_1000DDCB4          ; 写入 _status 并触发 UI 更新

关键发现MOV W1, #2 就是把状态设为 trial(tag=2)。改成 MOV W1, #0 即可设为 active(tag=0)。

3.2 定位 LicenseGate 门控

LicenseGate 是一个全屏覆盖层,阻止未激活用户使用 app。通过追踪 LicenseGate 类的初始化和引用,找到 sub_1000DE56C

1000de56c  LDR W8, [X22,#0x60]    ; 读取状态比较结果(0=active)
1000de570  LDR X9, [X22,#0x58]    ; 读取 LicenseGate 对象指针
1000de574  CMP W8, #0             ; 是否为 active?
1000de578  CSET W8, EQ            ; ★ 如果是 active,W8=1;否则 W8=0
1000de57c  STRB W8, [X9,#0x70]    ; 写入 gate 的 isActive 标志位

[X9, #0x70] 是 LicenseGate 对象的布尔字段:

  • 1 = 已激活,gate 放行
  • 0 = 未激活,显示许可证弹窗

关键发现CSET W8, EQ 根据状态判断是否放行。改成 MOV W8, #1 即永远放行。

3.3 验证 StatusPill 逻辑

右上角的状态标签由 sub_1000EC2FC 控制:

1000ec414  MOV W1, #4          ; 比较参数
1000ec418  BLR X8              ; 调用 getEnumTag
1000ec41c  MOV X8, X0          ; 保存 tag 值
1000ec420  CMP W0, #1
1000ec424  B.LE loc_1000EC460  ; tag <= 1 → 不显示 pill(active/invalid)
1000ec428  CMP W8, #2
1000ec42c  B.EQ loc_1000EC490  ; tag == 2 → 显示 "license.pill.trial"
1000ec430  CMP W8, #3
1000ec434  B.NE loc_1000EC49C  ; tag == 3 → 显示 "license.pill.expired"

_status = active (tag=0) 时,B.LE 自然成立,pill 不显示。无需额外 patch


0x04 Patch 方案

最终只需要 2 处 patch,改动极小:

Patch 1:LicenseGate 永远放行

项目
虚拟地址 0x1000DE578
文件偏移 0x402578
原始指令 CSET W8, EQ (0x1A9F17E8)
修改为 MOV W8, #1 (0x52800028)
效果 LicenseGate.isActive 永远为 true

Patch 2:_status 初始化为 active

项目
虚拟地址 0x1000DE860
文件偏移 0x402860
原始指令 MOV W1, #2 (0x52800041)
修改为 MOV W1, #0 (0x52800001)
效果 启动时 _status = active 而非 trial

文件偏移计算公式

文件偏移 = arm64_slice_offset + (VA - __TEXT_vmaddr)
         = 0x324000 + (VA - 0x100000000)

0x05 Patch 实施

方法一:Python 脚本(推荐)

import shutil
import struct

src = './Mole.app/Contents/MacOS/Mole'
dst = './Mole.app/Contents/MacOS/Mole'  # 直接改原文件,记得先备份

# 备份
shutil.copy2(src, src + '.bak')

patches = [
    # Patch 1: LicenseGate 永远放行
    (0x402578, struct.pack('<I', 0x52800028)),
    # Patch 2: _status = active
    (0x402860, struct.pack('<I', 0x52800001)),
]

with open(dst, 'r+b') as f:
    for offset, new_bytes in patches:
        f.seek(offset)
        old = f.read(4)
        f.seek(offset)
        f.write(new_bytes)
        print(f'[OK] 0x{offset:06X}: {old.hex()} → {new_bytes.hex()}')

print('Patch 完成!')

方法二:十六进制编辑器

用 010 Editor / Hex Fiend 打开 Mole.app/Contents/MacOS/Mole

偏移 搜索(原始) 替换为
0x402578 E8 17 9F 1A 28 00 80 52
0x402860 41 00 80 52 01 00 80 52

⚠️ 注意:第二处 41 00 80 52 在文件中可能出现多次,务必定位到 0x402860 精确修改。

重签名

Patch 后原始签名失效,需要重签:

codesign --force --deep --sign - /path/to/Mole.app

0x06 效果验证

验证项 结果
启动是否弹出许可证窗口 否 ✓
右上角是否显示"试用·激活" 否(pill 不显示 = active 状态)✓
内存中 _status 值 active (tag=0) ✓
功能是否正常使用 是 ✓

对比测试

  • 只有 Patch 1(gate 放行)→ 右上角仍显示"试用 · 激活",说明 _status 还是 trial
  • 加上 Patch 2(status = active)→ pill 消失,说明 _status 真正变为 active

0x07 技术要点总结

为什么不 patch 激活网络流程?

最初尝试 patch 激活回调(0x1000DCA9C),强制跳转到成功分支。但成功分支会读取网络响应数据(license key、instance ID 等),而这些数据在网络失败时不存在,导致 访问空指针闪退

为什么选择 patch 状态初始化?

  1. 不依赖网络数据:直接在枚举赋值时改 tag,不需要伪造响应
  2. 改动最小:只改一个立即数(2→0),一条指令
  3. 效果真实:内存中 _status 确实是 active,所有读取状态的地方都会得到正确结果
  4. 无副作用:不会触发网络请求,不会写入错误数据

Fat Binary 处理

IDA 只加载了 arm64 slice,patch 也只改 arm64 部分。在 Apple Silicon Mac 上运行时只会加载 arm64 slice,所以完全没问题。如果需要在 Intel Mac 上使用,需要额外分析 x86_64 slice。

Swift 枚举在内存中的表示

Swift 枚举使用 tag(discriminator)区分 case。通过逆向 storeEnumTag 调用和 StatusPill 的分支逻辑,还原出 tag 与状态的对应关系:

storeEnumTag(ptr, tag, numCases)
- tag 0 → active
- tag 1 → invalid  
- tag 2 → trial
- tag 3 → expired

0x08 免责声明

本文仅供技术学习与研究交流,请勿用于商业用途。如果觉得软件好用,请支持正版:

官网链接:https://mole.fit

免费评分

参与人数 2吾爱币 +2 热心值 +2 收起 理由
Leesly1 + 1 + 1 谢谢@Thanks!
西枫游戏 + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

西枫游戏 发表于 2026-5-15 13:27
佬的好文章必须支持下
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-5-16 03:36

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表