普通视图

发现新文章,点击刷新页面。
昨天以前小猪的博客

一段简单逆向之旅-绕开Xcode 13.3最低macOS 12.0限制

作者 DreamPiggy
2022年3月26日 04:47

因为众所周知的原因,苹果的Xcode版本会不断提高自己的最低安装版本,在Xcode 13.0-13.2.1上,这个最低安装版本是macOS 11

而随着Xcode 13.3正式版放出,这个最低部署版本在最后关头被提升到了macOS 12

Why?

一般来说,各位开发者或者众多基建,总有各种各样的原因需要暂时留在老版本的macOS系统上,但是又希望使用新Xcode版本自带的Toolchain进行一些工作开发调试,有些是主观问题,有些是客观限制:

举例子:

  1. macOS 12禁止了sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate off绕过GUI配置,导致一些公司采取Apple Device Management管理的电脑,无法正常关闭TCP拦截,会导致一些服务异常
  2. macOS 12加强了Kernel Extension的安全性,导致GitHub Action和Circle CI截止2022年3月底,迟迟无法更新他们的虚拟化集群到macOS 12,只有11.6的最新版本

这些都是闲聊,进入正题。那么有没有办法能够绕开,或者从原理上来讲,是否这个系统绑定的最低部署版本限制是必要的?

先放结论

  1. 可以绕过这个macOS 12的最低安装版本限制运行
  2. 这个限制是非必要的,绕过以后所有功能正常可用(构建,独立工具集,调试,连接iPhone)

下面来说明具体的逆向流程,和进行绕过的简单Step-by-step手法

安装Xcode

先说明测试机器Mac环境和Xcode环境:

  1. macOS 11.4 (20F71)
  2. Xcode Version 13.2 (13C90):需要保留一份以防万一
  3. 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是一个画着❎的样子,直接打开会提示如下:

1648210752406_af87e2021743b9639d35f86714f127c7

绕过GUI部分的限制

修改LSMinimumSystemVersion

作为iOS/macOS开发者,我们第一想到的就是,是否是Xcode.app对应的Info.plist中,设置了和最低部署版本相关的字段导致拒绝载入呢?

我们用另一个Xcode(或者plistutil)打开Xcode.app/Contents/Info.plist,果然发现了对应的字段:

1648210752770_b314e1b23bdd01fe803ede1383e29491

这个LSMinimumSystemVersion是Mac应用标准的声明最低部署版本的方式,修改为你的机器当前OS版本之后保存,执行

1
2
touch /Applications/Xcode-13.3.0.app
killall Finder

重新尝试双击。不错,这次我们打开了,初看起来不错(直到我们正式开始编译)!

1648210752322_3d4e9ae78eaab94de9cfc7d1f8eaecf0

绕过CLI部分的限制

神奇的xcrun

但是只要创建一下工程并执行编译,就会发现,各种命令行工具的调用是有问题的,比如我们先通过xcode-select设置为当前的Xcode 13.2,尝试执行:

1648210752844_f21e111a081ff5dfc61dab7137109a39
1648210752266_e83287242af496cbbec7817b4b60f9f0

但是我们如果直接找到,执行对应绝对路径的clang,是可以执行的

1648210752227_fd8cd9f89407475fe62c0489dc61a232

并且,我们可以直接检查clang这个二进制,是否链接时设置了target,这部分可以使用otool -l读取machO Header查看到:

1648210752133_02c1257232efb1e2adebcb51ea25ceb0

好,最低部署版本是macOS 10.14.6;那现在我们有充分的证据说明,一定可以在我当前的电脑运行clang,而上述提示应该是xcrun这个调度器,添加了额外的判断。

通过搜索关键词,可以在Xcode的strings输出中找到这句“Executable requires at least”的关键字:参考仓库:Xcode.app-strings

反编译libxcodebuildLoader

我们定位到这个libxcodebuildLoader.dylib,拖进Hopper尝试反编译理解他检查的原理,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
void _checkMinimumOSVersion(int arg0) {
var_2C = 0x0;
rbx = 0x0;
rax = _NSGetExecutablePath(0x0, &var_2C);
rdi = var_2C;
if (rdi != 0x0) {
rbx = malloc(rdi);
}
rax = _NSGetExecutablePath(rbx, &var_2C);
if (rax != 0x0) goto loc_282c;

loc_26fc:
rax = [NSString stringWithUTF8String:rbx];
rax = [rax retain];
r14 = rax;
r15 = [[NSURL fileURLWithPath:rax] retain];
if (rbx != 0x0) {
free(rbx);
}
rbx = CFBundleCopyInfoDictionaryForURL(r15);
CFRelease(r15);
if (rbx == 0x0) goto loc_2867;

loc_276a:
r13 = [CFDictionaryGetValue(rbx, @ DVTMinimumSystemVersion ) retain];
CFRelease(rbx);
if ((r13 == 0x0) || ([r13 length] == 0x0)) goto loc_280c;

loc_27a6:
r12 = *_objc_msgSend;
r15 = [[DVTVersion versionWithStringValue:r13] retain];
rax = [DVTVersion currentSystemVersion];
rax = [rax retain];
rbx = r12;
r12 = rax;
rdx = r15;
if ([rax isEqualToOrNewerThanVersion:rdx] == 0x0) goto loc_288c;

loc_27fb:
[r12 release];
[r15 release];
goto loc_280c;

loc_280c:
[r13 release];
[r14 release];
return;

loc_288c:
r15 = [(rbx)(r15, @selector(stringValue), rdx) retain];
rax = (rbx)(r12, @selector(stringValue), rdx);
rax = [rax retain];
r14 = [(rbx)(@class(NSString), @selector(stringWithFormat:), @ Executable requires at least macOS %@, but is being run on macOS %@, and so is exiting. , r15, rax) retain];
[rax release];
[r15 release];
fprintf(**___stderrp, %s\n , (rbx)(objc_retainAutorelease(r14), @selector(UTF8String), @ Executable requires at least macOS %@, but is being run on macOS %@, and so is exiting. ));
goto loc_292b;

loc_292b:
exit(0x1);
return;

loc_2867:
fwrite( Unable to open executable info dictionary; xcodebuild may be corrupt and should be reinstalled.\n , 0x60, 0x1, **___stderrp);
goto loc_292b;

loc_282c:
_DVTAssertionFailureHandler(*_self, *__cmd, void checkMinimumOSVersion() , /Library/Caches/com.apple.xbs/Sources/IDETools/IDETools-20008/xcodebuildLoader/xcodebuildLoader.m , 0x69, @ 0 , @ Couldn't get executable path to self! );
return;
}

好,阅读伪代码以及查阅资料可知:

xcrun会先一步调用到xcodebuild,检查DVTMinimumSystemVersion这个变量的值是否和当前OS版本匹配。

而这个变量,竟然是通过CFBundleCopyInfoDictionaryForURL打开的。

参考苹果的函数说明,它除了常规的打开一个.bundle的文件夹,解析为NSBundle.infoDictionary以外,竟然能打开存在于二进制__TEXT,__info_plist中的数据来解析为一个字典。所以我们接下来去找xcodebuild的二进制看看。

1648210752247_62087827de34d7b56eea1e48208b261e

参考:

  1. _NSGetExecutablePath函数说明,大概理解获取当前程序的可执行路径

反编译xcodebuild

同时,出于好奇,我们可以再把xcodebuild拖进Hopper去尝试理解,发现它整个程序竟然只有一个main函数,逻辑其实都在libxcodebuildLoader.dylib

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void _main(int arg0, int arg1) {
r14 = arg1;
rbx = arg0;
rax = dlopen( @rpath/libxcodebuildLoader.dylib , 0x1);
if (rax == 0x0) goto loc_100002b7f;

loc_100002b5c:
rax = dlsym(rax, XcodeBuildMain );
if (rax == 0x0) goto loc_100002bd8;

loc_100002b70:
(rax)(rbx, r14);
return;

loc_100002bd8:
rax = dlerror();
rax = [NSString stringWithUTF8String:rax];
rax = [rax retain];
rax = objc_retainAutorelease(rax);
r15 = rax;
rax = [rax UTF8String];
rsi = Error loading symbol: %s\n ;
goto loc_100002c2f;

loc_100002c2f:
fprintf(**___stderrp, rsi);
rbx = [_prunedErrorMessage() retain];
[r15 release];
if (rbx != 0x0) {
_main.cold.1(rbx, @selector(UTF8String));
}
return;

loc_100002b7f:
rax = dlerror();
rax = [NSString stringWithUTF8String:rax];
rax = [rax retain];
rax = objc_retainAutorelease(rax);
r15 = rax;
rax = [rax UTF8String];
rsi = Error loading required libraries. If there is an ongoing installation please wait for it to complete. Otherwise reinstall. (%s)\n ;
goto loc_100002c2f;
}

修改DVTMinimumSystemVersion

其实大家也发现了,xcodebuild二进制本身竟然内嵌了一段XML!我使用llvm-objdump把它直接提取了出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>21E160</string>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleDisplayName</key>
<string>xcodebuild</string>
<key>CFBundleIdentifier</key>
<string>com.apple.dt.xcodebuild</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>xcodebuild</string>
<key>CFBundleShortVersionString</key>
<string>13.3</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>20008</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>21E185d</string>
<key>DTPlatformName</key>
<string>macosx</string>
<key>DTPlatformVersion</key>
<string>12.3</string>
<key>DTSDKBuild</key>
<string>21E185d</string>
<key>DTSDKName</key>
<string>macosx12.3.internal</string>
<key>DTXcode</key>
<string>1330</string>
<key>DTXcodeBuild</key>
<string>13E112a</string>
<key>DVTMinimumSystemVersion</key>
<string>12.0</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
</dict>
</plist>

看到我们关心的DVTMinimumSystemVersionLSMinimumSystemVersion都在里面。其实也侧面证明了,真正的最低部署版本是macOS 11.0,而不是macOS 12.0(12.0只是苹果为了间接Push Developer去频繁更新macOS的阴谋罢了😂)

那下一步,要做的事情就是用魔改xcodebuild并重新codesign。修改的方式多种多样,你暴力使用Hex Editor也是最简单。但是我更好奇的是这个__TEXT,__info_plist的machO段和节的相关说明。

在网上搜索了一下相关资料,很容易就找到了感兴趣的资料:

  1. The Power Of Plist:解释Info.plist可以内嵌在二进制中
  2. Gimmedebugah: how to embedded a Info.plist into arbitrary binaries:对任意已有二进制注入Info.plist
  3. 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一波。

1648210752222_811d0d800592f601f309e48b732f1194

codesign:

1
2
codesign --remove-signature /Applications/Xcode-13.3.0.app/Contents/Developer/usr/bin/xcodebuild
sudo /Applications/Xcode-13.3.0.app/Contents/Developer/usr/bin/xcodebuild -license

测试一下CLI,很正常

1648210752374_0a5d6ef92b0e270d6164a05153565572

最终替换步骤

  1. 修改Xcode-13.3.0.app/Contents/Info.plist中的LSMinimumSystemVersion的值为11.0
  2. 替换Xcode-13.3.0.app/Contents/Developer/usr/bin/xcodebuild中的DVTMinimumSystemVersion的二进制为11.0,或者使用我这个已经替换好的(建议还是手动参考上面步骤[修改DVTMinimumSystemVersion]替换,授人以渔而不是授人以鱼)

发现iOS SDK的Bug - Hopper使用教程向

作者 DreamPiggy
2019年6月14日 05:39

Hopper简介

Hopper,全称Hopper Disassembler,是一个macOS和Linux平台上的反汇编IDE。提供了诸如伪代码,子程序,脚本,Debugger,Hex编辑等等一些列工具。相比于其他知名的反汇编工具如IDA,最大的好处是对平台特性,也就是Objective-C的反汇编有优化,提供非常贴近原始代码的伪代码(IDA目前则会是保留诸如objc_msgSend的伪代码),并且新版本也对Swift提供了一定的反汇编符号优化,因此作为探究iOS平台上的SDK实现,可以说是一利器。

Hopper安装

Hopper本身目前是收费的软件,提供了免费的使用(30分钟)。官方下载地址为:https://www.hopperapp.com

Mac版本后解压,拖到Application下即可使用。

对于个人使用,价格不菲,有两种方案,个人比较推荐第一种

  • Per User:收费为¥700,允许同一时间唯一激活,不绑定机器硬件
  • Per Computer,收费¥900,和一台电脑的机器硬件绑定

对于只是尝鲜或者轻度使用,其实使用免费版即可。网上现在也有针对旧版本的Cracked版本,不过存在一些问题和崩溃。如果是在需要,可尝试链接

Hopper使用

Hopper提供了一个教程,可以参考官方简易教程

针对我们的场景:分析iOS的SDK内实现或者问题,我这里提供了一个Step By Step的过程,教你如何查找问题。

获取需要反汇编的二进制文件

首先,我们需要获取一份iOS SDK的二进制Mach-O文件。最简单的方式,是通过Xcode提供的iPhone模拟器去获取它。在获取之前,我们先了解一下iOS SDK对应的二进制文件路径。

  • Xcode自带模拟器,对应系统根路径:

Xcode 11:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot

Xcode 10:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/

  • 已下载的历史版本固件的模拟器,对应系统根路径,自己根据版本版本修改中间的数字:

/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 11.4.simruntime/Contents/Resources/RuntimeRoot/

  • 真机的系统根路径,不用说了吧:

/

iOS 系统提的库和二进制,可以简单分以下几类,按照需要选择对应的相对根路径:

  • 公开Framework: /System/Library/Framework
  • 私有Framework:/System/Library/PrivateFrameworks
  • 系统App:/Applications
  • UNIX动态库: /usr/lib

这里我们以Xcode 10自带的iOS 12 SDK,UIKitCore为例(注意,UIKit从iOS 12开始,为了支持部署到macOS,将代码基本全盘移动到了私有Framework的UIKitCore.framework中,UIKit.framework只是一个外层的壳),我们就能直接去访问这个路径,获取它的Mach-O二进制:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore

一般来说,iPhone模拟器提供的二进制Mach-O即可够用,虽然它实际上是x86_64架构的编译产物,但是基本上的逻辑和真机上是一致的。如果涉及到需要只能在真机可用的库,如AVFoundation的摄像机,建议可以从真机中提取(也可以从iOS的IPSW固件中提取),见下文。

真机获取系统库的二进制文件

获取dyld shared cache

在真机上,为了加快动态库的加载,并减少iOS 占用磁盘的体积,dyld采取了一个缓存,将多个Mach-O文件合并到一起,由系统启动后就预热。因此,实际上系统库(公开和私有)的路径上,只有Framework和其中的资源文件,却没有对应的Mach-O二进制。我们需要首先获取到这个cache,然后解压出来对应的二进制。参考dyld_shared_cache

对应dyld shared cache路径(以arm64机器为例):/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64

当然,除了使用已经越狱的真机,我们还可以通过IPSW,即iOS的固件包,来直接提取对应的dyld shared cache,并解压得到对应的Mach-O文件。

IPSW可以从这个网页上下载,选择你的设备以及iOS版本号,就可以下载对应的IPSW文件。

将下载的IPSW解压(建议使用zip命令行,或者BetterZip之类的解压软件,Mac自带的解压似乎会报错),可以找到一个最大容量的DMG文件,双击即可加载

加载后就是完整的iOS系统根路径了,从对应路径下找到dyld shared cache。

解压dyld shared cache

为了解压dyld shared cache,市面上一些工具其实早已过期,要么不支持arm64,要么存在Bug。但实际上,Apple自己开源的dyld源代码,就已经包含了这样一个命令行工具,叫做dsc_extractor,我们这里直接用来源码来编译一份来使用即可。

进入opensource.apple.com选择最新的macOS的版本,我这里例子使用的是我写这篇文章最新公开的 macOS 10.14.1

然后下载两份代码,一份是dyld,一份是CommonCrypto

为了编译,需要一点小技巧,但是对于iOS开发者我觉得挺简单

  1. 用Xcode,打开dyld代码中的dyld.xcodeproj
  2. 修改Build Settings中,把对应的Base SDK,从macosx.internal改成公开的macOS
  3. 进入dsc_extractor.cpp,看到最后有一个test program,把上面的#if 0改成#if 1
  4. 我在编译新版本时发现依赖了一个叫做CommonDigestSPI.h的私有头文件,这个在下载的CommonCrypto工程中,拖进来改一下引用方式即可
  5. 选择dsc_extractor,Archive得到一个产物,叫做dsc_extractor.bundle,然而他实际就是一个Mach-O二进制,直接删掉后缀,chmod+x,即可使用

如果上面的编译比较麻烦,可以直接下载我这里编译好的一份二进制,然后放到你的PATH路径下:dsc_extractor

然后我们可以使用dsc_extractor来解压我们提取到的dyld shared cache,很简单的命令

1
dsc_extractor ./dyld_shared_cache_arm64e ./output

会得到所有dyld shared cache中的二进制Mach-O文件,按照路径排列,然后我们就可以用自己想反编译的库,如UIKitCore,来使用Hopper了。

载入Hopper

现在我们已经有了一个UIKitCore的Mach-O文件了,我们打开Hopper来载入它。我们可以使用Command+Shift+O来选择一个Mach-O文件,也可以将文件拖动到Hopper界面上来打开。

载入Mach-O文件后,Hopper会弹出框来选择具体分析的内容,大部分情况直接确认即可。如果是分析其他类型的文件,可能有特例如下:

  • 分析一个.a或者.dylib,并且该二进制由多个.a或者.dylib合成,这时候会提示你选择具体的某个编译产物
  • 分析一个FAT Binary,这时候会提示你选择具体的某一个架构的文件

载入开始后,一般需要等待一段时间来分析(下方会有进度条),等待分析完成后,你可以将当前分析的结果,保存成一个.hop结尾的文件,未来就不再需要分析了,非常有用(注:免费版不可用)。

符号分析

左侧有一个符号框,从左到右依次表示:

  • Labels: C/C++Objective-C的符号,包括类名,方法名,全局变量等
  • Proc:子程序,对应C/C++的函数,Objective-C的方法,Block代码段等
  • Str:常量段,包括了所有C/C++Objective-C字面量,即代码中直接用@"", ""写的内容

每项内容都支持搜索,一般来说取决于我们要解决的问题,有大概几个场景

  1. 分析特定方法的实现:使用Proc搜索
  2. 运行时抛出的异常或者Log:使用Str搜索关键字
  3. 得知一个类的所有方法:可以使用Label,但更好的方式是通过Class-dump获取头文件(见下)

Class-dump与私有头文件

Class-dump是一个能够解析Mach-O文件,对应的Objective-C符号,以生成一个完整的头文件的工具。得益于Objective-C运行时和符号的特点,可以方便的还原回基本接近原始的类声明代码。具体使用也很简单,参见项目的Readme,编译得到二进制,放到PATH中,然后执行:

1
class-dump UIKitCore.framework -r -o output -H

对于重头戏,关于iOS SDK的所有头文件,早有专人建立了一个在线网站去分析,点击跳转:iOS Runtime Headers

在这个网页上,可以支持Framework/类/方法级别的搜索,支持点击头文件跳转链接,非常的方便,一般的分析iOS SDK都可以采取这个网页的结果来辅助分析。

伪代码分析

当我们了解到需要分析的符号方法后,下一步一般就会进行伪代码分析。在Hopper中,点击到一个子程序入口,然后点击上方的这个像是if (b)代码的图标,即可打开伪代码分析框

对于简单的代码,我们基本上能够还原回100%可读的Objective-C代码,由于ARC时便一起,我们可以看到对应的Retain和Realse调用

分析调用关系

我们可以通过对应的子程序页面,右键选择”References To Selector”,来查看所有对这个Selector的调用。(由于Objective-C运行时的特点,只能是Selector级别的调用,如果有不同类的同名Selector,可以在弹出的窗口中搜索或者依次检查)

常见的分析姿势

Block

Objective-C会使用到Block,而Block由于其实现原理,会生成对应的C方法,Hopper目前原生解析的Block语法并不是很直观,这里提供一个简单的说明。

其实Hopper反编译出来就是Block实现的原理,如果对于Block实现原理不清楚,建议可以先看一遍《这个教程》

简单Block

1
2
3
dispatch_async(dispatch_get_main_queue(), ^{
printf("%d", 1);
});

Hopper原生反编译如下,实际Block代码会单独在另一个C方法中,在block implemented at:提示对应的方法中

1
2
3
4
5
6
dispatch_async([objc_retainAutoreleaseReturnValue(*__dispatch_main_q) retain], ^ {/* block implemented at ___29-[ViewController viewDidLoad]_block_invoke */ } });

void ___29-[ViewController viewDidLoad]_block_invoke(void * _block) {
printf("%d", 0x1);
return;
}

捕获变量

如果Block捕获了变量,那么根据Block的实现原理,可以知道这些变量在Block中可见的变量都是被值宝贝,对于NSObject就是指针

如果使用__block修饰,那么会保留原始的变量的指针,对于NSObject就是对象指针的指针,我们可以通过这个简单识别。

比如对于这样代码:

1
2
3
4
5
6
NSObject *obj = [NSObject new];
__block NSObject *obj2 = [NSObject new];
[self testBlock:^(int value){
NSLog(@"%@", obj);
obj2 = nil;
}];

实际反编译出来的结果长这样:

1
2
3
4
5
int ___29-[ViewController viewDidLoad]_block_invoke(int arg0, int arg1) {
NSLog(@"%@", *(arg0 + 0x20));
rax = objc_storeStrong(*(*(arg0 + 0x28) + 0x8) + 0x28, 0x0);
return rax;
}

对应的arg0就是第一个参数,而最后参数对应的是block_impl_0实现结构体,可以忽略。

CGRectMake等inline的C方法

一些带有inline数值计算的方法,会被苹果的clang在编译时优化,实际上并不是你看到的头文件的样子,这种就需要我们枚举出来,人肉还原回他的实现,举个例子:

这样的代码:

1
2
CGRect rect = CGRectMake(0, 1, 2, 3);
NSLog(@"%@", NSStringFromCGRect(rect));

反编译结果:

1
2
3
4
5
intrinsic_movsd(xmm1, *double_value_1);
intrinsic_movsd(xmm2, *double_value_2);
intrinsic_movsd(xmm3, *double_value_3);
_CGRectMake(&var_30, _cmd, rdx, rcx);
NSLog(@"%@", [NSStringFromCGRect(*(&var_30 + 0x10), *(&var_30 + 0x18)) retain]);

可以看到有mov之类的汇编命令调用,其实这就是为了压栈其实大部分场景我们只要熟悉简单的mov add sub mul几个基本的汇编命令的意义即可。

Swift

Swift作为Apple一致力推的下一代官方编程语言,随着iOS 13的发布,现在已经可以作为第一优先的SDK支持语言了,iOS 13上出现了4个Swift Only的库,因此对于Swift相关的反编译需求,也会慢慢出现。然而,不同于动态性强的Objective-C代码,Swift天生的静态强类型语言特性,造成了相当高的反编译难度(堪比C++开O2优化),在这里基本不细讲,只是大概说一下目前的状况。

Hopper从v4开始支持了对Swift符号的符号化,我们不再需要使用swift来反解决mangled的符号名。

由于Swift支持完整的命名空间,查询符号需要带上完整的符号

同时,Swift由于clang的优化,会讲很多编译器检查到的频繁的代码调用,自动转换为一个以sub开头的函数,以减少二进制大小。

对于Swift非@objcdynamic的属性和方法,会类似于C++的虚函数表,实际上的调用都是编译器展开的地址偏移,而不像Objective-C那样有符号可查。这种时候我们需要就是类似C++反编译那样,通过分析Swift class或者struct的属性,来对照偏移量得知调用。

对于Swift的会触发运行态的一些语法,需要你对Swift语言实现有了解,比如Protocol Extension Where子句,会生成Protocol Witness,我们可以在Hooper中搜索到它

可以看到,目前的Hopper对Swift有相应的支持,但受限于Swift的语言性质很难直观阅读,必要时候还是需要一些汇编,以及传统C++的反编译分析模式去对待它

总结

这篇教程基本上是从我个人的使用经验来介绍,以工具和流程为主,主要是为了给目标iOS平台,且不是专攻二进制安全的人来阅读。

其实对大部分iOS平台开发者,最主要的目的,其实是为在发现一些iOS SDK表现奇怪的行为,或者Crash时,能够有一定的分析和判断能力,去尝试定位原因,绕过问题,并最终能够有底气,去向Apple提交Bug Report。

反编译本身就是二进制安全中的灰色地带,而且还有类似二进制加固等攻防模式,并不是万能方式去了解一个程序运行的方式。还需要配合自己的编写代码经验,才能更好地解决问题

参考

❌
❌