一段简单逆向之旅-绕开Xcode 13.3最低macOS 12.0限制
因为众所周知的原因,苹果的Xcode版本会不断提高自己的最低安装版本,在Xcode 13.0-13.2.1上,这个最低安装版本是macOS 11
而随着Xcode 13.3正式版放出,这个最低部署版本在最后关头被提升到了macOS 12
Why?
一般来说,各位开发者或者众多基建,总有各种各样的原因需要暂时留在老版本的macOS系统上,但是又希望使用新Xcode版本自带的Toolchain进行一些工作开发调试,有些是主观问题,有些是客观限制:
举例子:
- macOS 12禁止了
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate off绕过GUI配置,导致一些公司采取Apple Device Management管理的电脑,无法正常关闭TCP拦截,会导致一些服务异常 - macOS 12加强了Kernel Extension的安全性,导致GitHub Action和Circle CI截止2022年3月底,迟迟无法更新他们的虚拟化集群到macOS 12,只有11.6的最新版本
这些都是闲聊,进入正题。那么有没有办法能够绕开,或者从原理上来讲,是否这个系统绑定的最低部署版本限制是必要的?
先放结论
- 可以绕过这个macOS 12的最低安装版本限制运行
- 这个限制是非必要的,绕过以后所有功能正常可用(构建,独立工具集,调试,连接iPhone)
下面来说明具体的逆向流程,和进行绕过的简单Step-by-step手法
安装Xcode
先说明测试机器Mac环境和Xcode环境:
- macOS 11.4 (20F71)
- Xcode Version 13.2 (13C90):需要保留一份以防万一
- Xcode Version 13.3 (13E113):目标安装的版本
首先,作为iOS/macOS开发者,我们肯定会使用dmg的格式,或者使用Xcodes.app来安装我们的Xcode 13.3了(App Store安装Xcode曾经出的坑:App Store version of Xcode 13.2 causing problems for developers,我是不会再用了)
安装完毕后,我们在Finder中看到的Xcode.app是一个画着❎的样子,直接打开会提示如下:
![]()
绕过GUI部分的限制
修改LSMinimumSystemVersion
作为iOS/macOS开发者,我们第一想到的就是,是否是Xcode.app对应的Info.plist中,设置了和最低部署版本相关的字段导致拒绝载入呢?
我们用另一个Xcode(或者plistutil)打开Xcode.app/Contents/Info.plist,果然发现了对应的字段:
![]()
这个LSMinimumSystemVersion是Mac应用标准的声明最低部署版本的方式,修改为你的机器当前OS版本之后保存,执行
1 |
touch /Applications/Xcode-13.3.0.app |
重新尝试双击。不错,这次我们打开了,初看起来不错(直到我们正式开始编译)!
![]()
绕过CLI部分的限制
神奇的xcrun
但是只要创建一下工程并执行编译,就会发现,各种命令行工具的调用是有问题的,比如我们先通过xcode-select设置为当前的Xcode 13.2,尝试执行:
![]()
![]()
但是我们如果直接找到,执行对应绝对路径的clang,是可以执行的
![]()
并且,我们可以直接检查clang这个二进制,是否链接时设置了target,这部分可以使用otool -l读取machO Header查看到:
![]()
好,最低部署版本是macOS 10.14.6;那现在我们有充分的证据说明,一定可以在我当前的电脑运行clang,而上述提示应该是xcrun这个调度器,添加了额外的判断。
通过搜索关键词,可以在Xcode的strings输出中找到这句“Executable requires at least”的关键字:参考仓库:Xcode.app-strings
反编译libxcodebuildLoader
我们定位到这个libxcodebuildLoader.dylib,拖进Hopper尝试反编译理解他检查的原理,伪代码如下:
1 |
void _checkMinimumOSVersion(int arg0) { |
好,阅读伪代码以及查阅资料可知:
xcrun会先一步调用到xcodebuild,检查DVTMinimumSystemVersion这个变量的值是否和当前OS版本匹配。
而这个变量,竟然是通过CFBundleCopyInfoDictionaryForURL打开的。
参考苹果的函数说明,它除了常规的打开一个.bundle的文件夹,解析为NSBundle.infoDictionary以外,竟然能打开存在于二进制__TEXT,__info_plist中的数据来解析为一个字典。所以我们接下来去找xcodebuild的二进制看看。
![]()
参考:
-
_NSGetExecutablePath:函数说明,大概理解获取当前程序的可执行路径
反编译xcodebuild
同时,出于好奇,我们可以再把xcodebuild拖进Hopper去尝试理解,发现它整个程序竟然只有一个main函数,逻辑其实都在libxcodebuildLoader.dylib中
1 |
void _main(int arg0, int arg1) { |
修改DVTMinimumSystemVersion
其实大家也发现了,xcodebuild二进制本身竟然内嵌了一段XML!我使用llvm-objdump把它直接提取了出来:
1 |
<?xml version="1.0" encoding="UTF-8"?> |
看到我们关心的DVTMinimumSystemVersion和LSMinimumSystemVersion都在里面。其实也侧面证明了,真正的最低部署版本是macOS 11.0,而不是macOS 12.0(12.0只是苹果为了间接Push Developer去频繁更新macOS的阴谋罢了😂)
那下一步,要做的事情就是用魔改xcodebuild并重新codesign。修改的方式多种多样,你暴力使用Hex Editor也是最简单。但是我更好奇的是这个__TEXT,__info_plist的machO段和节的相关说明。
在网上搜索了一下相关资料,很容易就找到了感兴趣的资料:
- The Power Of Plist:解释Info.plist可以内嵌在二进制中
- Gimmedebugah: how to embedded a Info.plist into arbitrary binaries:对任意已有二进制注入Info.plist
- llvm-objcopy:拷贝修改machO结构到新machO
基本解释得很明确清晰,如果你有源码,可以直接利用ld64的参数 --sectcreate __TEXT,__info_plist path_to/Info.plist来注入你的Info.plist信息。没有源码可以手动修改machO结构并签名即可。
对于我此次跑Xcode 13.3来说,我选择最傻瓜最直观的Hex Editor修改(我用的是开源小工具HexFiend),只需要把12.0修改为11.0即可满足我的需要,并重新codesign一波。
![]()
codesign:
1 |
codesign --remove-signature /Applications/Xcode-13.3.0.app/Contents/Developer/usr/bin/xcodebuild |
测试一下CLI,很正常
![]()
最终替换步骤
- 修改
Xcode-13.3.0.app/Contents/Info.plist中的LSMinimumSystemVersion的值为11.0 - 替换
Xcode-13.3.0.app/Contents/Developer/usr/bin/xcodebuild中的DVTMinimumSystemVersion的二进制为11.0,或者使用我这个已经替换好的(建议还是手动参考上面步骤[修改DVTMinimumSystemVersion]替换,授人以渔而不是授人以鱼)