Typora破解

样本是官方 Typora 1.14.9 (build 7785),Developer ID Abner Lee (9HWK5273G4),公证过,Hardened Runtime 开着。激活不在前端 JS 里,而在原生类 LicenseManager / Crypto

结论先放前面:服务端 RSA 只保护「这次激活事务」,不保护「这台机器现在能不能用」。 启动时只看本地 AES 字典里有没有 email + license 两个 key,再加上内存里一个 ObjC 布尔。会看二进制的人,绕过是小时级,不是「需要 RSA 私钥」级。

下面按机制、再按四种方式写。每种方式拆成 原理过程

授权判定的两条路径

样本和入口

版本 1.14.9 / 7785
bundle abnerworks.Typora
二进制 Mach-O universal,x86_64 + arm64
Hardened Runtime 开(flags=0x10000 runtime
关键 entitlements allow-dyld-environment-variablesallow-jitallow-unsigned-executable-memory

allow-dyld-environment-variables=true 等于主动放行 DYLD_INSERT_LIBRARIES。Hardened Runtime 在这里被自己拆掉一角。

授权 UI 是 license.html / Welcome,JS 只负责把输入丢给 native:

1
Setting.invokeWithCallback("addLicense" | "offlineActivation" | "license.machineCode")

真正干活的是 -[LicenseManager activate:with:force:callback:]

机制:写入有 RSA,启动没有

联网 / 离线激活(有验签)

  1. 邮箱正则。
  2. quickValidateLicense: —— 只做格式和弱校验和,能过这一步不代表服务器认。
  3. POST JSON 到 store.typora.iodian.typora.com.cn/api/client/activate
  4. 响应 code==0 之后,writeLicenseInfo:with:from: 才走 verifySig::RSA-2048 / SHA-256,公钥嵌在二进制里。
  5. 通过才把 _hasLicense = YES,AES 写本地文件,再通知 WebView。

离线激活把用户粘贴的 JSON 送进同一条 writeLicenseInfo,所以伪造离线包过不了,一样要合法签名。

序列号在客户端长这样:

  • 正则 ^([A-Z0-9]{6}-){3}[A-Z0-9]{6}$
  • 字母表去掉 0/1/I/O:L23456789ABCDEFGHJKMNPQRSTUVWXYZ
  • 去掉 - 后 24 字符,校验和只覆盖前 16 字符的奇偶位

这只决定「激活按钮能不能点」。checksum 合法的码,服务器照样可以拒。

每次启动(无验签)

1
2
3
4
LicenseManager.start
readLicenseInfo ← 只解密本地文件,不调用 verifySig:
未授权则 showWelcome
renew ← 联网续期,失败不撤本地授权

readLicenseInfo 的判定可以写成一句:

1
2
licensed = (dict["email"] != nil) && (dict["license"] != nil)
_hasLicense = @(licensed)

没有 RSA,没有 MAC,没有把设备字段再核一遍。字典里塞进这两个 key 再加密写回,下次启动就是已授权。

本地授权文件怎么来、怎么判

设备指纹和文件路径:

1
2
3
rawMachineId = IORegistryIOPlatformUUID
fingerPrint = b64(SHA256(uuid))[:10] ,按字符集 /=+- 切开,用 a 拼接
路径 = ~/Library/Application Support/abnerworks.Typora/.<fingerPrint>

本地加密:

1
2
3
key  = SHA256(IOPlatformUUID || "typora-license")   # 32 bytes
CCCrypt AES-256, ECB, PKCS7, IV = NULL
明文 = NSKeyedArchiver(NSMutableDictionary)

ECB、无 HMAC、密钥完全由公开硬件 UUID + 硬编码盐导出。换机器拷贝文件会解不开,在本机等于可任意重加密

运行时真正拦功能的点

关于窗口上的 window._options.hasLicense = true 只是外观。试用过期之后,锁新标签、锁空文档的是 native:

运行时强制点

-[LicenseManager hasLicense] 读 ivar _hasLicense(偏移 0x18)。ivar 为 nil 时默认返回 YES,init 里大约 50 秒后才会写成 kCFBooleanFalse。正常启动 readLicenseInfo 会立刻写成 NSNumber,这个窗口用不上,但默认值偏「已授权」。

-[LicenseManager cannotContonueUse:] 方法名就少写一个 i,hook / 补丁必须一字不差:

1
2
3
if (!force && !_appStarted) return NO;
if ([self hasLicense]) return NO;
return [self dayRemainsFrom:20] < 1;

调用点:

  • ControllerCommands setup(force=1)—— JS 命令通道
  • TyWindowController newTab:(force=0)
  • Document makeWindowControllers(空文档 force=1)
  • 授权面板 URL 参数

试用日写在同一份 AES 文件的 installDate。没有该字段时,回退 ~/Library/Preferences/abnerworks.Typora.plist 的创建时间。

四种方式总览

四种绕过

P0 任意一条就能打穿授权。方式四只续试用钟。

方式 改什么 范围 持久
一、DYLD 注入 hook 两个 ObjC 方法 副本进程
二、伪造本地文件 AES 字典写入 email+license 当前用户所有 Typora
三、二进制补丁 hasLicense 恒真、cannotContonueUse: 恒假 副本二进制 是(对这份副本)
四、重置试用 删隐藏文件 + 重写 plist 创建时间 当前用户试用钟 再 20 天

方式一 DYLD 注入

原理

Hardened Runtime 默认会丢掉 DYLD_* 环境变量。但这份官方 entitlements 明确写了:

1
com.apple.security.cs.allow-dyld-environment-variables

所以 DYLD_INSERT_LIBRARIES 能进进程。库存公证包仍有 Library Validation:异 Team 的 dylib 会被拒。完整链是:

1
2
3
4
复制 .app
→ entitlements 再开 disable-library-validation
→ ad-hoc 重签
→ DYLD_INSERT_LIBRARIES=hook.dylib 启动

allow-dyld-environment-variables 是原包自己放的;disable-library-validation 是后加的。两条都要。

注入之后只换两个 IMP,不改序列号、不碰 AES 文件、不需要 RSA 私钥:

  • -[LicenseManager hasLicense] → 恒 YES
  • -[LicenseManager cannotContonueUse:] → 恒 NO

顺手把 ivar _hasLicense 写成 @YES,再发通知让 WebView 跑 fillLicense 那串 JS,关于窗口也会变成已激活外观。

过程

  1. 从 DMG 拷一份 Typora.app,不要动 /Applications 里正在用的那份。
  2. 原 entitlements 导出后补上:
1
2
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
  1. ad-hoc 重签:
1
codesign --sign - --deep --force --entitlements ent.plist Typora.poc1.app
  1. 写一个 dylib,在 constructormethod_setImplementation。选择子必须用字符串,因为原方法名拼错了:
1
2
3
4
5
6
7
8
9
10
11
12
13
static BOOL hooked_hasLicense(id self, SEL _cmd) {
return YES;
}

static BOOL hooked_cannotContinue(id self, SEL _cmd, BOOL force) {
return NO;
}

SEL cannotSel = NSSelectorFromString(@"cannotContonueUse:");
method_setImplementation(class_getInstanceMethod(cls, @selector(hasLicense)),
(IMP)hooked_hasLicense);
method_setImplementation(class_getInstanceMethod(cls, cannotSel),
(IMP)hooked_cannotContinue);
  1. 启动:
1
2
3
DYLD_INSERT_LIBRARIES=./hook.dylib \
TYPORA_POC_LOG=/tmp/typora_poc1_hook.log \
open -a Typora.poc1.app
  1. 对照:同一份 dylib 插进未重签的库存公证包,Library Validation 会挡住,hook 日志不应出现。

确认:日志里有「已替换 hasLicense / cannotContonueUse:」,随后有 hasLicense 被调用 → 强制 YES。菜单 Help → License 不再催激活。试用没过期时功能上本来就能用,以日志和过期后是否锁新标签为准

方式二 伪造本地授权文件

原理

这是最能说明「启动不验签」的一条。

readLicenseInfo 启动时:

  1. ~/Library/Application Support/abnerworks.Typora/.<fingerprint>
  2. AES-256-ECB 解密,密钥 SHA256(IOPlatformUUID + "typora-license")
  3. NSKeyedUnarchiver 成字典
  4. 只要同时有 emaillicense 两个 key,_hasLicense = YES
  5. 这里不调用 verifySig:

RSA 只出现在联网/离线激活的写入路径。持久化文件没有完整性,本机可以按同样算法重加密。

占位内容故意一眼假即可,不必是服务器签过的码,客户端启动也不再拿去验:

1
2
email    poc-verify@local.test
license LLLLLL-LLLLLL-LLLLLL-LLLLLL

后一个串能过客户端 quickValidateLicense: 的字母表和 checksum,服务器不会签它。对方式二无所谓,因为根本不走服务器。

过程

  1. 取本机 UUID:
1
ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID
  1. 算指纹和 AES key:
1
2
3
4
5
6
import base64, hashlib, re

uuid = "……" # IOPlatformUUID
fp = "a".join(re.split(r"[/=+\-]", base64.b64encode(hashlib.sha256(uuid.encode()).digest()).decode()[:10]))
key = hashlib.sha256((uuid + "typora-license").encode()).digest()
# 文件:~/Library/Application Support/abnerworks.Typora/.<fp>
  1. 构造 NSKeyedArchiver 字典,至少包含 emaillicense,建议保留原来的 installDate
  2. CCCrypt AES-256-ECB PKCS7 加密后覆盖隐藏文件。写之前先备份。
  3. pkill Typora 再打开,避免热进程把旧状态写回。
  4. Help → License / 关于,邮箱和序列号应是上面那对占位符。

对照:把备份盖回去,字典里如果只剩 installDate,客户端会当成未授权。

副作用:改的是当前用户的 Application Support,/Applications/Typora.app 和任何副本都会读这份文件。验证完必须还原。

方式三 二进制补丁

原理

授权强制点收在两个 ObjC 方法里,函数体很短,适合直接改字节。

方法 原语义 补丁
-[LicenseManager hasLicense] 读 ivar _hasLicense return 1
-[LicenseManager cannotContonueUse:] 无授权且试用 < 1 天则拦截 return 0

arm64 上 hasLicense 整函数 20 字节。原逻辑:

1
2
3
4
5
ldr x0, [x0, #0x18]     ; _hasLicense
cbz x0, +8 ; nil → YES
b -[NSNumber boolValue]
mov w0, #1
ret

改成:

1
2
3
4
5
mov w0, #1
ret
nop
nop
nop

universal 二进制两片都要改。只改 arm64 的话,Rosetta 跑 x86_64 仍是旧逻辑。

补丁后 CDHash 对不上,必须:

1
codesign --sign - --deep --force Typora.poc3.app

这就是「改字节 + 重签」。Gatekeeper 第一次会提示,点一次仍可跑。没有运行时完整性校验,程序不会因为自己被 ad-hoc 重签而拒绝工作。

过程

  1. 从库存 DMG 再拷一份副本(不要和方式一的注入副本混用)。
  2. 用特征码定位,不要写死文件偏移。出现 0 次或多次都拒绝写入。
  3. Typora.app/Contents/MacOS/Typora 的 arm64、x86_64 两 slice 分别打补丁。
  4. ad-hoc 重签整个 .app
  5. 打开副本。codesign -dv 应是 Signature=adhoc,不再是 Abner Lee

用 otool 直接看入口:

1
2
3
otool -tv -arch arm64 \
-p '-[LicenseManager hasLicense]' \
Typora.poc3.app/Contents/MacOS/Typora

开头应变成 mov w0, #0x1; ret。试用过期后,未补丁进程锁新标签,补丁进程不锁。

方式四 重置 20 天试用

原理

未授权时 cannotContonueUse: 用:

1
2
剩余天数 = 20 - (今天 - installDate)
剩余 < 1 → 拦截

installDate 在 AES 授权文件里。文件缺失该字段时,回退到 Preferences plist 的 birthtime

只删 .<fingerPrint> 不够:plist 创建时间还在,试用不会回到 20。两条都要动。

这不是授权绕过,是试用钟。过了 20 天还可以再来一次,除非以后改成 Keychain + 首次网络时间交叉。

过程

当前还在试用期内的话,直接把日期改成「今天」,界面几乎看不出差别。要亲眼看锁 / 解锁:

  1. 先把 installDate 写成 21 天前,并去掉 email / license(否则会被当成已授权,试用锁走不到)。
  2. 打开 Typora,应出现试用结束,新标签 / 空文档被拦。
  3. 删掉 Application Support 里那份隐藏授权文件。
  4. ~/Library/Preferences/abnerworks.Typora.plist 按原字节写回一遍,让 birthtime 变成现在(内容可以不变)。macOS 上 cp 往往保留 birthtime,需要删了再写,或用 SetFile
  5. 再打开,程序会自己把授权文件写回来,installDate 接近现在,剩余约 20 天。

建议先备份隐藏文件和 plist,看完立刻还原。官方安装同样读这两处。

对照:什么没用

手法 结果
只改 JS hasLicense 试用期内看起来激活了;过期后 native 仍锁功能
按校验和生成序列号骗服务器 checksum 可算,没有私钥过不了服务端
自签激活服务器 / 伪造离线 JSON 要换嵌在包里的 RSA 公钥,等于还得做方式三
Frida / lldb 改 _hasLicense 本次进程有效,和方式一同类,重启失效

根因

  1. 校验与强制分离。 密码学只出现在激活响应;强制点是内存布尔 + 本地文件两个字符串 key。
  2. 本地密文无完整性。 AES-ECB + 可导出密钥,等于混淆。
  3. Hardened Runtime 放行 DYLD insert。 注入改 ObjC 方法是官方允许的加载路径。
  4. 前端授权位可写。 增加「看起来激活了」的误判,不构成防护。
  5. 无运行时完整性。 不校验自己的 CDHash,被 ad-hoc 重签照样跑。

若这套逻辑要上线,最低限度是:每次启动用嵌入公钥验签持久化 payload(email / license / device / exp);关掉 allow-dyld-environment-variables;AES 换成 AEAD,key 进 Keychain;试用时间不要只靠一个可删文件的时间戳。客户端 DRM 对有二进制能力的人做不到绝对安全,但当前这套连业余静态分析都挡不住。

样本和汇编都基于 1.14.9。换版本后偏移和特征码会变,判定结构——启动不验签、两个 ObjC 布尔——才是可以跟着看的东西。


Typora破解
https://noeplex.com/2026/09/12/Typora破解/
作者
To1y5
发布于
2026年9月12日
许可协议