破解 ARM64 架构的 Sublime Text 4180

1 序

终于用上了 M 芯片的 Macbook,拿到电脑的第一件事就是装各种软件。

在安装 Sublime Text 的时候,遇到了问题:网上提供的破解方法都是针对 Intel 芯片的,破解后的应用在 M 芯片上仍然显示未注册。

不过,既然有 Intel 版的破解方法,顺藤摸瓜搞出一个 M 版的破解,应该也不是太难。

太长不看?上链接:

2 分析

2.1 拆解文件

修改一个二进制文件,在 Intel 芯片上有效,在 M 芯片上无效,说明 Sublime Text 针对两种芯片分别做了编译,然后打包到了一个 app 中。

经过一翻搜索,最终找到了相关资料(*1)。

与预想的一样,Sublime Text 是一个 Universal Binary,不同架构的可执行文件被打包成了一个文件。

查看一下文件的详细信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
% lipo Sublime\ Text.app/Contents/MacOS/sublime_text -detailed_info
Fat header in: Sublime Text.app/Contents/MacOS/sublime_text
fat_magic 0xcafebabe
nfat_arch 2
architecture x86_64
cputype CPU_TYPE_X86_64
cpusubtype CPU_SUBTYPE_X86_64_ALL
capabilities CPU_SUBTYPE_LIB64
offset 16384
size 15921520
align 2^14 (16384)
architecture arm64
cputype CPU_TYPE_ARM64
cpusubtype CPU_SUBTYPE_ARM64_ALL
capabilities 0x0
offset 15941632
size 16261536
align 2^14 (16384)

由此可知,sublime_text 的文件结构如下:

分别导出两个架构的独立可执行文件:

1
2
3
4
% lipo /path/to/Sublime\ Text.app/Contents/MacOS/sublime_text \
-thin x86_64 -output sublime_text-x64
% lipo /path/to/Sublime\ Text.app/Contents/MacOS/sublime_text \
-thin arm64 -output sublime_text-arm64

其中,x86_64 架构的可执行文件在 Intel 芯片上运行,而 arm64 架构的可执行文件在 M 芯片上运行。

2.2 x86_64 版破解方法

先来看看大佬提供的 x86_64 版破解方法:

1
2
sudo perl -pi -e 's/\x80\x79\x05\x00\x0F\x94\xC2/\xC6\x41\x05\x01\xB2\x00\x90/' \
/Applications/Sublime\ Text.app/Contents/MacOS/sublime_text

即从 sublime_text 文件中,搜索十六进制数据 80 79 05 00 0F 94 C2,并将其替换为 C6 41 05 01 B2 00 90

这段修改起到了什么作用呢?使用在线反汇编工具(*2)转换看看:

破解前:

1
2
3
4
; 从 rcx + 5 指向的内存取出 1 字节数据,与 0 做比较。
cmp byte ptr [rcx + 5], 0
; 若相等,则将寄存器 dl 赋值为 1;反之,赋值为 0。
sete dl

破解后:

1
2
3
4
5
6
; 将 rcx + 5 指向的 1 字节内存设置为 1。
mov byte ptr [rcx + 5], 1
; 将寄存器 dl 赋值为 0。
mov dl, 0
; 空指令
nop

从破解后的汇编指令,可以做出如下推测:

  • rcx + 5 指向的 1 字节内存,存储的是全局注册状态,1 表示已注册,0 表示未注册,修改后将对全局生效。
  • 寄存器 dl 存储的也是注册状态,0 表示已注册,1 表示未注册,仅供后面的程序做判断。
  • 破解指令通过强行修改这两处,达到了让 sublime_text 以为用户已注册。

2.3 定位修改点

知道修改的逻辑了,接下来要定位在可执行文件中修改的位置,以便推导在 arm64 可执行文件中修改的位置。

请出老朋友 Cutter,加载文件 sublime_text-x64。

搜索 hex string: 80 79 05 00 0F 94 C2,可以看到指令的起始地址为 0x1000f7c00

双击搜索结果,跳到反汇编界面,可以看到上下文指令:

还可以知道被修改的函数为 edit_window::on_update_state

2.4 分析修改点

当汇编指令执行到地址 0x1000f7c00 时,rcx + 5 指向的内存数据就是注册状态了。

那么 rcx 是什么时候被赋值的呢?往上来到地址 0x1000f7bf7

1
2
; 从 r15 + 0x4b0 指向的内存,取出 1 qword(8字节) 数据,赋值给 rcx。
mov rcx, qword [r15 + 0x4b0]

再往上找 r15,来到地址 0x1000f7bc1

1
2
; 将寄存器 rdi 存储的值,赋值给 r15。
mov r15, rdi

再往上就到了函数开始,而寄存器 rdi 存储的值,是函数的第一个参数(*3-1)。

至此,溯源完成。

把这些逻辑写成 C++ 代码,大概是这样:

1
2
3
4
5
6
void edit_window::on_update_state() {
// 对 C++ 的类方法来说,第一个参数是 this。
int64_t p_reg_info = *((int64_t *)((void *)this + 0x4b0));
bool *p_reg_flag = (bool *)(p_reg_info + 5);
// p_reg_flag 即为指向全局存储注册状态的指针。
}

3 破解 arm64 版

3.1 定位修改点

这次换个工具来搞反汇编:

1
% otool -tjvV sublime_text-arm64 >arm64.asm

然后用 Sublime Text 打开 arm64.asm 文件,用正则搜索 edit_window.*on_update_state,找到目标函数:

一眼望去,这两条汇编指令简直不要太熟悉:

1
2
3
4
; 在 x19 + 0x4b0 指向的内存,取出 8 字节数据,存储到寄存器 x8。
00000001000dabd4 f9425a68 ldr x8, [x19, #0x4b0]
; 在 x8 + 0x5 指向的内存,取出 1 字节数据,存到寄存器 w9。
00000001000dabd8 39401509 ldrb w9, [x8, #0x5]

接下来,只要证明寄存器 x19 存储的值,是函数的第一个参数,就可以认为我们找对了地方。

在这段指令的上面,没有直接看到对寄存器 x19 的赋值,但是有 4 次跳转到子过程的指令。

挨个检查这些子过程,在 _OUTLINED_FUNCTION_6978 看到了如下指令:

1
2
3
4
5
6
_OUTLINED_FUNCTION_6978:
; 将寄存器 x0 存储的值,赋值给寄存器 x19。
00000001004a7148 aa0003f3 mov x19, x0

00000001004a714c f9401408 ldr x8, [x0, #0x28]
00000001004a7150 d65f03c0 ret

而寄存器 x0 存储的值,就是函数的第一个参数(*3-2)。

至此,证明完毕。

结论:对于 arm64 版 sublime_text ,破解代码的修改位置为地址 00000001000dabd8

3.2 编写破解指令

与 x86_64 版思路相同,破解指令要做这两件事情:

  1. 强行修改全局的注册状态,即 x8 + 0x5 这块指向的内存。
  2. 强行修改比较的结果,欺骗后面的程序逻辑。

先来修改 x8 + 0x5

1
2
3
; ARM64 汇编不支持存储立即数(immediate),因此需要借助一次寄存器。
mov w9, #1
strb w9, [x8, #0x5]

由于 arm64 每条汇编指令对应操作码的都是 4 字节,因此直接替换原始的 2 条指令即可。

来看看要被替换掉的 2 条指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
; 在 x8 + 0x5 指向的内存,取出 1 字节数据,存到寄存器 w9。
00000001000dabd8 39401509 ldrb w9, [x8, #0x5]
; 跳转到子过程 _OUTLINED_FUNCTION_10851 。
00000001000dabdc 940f6b00 bl _OUTLINED_FUNCTION_10851

; 子过程
_OUTLINED_FUNCTION_10851:
; 比较 w9 与 0
00000001004b57dc 7100013f cmp w9, #0x0
; w9 = (w9 == 0) ? 1 : 0
00000001004b57e0 1a9f17e9 cset w9, eq
; 返回
00000001004b57e4 d65f03c0 ret

; 假如用户已注册,到这里时:
; x8 + 5 指向的 1 字节内存,数据为 1。
; w9 存储的值为 0。

这里就有问题了:修改后,寄存器 w9 存储的值是不对的,这可能会影响到后面的程序逻辑。因此,需要修改更多的指令。

继续往后,看看哪里用到了 w9 存储的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
; w10 = w9 << 1
00000001000dabe0 531f792a lsl w10, w9, #1

; w8 = *((byte *)(x8 + 4))
00000001000dabe4 39401108 ldrb w8, [x8, #0x4]

; w11 = 4 | (w9 & 0x01) << 1
00000001000dabe8 5280008b mov w11, #0x4
00000001000dabec 331f012b bfi w11, w9, #1, #1

; w8 = (w8 == 0) ? w10 : w11
00000001000dabf0 7100011f cmp w8, #0x0
00000001000dabf4 1a8b0148 csel w8, w10, w11, eq

; w9 = (w0 > 1) ? 1 : 0
00000001000dabf8 7100041f cmp w0, #0x1
00000001000dabfc 1a9f97e9 cset w9, hi

可以看到,到达地址 00000001000dabfc 时,寄存器 w9 被用来存储其他数据了。

而在这之前,w9 存储的值被用来做了两次计算,计算结果分别存在了寄存器 w10w11

假定用户已注册,则 w9 存储的值应该为 0,进而可以推算出:w10 存储的值应该为 0w11 存储的值应该为 4

直接将这些运算转换为赋值语句,并稍微调整一下指令顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
; 强行将寄存器 w10 赋值为 0。
00000001000dabe0 xxxxxxxx mov w10, #0

; 无须修改,上移一行。
00000001000dabe4 5280008b mov w11, #4
; 空指令。
00000001000dabe8 xxxxxxxx nop

; 无须修改,下移两行。
00000001000dabec 39401108 ldrb w8, [x8, #0x4]

; 这之后的指令保持不变。
00000001000dabf0 7100011f cmp w8, #0x0
00000001000dabf4 1a8b0148 csel w8, w10, w11, eq
00000001000dabf8 7100041f cmp w0, #0x1
00000001000dabfc 1a9f97e9 cset w9, hi

总结下来,一共需要修改 6 条指令,修改后的指令为:

1
2
3
4
5
6
7
; 起始地址:00000001000dabd8
mov w9, #1
strb w9, [x8, #5]
mov w10, #0
mov w11, #4
nop
ldrb w8, [x8, #4]

3.3 修改文件

使用在线汇编工具,将修改后的汇编指令转换为二进制数据:

而修改前的汇编指令,其对应的二进制数据为:

汇编码 指令码 二进制数据
ldrb w9, [x8, #0x5] 39401509 09 15 40 39
bl _OUTLINED_FUNCTION_10851 940f6b00 00 6b 0f 94
lsl w10, w9, #1 531f792a 2a 79 1f 53
ldrb w8, [x8, #0x4] 39401108 08 11 40 39
mov w11, #0x4 5280008b 8b 00 80 52
bfi w11, w9, #1, #1 331f012b 2b 01 1f 33

因此,只需要在目标文件中搜索二进制数据:

1
09 15 40 39 00 6b 0f 94 2a 79 1f 53 08 11 40 39 8b 00 80 52 2b 01 1f 33

并将其替换为以下数据即可:

1
29 00 80 52 09 15 00 39 0A 00 80 52 8B 00 80 52 1F 20 03 D5 08 11 40 39

将 x86_64 版的破解脚本简单修改一下,就变成了 arm64 版的破解脚本:

1
2
3
4
5
# 修改文件
sudo perl -pi -e 's/\x09\x15\x40\x39\x00\x6b\x0f\x94\x2a\x79\x1f\x53\x08\x11\x40\x39\x8b\x00\x80\x52\x2b\x01\x1f\x33/\x29\x00\x80\x52\x09\x15\x00\x39\x0A\x00\x80\x52\x8B\x00\x80\x52\x1F\x20\x03\xD5\x08\x11\x40\x39/' \
/Applications/Sublime\ Text.app/Contents/MacOS/sublime_text
# 重新签名
sudo codesign --sign - --force --deep /Applications/Sublime\ Text.app

再次运行 Sublime Text,破解成功:

4 参考资料

  1. Universal binary
  1. 在线汇编 / 反汇编工具
  1. Calling conventions
  1. 汇编指令集