吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 4174|回复: 49
收起左侧

[MacOS逆向] Sublime许可证分析:RSA PKCS#1标准详解+跨平台Keygen [开源]

  [复制链接]
skrets 发表于 2026-2-6 20:29

作为Sublime家的忠实用户,一直都想分析一下,也看过其他大佬的文章,还是决定自己动手试试,探索一下底层的实现细节,顺带研究研究RSA以及相关标准的原理

文中完整地分析了一下RSASSA-PKCS#1-v1.5标准的签名和验证流程,并且简单实现了一下签名的流程

还是再说一下,如果喜欢Sublime家软件的话还是最好支持一下正版哦!(我是学生还没赚钱

本文基于ARM架构的Sublime Merge 2121版本进行分析

下载链接:sublime_merge_build_2121_mac.zip

用到的工具:Hopper, lldb

免责声明

本文仅用于密码学原理、软件许可机制及 PKCS#1 标准的技术研究与学习交流目的。文中涉及的 Sublime Merge 许可证结构分析、算法原理说明及相关示例,均不构成对任何商业软件的破解、绕过授权或非法使用的建议或支持。

本文所提及的 Keygen 相关内容仅作为理论研究、逆向工程方法论探讨及跨平台实现思路的示例说明,不鼓励、不支持、也不倡导将其用于任何违反软件许可协议、法律法规或侵犯软件著作权的行为。任何基于本文内容产生的实际使用行为,均由使用者自行承担相应的法律责任。

Sublime Merge 及其相关商标、版权和软件著作权均归其原作者或所属公司所有。若你在实际工作或长期使用中依赖该软件,请支持正版,购买合法授权以支持开发者。

作者不对因阅读或使用本文内容而导致的任何直接或间接后果承担责任。

定位关键函数

我们依然可以从字符串来入手,随便输入点东西,点 Use License 就会有无效许可证的弹窗

image-20260204165502986.png

在 Hopper 里可以直接搜到这个字符串 "Please check that you have entered all lines from the license key, including the BEGIN LICENSE and END LICENSE lines."

macOS 版本并没有抹去符号,我们可以看到它的函数名是show_invalid_key(void*),查引用可以看到一个switch表的跳转结构

image-20260204165846666.png

这里对应的其他的分支也都是激活失败的弹窗,往上看看

image-20260204170328881.png

可以看到,当_OUTLINED_FUNCTION_11988函数返回值是0x119时才不会走到这个激活失败的分支

通过lldb动态调试可以看到传入的参数,其中x0包含了一个指向许可证字符串的指针,这应该就是核心的验证函数了

这个函数很短,其实是对另一个函数的封装

_OUTLINED_FUNCTION_11988:
add        x4, x8, #0x4
add        x5, x8, #0x10
b          apple_fruit(string const&, string*, int*, int*, int*, bool*)

接下来就是对这个apple_fruit (0x1001cd338) 函数的分析

许可证格式分析 Part1

假装我并不知道Sublime的许可证长什么样,来试试从零把它的许可证格式分析出来

这个函数先上来会判断公钥是否解密并加载公钥,我们先不管它,看看对许可证格式的检查

image-20260204172440723.png

上来先检查许可证里是是否出现了"Preview License",如果有出现就直接用"Single User License"来替代(好粗暴的解决方式

往下可以看到它从栈上分配了一些空间,然后调用解析许可证的函数 (0x10009d8ac),当成功时应返回1

loc_1001cd3cc:
bl         _OUTLINED_FUNCTION_5610
       ; Stored some zeros…
add        x24, sp, #0x28
add        x0, sp, #0x8 ; argument #1
add        x1, sp, #0x70 ; argument #2
add        x2, sp, #0x24 ; argument #3
add        x3, sp, #0x58 ; argument #4
add        x4, sp, #0x40 ; argument #5
add        x5, sp, #0x28 ; argument #6
add        x6, sp, #0xa0 ; argument #7
bl         parse_license(string const&, string*, int*, string*, string*, string*, string*)
cbz        w0, loc_1001cd5e4

这个函数上来就是一波谜之操作,这其实是判断string类型是否用了短字符串优化(SSO),然后取出了data()指针

ldrsb      w8, [x0, #0x17]      ; 先取出string类型的最后一个字节
ldr        x9, [x0]                     ; 取出data()指针(长字符串)
cmp        w8, #0x0
csel       x25, x9, x0, lt      ; w8 < 0 长字符串,否则短字符串(直接从对象指针处开始)

然后是一层循环,通过'\r''\n'把许可证分开并加入一个vector<string>的数组里,这里我改了下函数名方便阅读

image-20260204192054624.png

str_end处退出了分割许可证的循环,然后对拆分出来的vector<string>里面每个字符串调用trim(string&)

就是去除字符串首尾的空格与制表符'\t'

然后下面对把trim后的空字符串erase掉了,就不贴代码了

image-20260204195917336.png

这里把vector<string>end()begin()指针相减,与0x138做比较,一个string0x18,说明许可证应该有13行

而且第一行和第十三行没用会直接删掉,得到中间的十一行

image-20260204200554095.png

这里取出了许可证的第三行进行比较,可以是"Single User License",或者"Unlimited User License",还有一个分支是用atoi()解析了开头的一个整数,这里的w0指的应该是许可证用户数量,为0就是Unlimited

image-20260204201631488.png

然后用strstr判断第四行里是否出现了'-',然后把第四行用-分割,并存到传入参数的地址里

这就是最后一个解析成功的判断了,走通这个它就会返回1表示解析成功

注意这里第四行是可以有两个'-'的,但我好像没见过这样的许可证,不过很快就会知道原因了

后面接着就是把第五行到第十二行拼起来形成一个长字符串,并删去了空白字符,截图略

到这里我们可以总结一下这个parse_license函数从参数返回了哪些值

arg3 -> license seats
arg4 -> line4.split('-')[0].trim()
arg5 -> line4.split('-')[1].trim()
arg6 -> line4.split('-')[2].trim()
arg7 -> concat(line5..12).remove(' ', '\t')

(奇怪的伪代码,意会一下)

回到之前的apple_fruit,我们继续

签名验证分析

parse_license下面接着的是一个很简单的函数

generate_data(string, char* const&, int, string, char* const&, string, char* const&, string, char* const&)

它把parse_license分开来的部分按照许可证用户数量又拼回去了

分别是

1 -> "%s\nSingle User License\n%s-%s"
0 -> "%s\nUnlimited User License\n%s-%s"
? -> "%s\n%d User License\n%s-%s"

这里我们就知道上面的atoi是用来干什么的了,来识别像999 User License这样的许可证

如果第四行有两个-就在后面补一个-%s

这个设计其实还是很合理的,先解析再拼接就不至于说加个空格或者换行许可证就失效了

generate_data拼出这前几行后,就是签名验证了,在apple_fruit里紧接的就是

verify_signature(string const&, string const&, string const&)

lldb里可以看到三个参数分别是:

  1. 前面generate_data生成的字符串
  2. parse_license返回的第5行到第12行拼起来的字符串
  3. DER格式转了16进制的公钥字符串"30819D300D0609..."

这是PEM格式的公钥

-----BEGIN PUBLIC KEY-----
MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDYe6JFYvfF0UoM+xK5dAwZXGvc
fm1uySusDrKdWeHZrmeJDCuIw6vcr/59SjPcwb++UxolHO8Mkj8GvnmyMoVZrP7p
htXhXk0XZupWxOEGV/p02wl3w/t1greM1Huyx/myUrSpRj0V9q5u6SN9VMVIG/Pg
sJkgGQvPsx5b5QnDOwIBEQ==
-----END PUBLIC KEY-----

好,开始分析这个最关键的verify_signature

image-20260204210403696.png

这CFG图看起来还有点小复杂,曾经令我望而却步,只好直接改 ret 1,今天就来啃啃硬骨头

首先根据一些错误提示信息,可以看出它用的加密库是libtomcrypt

函数头部先注册了使用的数学库提供者,还加载了SHA1哈希

adrp       x0, #0x100655000 ; _ltc_mp@PAGE
add        x0, x0, #0xd70 ; _ltc_mp@PAGEOFF, argument "dst" for method imp___stubs__memcpy, _ltc_mp
adrp       x1, #0x100629000 ; _ltm_desc@PAGE
add        x1, x1, #0x890 ; _ltm_desc@PAGEOFF, argument "src" for method imp___stubs__memcpy, _ltm_desc
mov        w2, #0x1a0   ; argument "n" for method imp___stubs__memcpy
bl         imp___stubs__memcpy ; memcpy
adrp       x22, #0x100654000 ; _hash_descriptor@PAGE
add        x22, x22, #0x370 ; _hash_descriptor@PAGEOFF, _hash_descriptor
mov        w24, #0x20
adrp       x23, #0x100629000 ; _sha1_desc@PAGE
add        x23, x23, #0xa30 ; _sha1_desc@PAGEOFF, _sha1_desc

后面是一些循环用来获取SHA1的具体函数指针,看起来是内联展开的find_hash函数

再往下走可以看到它计算了generate_data返回的字符串的SHA1值

image-20260204212700309.png

接着把16进制编码的公钥转换成字节数据

ldrb       w8, [x21, #0x17]
sxtb       w9, w8
ldr        w10, [x21, #0x8]
cmp        w9, #0x0
csel       w23, w10, w8, lt
mov        w8, #0x2
ldr        x9, [x21]
csel       x0, x9, x21, lt ; argument #1 for method hex_to_bytes(char const*, unsigned char*, int)
sdiv       w22, w23, w8
add        x1, sp, #0x1, lsl #12
add        x1, x1, #0x148 ; argument #2 for method hex_to_bytes(char const*, unsigned char*, int)
       ; x1 -> 输出字节
       ; x0 -> 16进制字符串的长度
       ; x2 -> 输出字节的长度
mov        x2, x22      ; argument #3 for method hex_to_bytes(char const*, unsigned char*, int)
bl         hex_to_bytes(char const*, unsigned char*, int)
adrp       x8, #0x100655000 ; _ltc_mp@PAGE
ldr        x8, [x8, #0xd70] ; _ltc_mp@PAGEOFF, _ltc_mp
cbz        x8, loc_1000e94e8

这里紧接着的这个复杂的函数块,调用了ltc_init_multider_decode_sequence_exder_decode_sequence_multi等一些偏低层的复杂函数

image-20260204222639479.png

但如果仔细看上面的错误日志,很容易猜到这是导入RSA密钥的函数,我开始还在这里卡了好一会

"../../../third_party/libtomcrypt/src/pk/rsa/rsa_import.c"

RSASSA-PKCS#1-v1.5详解

加载完公钥接下来就是用公钥进行RSA的签名验证了,这整个流程在这里被展开了,我们一步一步地来分析

loc_1000e90dc:
str        w23, [sp, #0x3a2 + var_2B2] ; CODE XREF=verify_signature+1632
bl         _OUTLINED_FUNCTION_4513 ; _OUTLINED_FUNCTION_4513
ldrb       w8, [x20, #0x17]
bl         _OUTLINED_FUNCTION_8937 ; _OUTLINED_FUNCTION_8937
ldp        x10, x9, [x20]
       ; x23 -> line 5..12 字符串的长度
csel       x23, x9, x8, lt
lsr        x22, x23, #0x1
csel       x0, x10, x20, lt ; argument #1 for method hex_to_bytes(char const*, unsigned char*, int)
       ; x1 -> 输出字节
add        x1, sp, #0x148 ; argument #2 for method hex_to_bytes(char const*, unsigned char*, int)
       ; x22 -> 输出字节的长度
mov        x2, x22      ; argument #3 for method hex_to_bytes(char const*, unsigned char*, int)
       ; x0 -> line 5..12 字符串
bl         hex_to_bytes(char const*, unsigned char*, int) ; hex_to_bytes(char const*, unsigned char*, int)
       ; 16进制字符串转字节数据
adrp       x20, #0x100655000 ; qword_100655dd8@PAGE
add        x20, x20, #0xdd8 ; qword_100655dd8@PAGEOFF, qword_100655dd8
ldr        x8, [x20]    ; qword_100655dd8
ldr        x0, [sp, #0x3a2 + var_29A]
       ; x8 -> count_bits -> 0x400 (1024位) 获取RSA公钥模数大小
blr        x8
mov        x21, x0
ldr        x8, [x20, #0x28] ; qword_100655e00
ldr        x0, [sp, #0x3a2 + var_29A]
       ; unsigned_size 模数写作字节的长度
blr        x8
cmp        x0, x23, lsr #1
       ; x0 -> 模数字节长度 (1024 / 8 = 128)
       ; x23 -> line 5..12 字符串长度 / 2
b.ne       loc_1000e9478

首先判断了签名数据的长度,把它与公钥模数的位数对比

从上面可以看出来许可证第5行到第12行都是16进制字符串,且加起来*长度为128 2 = 256**

mov        x20, x0
str        x22, [sp, #0x3a2 + var_262]
adrp       x8, #0x100655000 ; qword_100655ef0@PAGE
ldr        x8, [x8, #0xef0] ; qword_100655ef0@PAGEOFF, qword_100655ef0
       ; x0 -> 转换后的签名数据
add        x0, sp, #0x148
       ; x3 -> int *outlen
add        x3, sp, #0x140
add        x5, sp, #0xf0
       ; x22 -> inlen 0x80 固定的输入长度
       ; x20 -> unsigned char *out
bl         _OUTLINED_FUNCTION_3909 ; _OUTLINED_FUNCTION_3909
mov        w4, #0x0
       ; rsa_exptmod 底层的RSA模幂运算
blr        x8
mov        w23, #0x0
cbnz       w0, loc_1000e94cc

接下来直接对这个签名数据进行模幂运算,也就是RSA的底层数学运算

RSA数学原理

这里稍微回顾一下RSA加密解密的底层数学原理

$N$ 是两个素数 $p,q$ 的乘积,取 $r=\varphi(N)$(欧拉函数),$e$ 是一个与 $r$ 互质的数,$d$$e$ 关于 $r$ 的模逆元(即 $ed\equiv1\pmod{r}$

定义公钥 $(N,e)$,私钥 $(N,d)$,需要加密的消息 $m$,加密过的消息 $c$

那么加密操作为 $c=m^d\pmod{N}$

对应解密操作为 $m=c^e\pmod{N}$

这个加解密过程的正确性这里就不讨论了,感兴趣可以去百科上了解详细的数学证明

那么在这里 rsa_exptmod 干了什么呢?

首先把输入的签名数据大端序当作一个大整数读入作为密文 $c$,根据公钥提供的模数和指数计算 $m = c^e\pmod{N}$,再把 $m$大端序写为输出的数据得到编码消息

因为这里的运算都是在模域下进行的,也就是对 $N$ 取模,所以输入输出的大小都会是和 $N$ 同样的 1024 位(128字节)

好,那我们回到签名验证流程接着看

image-20260205204417210.png

PKCS #1 v1.5 Padding

这部分在读取编码消息EM开头的 PKCS #1 v1.5 Padding,它长这样:

EM = 0x00 || 0x01 || PS || 0x00 || M

01是固定开头,中间的PS为至少8字节的0xFF填充,M 是实际的消息(第二位的1表示签名,若为2则表示加密)

这里根据前两位来判断是否有Padding,w8是标记的flag,x10为Padding的总长度

读取后会有判断,Padding的长度要大于等于11,否则就会直接跳转到验证失败的分支(这是 PKCS #1 v1.5 标准的要求)

       ; x10 -> padding 长度
       ; x20 -> rsa解密后的数据
add        x1, x20, x10
mov        x0, x21
bl         outline_memcpy_x23 ; outline_memcpy_x23
mov        w8, #0x7
str        w8, [sp, #0x3a2 + arg_1EF6]
sub        x8, fp, #0xd8
mov        w9, #0x10
str        x8, [sp, #0x3a2 + arg_1EFE]
str        x9, [sp, #0x3a2 + arg_1F06]
mov        w8, #0x6
str        wzr, [sp, #0x3a2 + arg_1F0E]
str        w8, [sp, #0x3a2 + arg_1F36]
str        xzr, [sp, #0x3a2 + arg_1F46]
str        xzr, [sp, #0x3a2 + arg_1F3E]
str        wzr, [sp, #0x3a2 + arg_1F4E]
mov        w8, #0xd
str        w8, [sp, #0x3a2 + arg_1E76]
add        x8, sp, #0x2, lsl #12
add        x8, x8, #0x248
mov        w9, #0x2
str        x8, [sp, #0x3a2 + arg_1E7E]
str        x9, [sp, #0x3a2 + arg_1E86]
mov        w8, #0x5
str        wzr, [sp, #0x3a2 + arg_1E8E]
str        w8, [sp, #0x3a2 + arg_1EB6]
str        x20, [sp, #0x3a2 + arg_1EBE]
str        x22, [sp, #0x3a2 + arg_1EC6]
str        wzr, [sp, #0x3a2 + arg_1ECE]
add        x2, sp, #0x2, lsl #12
add        x2, x2, #0x1c8
       ; x2 = $sp + 0x21c8
       ; -> 2 ltc_asn1_list
bl         _OUTLINED_FUNCTION_3246 ; _OUTLINED_FUNCTION_3246
       ; w3 = 2 -> outlen
mov        w3, #0x2     ; argument #4 for method outline_der_decode_sequence_ex
       ; x0 <- x21 (unpadded data)
       ; x1 <- x23 (unpadded data length)
bl         outline_der_decode_sequence_ex ; outline_der_decode_sequence_ex
       ; int der_decode_sequence_ex(const unsigned char *in, unsigned long  inlen,
       ;                            ltc_asn1_list *list,     unsigned long  outlen, unsigned int flags);
cbnz       w0, loc_1000e9480

接下来从消息M中用der_decode_sequence_ex读取了一个长度为2的ASN.1 DER编码序列

image-20260206094353987.png

PKCS#1 DigestInfo

可以看出来,两个编码后的元素,第一个是SHA1的OID,第二个是前面对generate_data返回的字符串计算的SHA1值

这个其实就是 PKCS#1 DigestInfo 的结构

DigestInfo ::= SEQUENCE {
    digestAlgorithm AlgorithmIdentifier,
    digest OCTET STRING
}

接着就是把读取到的两个值与期望值分别作比较,都相同时则返回验证成功

小总结

我们来总结一下这个验证流程

  1. 解码128字节 (1024bit) 的签名数据
  2. 用公钥计算 $m=c^e\pmod{N}$ 得到编码消息
  3. 校验 PKCS#1 v1.5 Padding 编码格式
  4. 解析编码消息中的 PKCS#1 DigestInfo 获得SHA1值
  5. 把得到的SHA1值与原消息的SHA1值对比

这是一个标准的 RSASSA-PKCS#1-v1.5 签名验证流程

可以参考标准文件的8.2节 https://datatracker.ietf.org/doc/html/rfc8017#section-8.2

不过这个这个标准已经有些过时而且缺乏安全性了,现在用的多的应该是 OAEP

签名实现

签名和验证的流程是反过来的,了解了底层原理之后,我就想试试看自己实现一个看看

正好最近了解到zig可以支持最大位宽为65535的整数,那就来试试用它实现一个基于1024位RSA的签名吧

pub export fn z_rsassa_pkcs1v15_sign(in: 
  • const u8, inlen: usize, signature: *[128]u8) void {         // 计算消息的SHA1值     var sha1_hash: [20]u8 = undefined;     std.crypto.hash.Sha1.hash(in[0..inlen], &sha1_hash, .{});     // 构造 PKCS#1 DigestInfo 结构     const dlen = 35;     var digest_info: [35]u8 = undefined;     @memcpy(digest_info[0..15], &[15]u8{0x30,0x21,0x30,0x09,0x06,0x05,0x2B,0x0E,0x03,0x02,0x1A,0x05,0x00,0x04,0x14});     @memcpy(digest_info[15..], sha1_hash[0..20]); // 其实在SHA1值前面拼一个固定的前缀就好了     // 给 DigestInfo 加上 PKCS#1 Padding     var encoded_message: [128]u8 = undefined;     encoded_message[0] = 0;     encoded_message[1] = 1;     @memset(encoded_message[2..128-dlen-1], 0xff); // 中间的若干个0xFF     encoded_message[128-dlen-1] = 0;     @memcpy(encoded_message[128-dlen..], digest_info[0..]); // 最后是DigestInfo     // 用私钥计算出签名值     const m = std.mem.readInt(u1024, &encoded_message, .big); // 按大端序读入一个1024bit的整数     const c = key.exptmod(m); // 这一步就是计算 m=c^e\pmod N     std.mem.writeInt(u1024, signature, c, .big); // 按大端序写入一个1024bit的整数     return; }
  • 最后用私钥计算签名的那一步key.exptmod(m)我封装了一下,用了CRT(中国剩余定理)和快速幂来优化,如下

    fn qpow(b: u1024, e: u512, m: u512) u512 {
        var res: u1024 = 1;
    
        const mod: u1024 = m;
        var exp = e;
        var base: u1024 = b % mod;
        while (exp != 0) : (exp >>= 1) {
            if (exp & 1 == 1) {
                res = res * base % mod;
            }
            base = base * base % mod;
        }
    
        return @as(u512, @truncate(res));
    }
    
    const rsa_key = struct {
        p: u512,
        q: u512,
        dp: u512,
        dq: u512,
        qinv: u512,
            // 基本上RSA私钥里都会有存这些字段
        fn exptmod(self: *const rsa_key, m: u1024) u1024 {
            const sp = qpow(m, self.dp, self.p);
            const sq = qpow(m, self.dq, self.q);
            const diff = if (sp > sq) sp - sq else sp + (self.p - sq % self.p);
            const h = @as(u1024, self.qinv) * diff % self.p;
            const c = sq + self.q * h;
            return c;
        }
    };

    这基本就完成了签名的部分,虽然用了一些优化,但速度和正经的加密库还是没得比的,因为zig底层大数运算实际就是编译器把它展开成了很多小段来运算,和专门的大数运算库用的一些算法优化相比差的远

    不过就结果而言,它和CryptoPP中的RSASSA_PKCS1v15_SHA_Signer或者是Security框架中的kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA1得到的结果是完全一样的,我已经很满意了

    这样折腾一下还是非常好玩的,我们得以一瞥工程标准实现与数学原理之间的差距与联系

    许可证格式分析 Part2 + 公钥检查

    那么回到Sublime,我们继续分析剩下的一点点东西

    验证完签名首先检查了一下第四行第一个部分是否不等于"EA7E"

    adrp       x1, #0x1004d3000 ; aEa7e@PAGE
    add        x1, x1, #0x95c ; aEa7e@PAGEOFF, argument #2 for method _OUTLINED_FUNCTION_12513, "EA7E"
           ; Compare line4 part 1 with "EA7E"
    bl         _OUTLINED_FUNCTION_12513 ; _OUTLINED_FUNCTION_12513
    cmp        w0, #0x0
    mov        w8, #0x119
           ; w27 = w8 + 1 (if w8 != 0 -> str equal)
           ; (Should not equal…)
    cinc       w27, w8, ne

    接着麻烦的就来了,它计算了公钥(DER格式的16进制字符串)的SHA256值,并且检查了其中的几位

    loc_1001cd468:
    adrp       x8, #0x100656000 ; 0x100656ae0@PAGE
    add        x8, x8, #0xae0 ; 0x100656ae0@PAGEOFF, 0x100656ae0
           ; der public key hex string
    bl         _OUTLINED_FUNCTION_484 ; _OUTLINED_FUNCTION_484
    csel       x2, x11, x9, lt ; argument #3 for method __Z11crypto_hashPhPKhy
    add        x0, sp, #0xa0 ; argument #1 for method __Z11crypto_hashPhPKhy
           ; sha256 on public key
    bl         crypto_hash(unsigned char*, unsigned char const*, unsigned long long)
           ; c6340397059a1d2ab1a41340ced7a5a8f86dea0c227fa940b6e74f0ef84656ea
    ldrb       w8, [sp, #0x240 + var_1A0]
    and        w8, w8, #0xff
           ; digest[0] == 0xc6
    cmp        w8, #0xc6
    ldrb       w8, [sp, #0x240 + var_192]
    and        w8, w8, #0xff
           ; digest[14] == 0xa5
    mov        w9, #0xa5
    ccmp       w8, w9, #0x0, eq
    ldrb       w8, [sp, #0x240 + var_18E]
    and        w8, w8, #0xff
           ; digest[18] == 0xea
    mov        w9, #0xea
    ccmp       w8, w9, #0x0, eq
    ldrb       w8, [sp, #0x240 + var_182]
           ; digest[30] == 0x56
    mov        w9, #0x56
    ccmp       w8, w9, #0x0, eq
           ; w25 <- w27 (if public key check passed)
    csel       w25, w27, w26, eq
           ; Should equal
    cmp        w25, #0x119
    b.ne       loc_1001cd518

    如果不相等会直接导致激活失败,在apple_fruit的下面还有一处不是很起眼的检查

    image-20260206123955852.png

    非常狡猾啊,在最后检查了SHA256的最后一位,而且不相等的话是随机触发激活失败,如果你这一次成功了,没准哪次打开就会弹出一个激活失败

    总共检查了5位SHA256的值,下面标记^^就是被检查了的位

    c6340397059a1d2ab1a41340ced7a5a8f86dea0c227fa940b6e74f0ef84656ea
    ^^__________________________^^______^^______________________^^^^

    我们在替换公钥的时候别忘了还要把这几位更新一下

    我们可以直接用libtomcrypt生成新的公钥并计算新的SHA256值

    // X.509 SubjectPublicKeyInfo (公钥DER编码)
    unsigned char encoded_key[200];
    unsigned long encoded_len = sizeof(encoded_key);
    if ((err = rsa_export(encoded_key, &encoded_len, PK_PUBLIC | PK_STD, key))
        != CRYPT_OK) {
        printf("rsa_export error: %s\n", error_to_string(err));
        return -1;
    }
    
    // 编码成16进制字符串
    char hex_key[400];
    unsigned long hex_len = sizeof(hex_key);
    if ((err = base16_encode(encoded_key, encoded_len, hex_key, &hex_len, 1))
        != CRYPT_OK) {
        printf("base16_encode error: %s\n", error_to_string(err));
        return -1;
    }
    
    // sha256
    hash_state md;
    unsigned char sha256_out[32];
    sha256_init(&md);
    sha256_process(&md, (unsigned char *)hex_key, hex_len);
    sha256_done(&md, sha256_out);

    然后继续是第四行第一部分的检查,可以是"E52D"或者是"E3D2",这个字段就是许可证类型了

    如果看一眼Sublime Text,它支持的类型是"EA7E"和"E3D2",诶,有没有发现这个"E3D2"是通用的,应该是bundled license的意思

    image-20260206133421716.png

    再往下,它把第四行第二部分转换成一个整数,应该是许可证ID,然后到全局的黑名单里检查这个ID有没有被拉黑(图略)

    然后到了第四行第三部分的一个判断,设置了一个很奇怪的flag,就是这里的w8

    ldr        w8, [sp, #0x240 + var_21C]
           ; w8 -> 许可证用户数量
    sub        w9, w24, #0x1
           ; w24 -> 第四行第三部分转换为整数
    cmp        w8, #0x0
    mov        w8, #0x45c
    movk       w8, #0x7, lsl #16
           ; w8 = 0x7045c
    ccmp       w9, w8, #0x2, ne
           ; If not unlimited && w24-1 < 0x7045c
           ;  -> w8=1
           ; Otherwise
           ;  -> w8=0
    cset       w8, lo
           ; x22 -> arg5
    strb       w8, [x22]

    虽然不清楚这一段是什么含义,但在调用方可以看到如果w8=1的话会触发一个需要升级许可证的弹窗

    ldr        x8, [x21, #0x268]
           ; line4 part3 flag!
    ldrb       w8, [x8, #0x10]
           ; Show thanks
    adr        x9, #0x1001cdbf0
    nop
           ; Upgrade required
    adr        x10, #0x1001cdb7c
    nop
    cmp        w8, #0x0
    csel       x0, x10, x9, ne

    所以说这个第三段直接去掉就好,不用管它

    到这里apple_fruit函数的验证流程就差不多分析完了,上面所有验证都通过了它会返回0x119表示许可证有效

    我们来回顾一下上面分析出来的Sublime许可证格式

    • 共13行,第一行和最后一行没用
    • 第2行为用户名
    • 第3行包含许可证用户数,可以是"Single User License"/"Unlimited User License"/"%d User License"
    • 第4行是用'-'分隔的两段数据,第一段是固定的许可证类型"E52D"/"E3D2",第二段是一个数字表示许可证ID
    • 第5-12行为签名数据

    替换公钥 + Keygen

    Sublime存在二进制里的公钥是按字节异或过的,具体的解密流程在apple_fruit函数头部

    image-20260206143408511.png

    用来异或的常量是0xc6,异或后会得到DER格式编码的公钥,Sublime这里可能是为了方便处理还转换成了16进制字符串

    所以我们只需要把生成的DER格式公钥也每字节异或上0xc6然后替换就好了,然后要更新一下apple_fruit函数里校验的那几位SHA256值

    就可以按照上面的格式生成我们自己的许可证了,别忘了它还有在线验证,屏蔽license.sublimehq.com就好了,可以直接替换掉这个字符串

    又到了喜闻乐见的环节

    image-20260206144204150.png

    结束了吗?还没捏,最后来记录一下实现跨平台Keygen过程遇到的一些困难

    一个很麻烦的问题是,Sublime用来异或公钥的这个常量并不是固定的,不同平台不同版本都不一样,很难直接定位并修改

    当然,可以把所有可能的常量枚举一遍,共256种,但那未免效率太低了

    我想到的办法是可以利用异或的性质来确定这个公钥

    假设公钥为 $\{a_n\}$,异或后的公钥为 $\{b_n\}$,且 $b_n = a_n\oplus k$$k$ 为异或的常量

    那么就有

    $$ \begin{align*} b_i\oplus b_{i+1} & = (a_i\oplus k)\oplus(a_{i+1}\oplus k)\\ & = a_i \oplus a_{i+1} \end{align*} $$

    也就是说,异或前后,公钥的相邻位的异或值是不变的,我们就可以利用这个来定位异或过的公钥了

    在此基础上,再配合上skip搜索算法,可以在极短的时间内找到公钥(实测 0.01s 左右)

    然后就是公钥的校验了,在不同的平台上,相同架构的有些指令是相同的,所以不需要很多补丁就可以实现跨平台多版本通杀

    (说是不多但也有十几个了)

    最终我用 C 和 Zig 写了一个跨平台的命令行Keygen,支持所有的系统和架构,已经开源在Github了,欢迎来fork或者star

    https://github.com/Antibioticss/sublime-keygen

    用法很简单,简单说下

    subkg patch '/Applications/Sublime Merge.app/Contents/MacOS/sublime_merge'

    patch 后面加上主程序二进制的路径,用来替换公钥以及公钥的校验字节

    subkg keygen -n hello -t bundle -i 00000000 -s 1026

    keygen 的参数都是可选的,-n 指定名字,-t 为许可证类型,-i 许可证ID,-s 许可证用户数

    macOS上patch完了别忘了重签名

    codesign -f -s - '/Applications/Sublime Merge.app/Contents/MacOS/sublime_merge'

    后记

    终于写完辣!希望大家看完本文也能有收获!

    非常感谢 CryptoPPlibtomcrypt 这两个项目,我从文档中学到了不少东西

    对了,最后提一嘴,审核大大什么时候可以通过一下我的新头像,感谢!

    免费评分

    参与人数 40吾爱币 +43 热心值 +36 收起 理由
    夜莺高歌 + 1 + 1 谢谢@Thanks!
    lonely_coder + 1 + 1 用心讨论,共获提升!
    wangxb2555 + 1 我很赞同!
    ahaneo + 1 + 1 谢谢@Thanks!
    Honeymoon + 1 + 1 我很赞同!
    DawnXi + 1 + 1 用心讨论,共获提升!
    liphys + 1 + 1 我很赞同!
    KEKAI2025 + 1 + 1 热心回复!
    elvischoo + 1 + 1 谢谢@Thanks!
    边缘人静心 + 1 + 1 用心讨论,共获提升!
    k1mxn + 1 + 1 我很赞同!
    Atnil + 1 + 1 用心讨论,共获提升!
    78zhanghao87 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
    xsxz + 1 + 1 鼓励转贴优秀软件安全工具和文档!
    yixi + 1 + 1 我很赞同!
    dragonsome + 1 + 1 用心讨论,共获提升!
    zhourunfav + 1 + 1 鼓励转贴优秀软件安全工具和文档!
    ioyr5995 + 1 + 1 热心回复!
    nsrcc + 1 + 1 谢谢@Thanks!
    HOWMP + 1 + 1 我很赞同!
    vLove0 + 1 + 1 谢谢@Thanks!
    eagleangle + 1 + 1 用心讨论,共获提升!
    goldenrose + 1 + 1 热心回复!
    YZ228228 + 1 + 1 谢谢@Thanks!
    tao2017 + 1 + 1 谢谢@Thanks!
    sunil + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
    zqqqq + 1 谢谢@Thanks!
    randomone + 1 + 1 鼓励转贴优秀软件安全工具和文档!
    zhczf + 1 + 1 我很赞同!
    gaosld + 1 + 1 热心回复!
    孤狼微博 + 1 + 1 用心讨论,共获提升!
    fengbolee + 1 + 1 用心讨论,共获提升!
    怜渠客 + 1 + 1 用心讨论,共获提升!
    BrutusScipio + 1 + 1 用心讨论,共获提升!
    allspark + 1 + 1 用心讨论,共获提升!
    kavxc + 1 + 1 谢谢@Thanks!
    Vvvvvoid + 3 + 1 鼓励转贴优秀软件安全工具和文档!
    SkinnyTiger + 1 我很赞同!
    江南小虫虫 + 1 用心讨论,共获提升!
    helian147 + 1 + 1 热心回复!

    查看全部评分

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

    blyizhi 发表于 2026-2-7 22:21
    lz是真的牛,
    孤狼微博 发表于 2026-2-7 13:51


    1. 安装 Package Control
    要安装汉化包,首先需要确保已安装 Package Control,这是 Sublime Text 的插件管理工具。

    如果尚未安装,可按以下步骤操作:
    打开 Sublime Text
    按下 Ctrl+`(反引号)打开控制台
    粘贴并运行以下代码:
    [Asm] 纯文本查看 复制代码
    import urllib.request,os; pf = 'Package Control.sublime-package'; ipp = sublime.installed_packages_path(); urllib.request.install_opener( urllib.request.build_opener( urllib.request.ProxyHandler()) ); open(os.path.join(ipp, pf), 'wb').write(urllib.request.urlopen( 'https://packagecontrol.io/' + pf.replace(' ', '%20')).read())


    重启 Sublime Text 后,可通过菜单 Tools → Command Palette 搜索 "Package Control" 确认是否安装成功。
    picoyiyi 发表于 2026-2-6 21:19
    一个人完成了这么大的工程,厉害了我的楼主
    SkinnyTiger 发表于 2026-2-6 21:30
    本帖最后由 SkinnyTiger 于 2026-2-6 21:58 编辑

    PixPin_2026-02-06_21-29-06.png 为什么我注册后 就提示需要升级,断网重新注册都不行。Package Control Messages - Sublime Text (LICENSE UPGRADE REQUIRED)
     楼主| skrets 发表于 2026-2-6 21:57
    SkinnyTiger 发表于 2026-2-6 21:30
    为什么我注册后 就提示需要升级,断网重新注册都不行。Package Control Messages - Sublime Text (LICENSE  ...

    和便携版应该没什么关系,可能是踩到黑名单了
    重新生成一个许可证试试看,换一个ID也许就好了
    SkinnyTiger 发表于 2026-2-6 21:58
    skrets 发表于 2026-2-6 21:57
    和便携版应该没什么关系,可能是踩到黑名单了
    重新生成一个许可证试试看,换一个ID也许就好了

    换ID也不行,换安装版也不行
    osforum 发表于 2026-2-6 22:05
    本帖最后由 osforum 于 2026-2-6 22:07 编辑

    大佬您好,我在Windows端执行提示:fopen: Permission denied
    window执行提示:.\subkg.exe patch "C:\Users\test1\Desktop\sublime_merge_build_2120_x64"
    fopen: Permission denied

    以管理员方式运行cmd也不行。

    是我的执行方式有问题?
    SkinnyTiger 发表于 2026-2-6 22:08
    osforum 发表于 2026-2-6 22:05
    大佬您好,我在Windows端执行提示:fopen: Permission denied
    window执行提示:.\subkg.exe patch "C:%use ...

    你这是需要关闭程序吧?
    osforum 发表于 2026-2-6 22:10
    SkinnyTiger 发表于 2026-2-6 22:08
    你这是需要关闭程序吧?

    没有打开过这个程序。程序也没有后台运行。安装包解压后就执行的命令。
     楼主| skrets 发表于 2026-2-6 22:11
    osforum 发表于 2026-2-6 22:05
    大佬您好,我在Windows端执行提示:fopen: Permission denied
    window执行提示:.\subkg.exe patch "C:%use ...

    patch 后面跟的是 sublime_merge.exe 的路径,你这个写成了文件夹了
    SkinnyTiger 发表于 2026-2-6 22:11
    osforum 发表于 2026-2-6 22:10
    没有打开过这个程序。程序也没有后台运行。安装包解压后就执行的命令。

    你是路径不对,要指定文件,而不是文件夹
    您需要登录后才可以回帖 登录 | 注册[Register]

    本版积分规则

    返回列表

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

    GMT+8, 2026-3-10 07:36

    Powered by Discuz!

    Copyright © 2001-2020, Tencent Cloud.

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