破解 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 | % lipo Sublime\ Text.app/Contents/MacOS/sublime_text -detailed_info |
由此可知,sublime_text 的文件结构如下:
分别导出两个架构的独立可执行文件:
1 | % lipo /path/to/Sublime\ Text.app/Contents/MacOS/sublime_text \ |
其中,x86_64 架构的可执行文件在 Intel 芯片上运行,而 arm64 架构的可执行文件在 M 芯片上运行。
2.2 x86_64 版破解方法
先来看看大佬提供的 x86_64 版破解方法:
1 | sudo perl -pi -e 's/\x80\x79\x05\x00\x0F\x94\xC2/\xC6\x41\x05\x01\xB2\x00\x90/' \ |
即从 sublime_text 文件中,搜索十六进制数据 80 79 05 00 0F 94 C2
,并将其替换为 C6 41 05 01 B2 00 90
。
这段修改起到了什么作用呢?使用在线反汇编工具(*2)转换看看:
破解前:
1 | ; 从 rcx + 5 指向的内存取出 1 字节数据,与 0 做比较。 |
破解后:
1 | ; 将 rcx + 5 指向的 1 字节内存设置为 1。 |
从破解后的汇编指令,可以做出如下推测:
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 | ; 从 r15 + 0x4b0 指向的内存,取出 1 qword(8字节) 数据,赋值给 rcx。 |
再往上找 r15
,来到地址 0x1000f7bc1
:
1 | ; 将寄存器 rdi 存储的值,赋值给 r15。 |
再往上就到了函数开始,而寄存器 rdi
存储的值,是函数的第一个参数(*3-1)。
至此,溯源完成。
把这些逻辑写成 C++ 代码,大概是这样:
1 | void edit_window::on_update_state() { |
3 破解 arm64 版
3.1 定位修改点
这次换个工具来搞反汇编:
1 | % otool -tjvV sublime_text-arm64 >arm64.asm |
然后用 Sublime Text 打开 arm64.asm 文件,用正则搜索 edit_window.*on_update_state
,找到目标函数:
一眼望去,这两条汇编指令简直不要太熟悉:
1 | ; 在 x19 + 0x4b0 指向的内存,取出 8 字节数据,存储到寄存器 x8。 |
接下来,只要证明寄存器 x19
存储的值,是函数的第一个参数,就可以认为我们找对了地方。
在这段指令的上面,没有直接看到对寄存器 x19
的赋值,但是有 4 次跳转到子过程的指令。
挨个检查这些子过程,在 _OUTLINED_FUNCTION_6978
看到了如下指令:
1 | _OUTLINED_FUNCTION_6978: |
而寄存器 x0
存储的值,就是函数的第一个参数(*3-2)。
至此,证明完毕。
结论:对于 arm64 版 sublime_text ,破解代码的修改位置为地址 00000001000dabd8
。
3.2 编写破解指令
与 x86_64 版思路相同,破解指令要做这两件事情:
- 强行修改全局的注册状态,即
x8 + 0x5
这块指向的内存。 - 强行修改比较的结果,欺骗后面的程序逻辑。
先来修改 x8 + 0x5
:
1 | ; ARM64 汇编不支持存储立即数(immediate),因此需要借助一次寄存器。 |
由于 arm64 每条汇编指令对应操作码的都是 4 字节,因此直接替换原始的 2 条指令即可。
来看看要被替换掉的 2 条指令:
1 | ; 在 x8 + 0x5 指向的内存,取出 1 字节数据,存到寄存器 w9。 |
这里就有问题了:修改后,寄存器 w9
存储的值是不对的,这可能会影响到后面的程序逻辑。因此,需要修改更多的指令。
继续往后,看看哪里用到了 w9
存储的值:
1 | ; w10 = w9 << 1 |
可以看到,到达地址 00000001000dabfc
时,寄存器 w9
被用来存储其他数据了。
而在这之前,w9
存储的值被用来做了两次计算,计算结果分别存在了寄存器 w10
和 w11
。
假定用户已注册,则 w9
存储的值应该为 0
,进而可以推算出:w10
存储的值应该为 0
,w11
存储的值应该为 4
。
直接将这些运算转换为赋值语句,并稍微调整一下指令顺序:
1 | ; 强行将寄存器 w10 赋值为 0。 |
总结下来,一共需要修改 6 条指令,修改后的指令为:
1 | ; 起始地址:00000001000dabd8 |
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 | # 修改文件 |
再次运行 Sublime Text,破解成功:
4 参考资料
- Universal binary
- https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary
- https://www.jviotti.com/2021/07/23/a-deep-dive-on-macos-universal-binaries.html#creating-universal-objects
- 在线汇编 / 反汇编工具
- https://disasm.czbix.com/
- https://www.disasm.pro/
- https://shell-storm.org/online/Online-Assembler-and-Disassembler/
- Calling conventions
- x86-64:https://en.wikipedia.org/wiki/X86_calling_conventions#System_V_AMD64_ABI
- ARM64:https://en.wikipedia.org/wiki/Calling_convention#ARM_(A64)
- 汇编指令集