做逆向必须先熟悉 iOS 开发,默认都会,所以本文都是简述。

1 界面、交互、调试

视图树

Xcode 可以点击 Debug View Hierarchy 查看视图树,例如:

iOS vhdx ios vhdx分析_App


明确地展示了视图层级关系,点击某个视图可以查看非常详细的属性。

上篇文章中的 Reveal 提供了更强大的功能。

内存图

Xcode 可以点击 Debug Memory Graph 查看对象引用关系、内存申请情况等,例如:

iOS vhdx ios vhdx分析_可执行文件_02


1 可以十分明确地看出对象间引用关系:图中的 UINavigationController 通过属性 _childViewControllers 引用了一个 NSMutableArray,可变数组对象,数组偏移8字节处,也就是数组的第二个元素,引用的是一个自定义的 Controller 对象。

内存管理的核心就是引用计数,对象间引用关系利用这个工具可以一目了然。

图中的引用关系太简单,看不出优点,可以自己写一个引用较混乱的看一下。只要得到某个对象的地址,比如是从 Debug View Hierarchy 看到的地址,在内存图中直接搜索该地址,就能看到对应的内存和引用关系。

2 以图中的 _childViewController 为例,点击后右侧可以看到它作为成员变量的偏移量: <UINavigationController 0x7fcce800c200> [1536] +416,lldb 用 po 命令查看某地址上的对象,便于调试,这样就可以便捷查看对象或某个成员了。后面会详细介绍 LLDB。

响应者链

如果想复习的话,这里有响应者链的文章链接,不看也行。

2 存储相关

沙盒

iOS 的沙盒(Sandbox)包含:

  • Bundle Container:存储的是 .app,不可写,否则破坏了签名
  • Data Container:真正的“沙盒”,随便读写。默认包含 Documents Library Temp 三个文件夹
  • iCloud Container:需要去苹果开发者中心添加、项目内打开,然后才能读写。

前面有文章介绍了前两个 Container 的路径,iCloud Container 具体路径我忘了,但可以用 [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil] 获取(项目必须打开 iCloud 存储能力)

持久化方式

在沙盒内直接创建、读写文件即可实现持久化。除了自定义的格式,常见的有:
property list,settings bundle,数据库文件等。

property list 文件分三种,通过 file 命令可以查看具体格式。分别是 XML(最常见的 xxx.plist 就是),binary,ASCII。binary 格式的无法查看,可以用 plutil 命令转换为 XML 格式的查看,如 plutil -convert xml1 xxx.log -o xxx.plist 。前面提到的内存图就可以导出为文件,格式就是 binary property list。User Defaults 就是 XML 格式的 xx.plist。

settings bundle 就是系统设置里可以对 app 进行的设置。有一套规则,但一般 App 都没做,只有苹果的 App 实现了。

数据库类的 SQLite、CoreData(本质是封装的 SQLite)的 .db 文件,可以通过 brew cask install db-browser-for-sqlite 安装的软件查看数据库有哪些表 等信息。

读取钥匙串(非业务读写):下载 keychain_dumper,复制(scp 命令)到手机,chmod +r /var/Keychains/keychain-2.db 确保文件可读,然后执行 keychain_dumper,就能看到各个 App 存到 keychain 的信息了!

3 runtime 基础

基础

C/C++ 基础,数据结构基础,操作系统基础,主要就体现在对代码段、数据段、堆、栈的理解。如果学过汇编,这些基本没问题。
ARM 汇编、LLDB、DYLD、Mach-O 后面的文章会详细介绍。

runtime 源码

做逆向的话,下面列的源码的具体实现不一定要读,但要熟悉部分 API,比如根据字符串获得类对象,创建、注册一个类,给类添加方法等。如果要做大厂 iOS 开发工程师,以下全得读。源码下载。

逆向必看的部分:
熟悉对象与类:objc-private.h 和 objc-runtime-new.h
熟练使用 API:runtime.h 和 NSObject.h

iOS 中/高级开发的部分:
对象与类
实现类过程(realizeClass)
读取 image 过程(_read_images)(load_images)
消息机制,方法缓存
引用计数管理,弱引用管理,自动释放池
关联对象
@synchronized
runtime 外的比较重要的源码:
block 源码
Foundation 中 Runloop 的源码
GCD 一部分源码

上面源码有些在我 [iOS 理解] 系列文章中解读过,有兴趣的可以看一下。

实践

典型:KVO 原理,Method Swizzling,Zombie 对象原理,各种 __attribute__ 实现原理…

Method Swizzling 是常见的 Hook 手段,所以要熟练使用 runtime API。后面文章会仔细介绍 Hook。
总之,必须得跟 runtime API 混个眼熟,熟悉内存。

4 App 构建

App 获取

iTunes 12.7 之后没有 App Store 了,不能再下载应用包。iTunes 下载下来的是 .ipa 格式的压缩包,其实就是 .zip 格式的,解压后是 Payload 文件夹内有一个 xxx.app(其他可忽略)。虽然可以下载旧版 iTunes 再下载 App,但完全可以通过越狱设备安装后再复制出来 xxx.app,放入 Payload 文件夹再压缩,是一样的。

如果想下载旧版本的 App,可以用 Charles 抓包,得到各个版本号,修改请求某个版本,挺麻烦。Cydia 上有插件实现了这个过程,可以从 App Store 选择 App 旧版本下载,插件叫 AppStore++ 降级神器。

获取到 xxx.app,观察 app 包内容,包含:可执行文件、库文件、配置文件、资源文件等。

App 构建

先用 Xcode Build 一个项目,点击 navigator 最后一个,就是查看具体过程的。
观察构建过程可以发现,实际只是 Xcode 帮我们执行了一系列命令,大致过程:
1 编译代码
2 链接
3 编译 storyboard/xib 文件
4 复制 Provisioning Profile
5 生成 entitlements
6 签名

先简单介绍一下这一系列过程中涉及到的东西。

clang

clang 是一个编译器,兼容 gcc,所以基础用法和 gcc 相同。

经常用 gcc 在命令行中编译 C 程序的话,下面几个命令应该很熟悉:

1 gcc -c 生成可重定位文件(俗称目标文件。按规范,目标文件包含 可重定位文件、共享目标文件即动态链接库、可执行文件)
2 nm 命令查看目标文件中的全部符号。可以看到符号的信息:虚拟地址(可重定位文件中全0);所在位置:代码段/数据段/未定义(外部定义,只找到声明)。通过参数可以分类查看:可被外部引用的符号;调试程序加进去的符号;只看/不看外部定义的符号
3 file 命令查看文件格式 relocatable/executable 等基础信息
4 ld 命令链接,编排地址,即重定位,生成可执行文件。-Ttext 指定最终生成的可执行文件的起始虚拟地址;-e 或 --entry 指定程序从哪里开始执行,参数可以是数字地址,可以是符号名,默认值是符号 _start。

这些是编译、链接的基础概念,clang 可能会加很多参数,但核心的编译链接流程是相同的。

xcrun

xcrun 是 Xcode 提供的用于编译打包的命令,比 clang 更好用一些。常见使用方法:

1 编译
xcrun	-sdk iphoneos clang 	(真机 sdk)
		-arch arm64 			(arm64 架构)
		-mios-version-min=11.0 	(支持的最低版本)
		-F UIKit				(链接哪些 framework)
		-fobjc-arc 				(开启 ARC)
		-c main.m 				(仅编译源文件)
		-o main.o 				(输出可重定位文件)
2 链接
xcrun	-sdk iphoneos clang
		main.o file1.o file2.o
		-arch arm64
		-mios-version-min=11.0
		-fobjc-arc
		-framework UIKit
		-o exec	(输出可执行文件)
ibtool

ibtool 用来编译 storyboard/xib 文件。

ibtool	a.storyboard 
		--compile a.storyboardc
	 
ibtool	a.xib 
		--compile a.nib
actool

actool 用于打包项目中保存在 Assets.xcassets 中的资源文件,该命令保存在 /Applications/Xcode.app/Contents/Developer/usr/bin/actool 位置。

actool	Assets.xcassets 
		--compile outputDir		(需要提前创建输出文件夹)
		--platform iphoneos 	(目标设备类型)
		--minimum-deployment-target 8.0

执行后,Assets.car 被输出到指定文件夹内。

下载 Provisioning Profile

复习第一节,Provisioning Profile 文件即 .mobileprovision 后缀的文件,内容包含:
(App ID + 测试设备 IDs + App 权限)+ 签名,是从苹果服务器请求签名后下载的,用于安装 App 时验证。

对于免费的开发者,好像无法从开发者网站下载,但如果仅仅尝试手动打包过程,可以先在 Xcode 里配置项目的 Team、Bundle Identifier,然后 Xcode 会自动请求配置文件,并下载到 ~/Library/MobileDevice/Provisioning Profiles/xxx.mobileprovision,直接拿来用就行了。

可以通过 security cms -D -i xxx.mobileprovision 查看该文件内容。

xxx.app 文件夹

上面提到的 可执行文件,storyboardc 文件,Assets.car 文件,nib 文件和 plist 文件,第三方 framework 文件,Provisioning Profile,其他资源文件,都拷贝到一个文件夹内,文件夹命名为 xxx.app。需要注意 info.plist 可以直接复制项目里的,但要把里面值为 $() 的选项手动填上;注意多语言文件夹,比如 storyboardc 默认放在 Base.lproj 文件夹内。

iOS vhdx ios vhdx分析_逆向工程_03

dsym

.dSYM 文件,是二进制地址与源代码符号映射表,可以从崩溃信息中的二进制地址解析出源代码的符号。

dsymutil -arch arm64 链接生成的可执行文件

这一步也可以去掉。

codesign

在项目里打开某项能力时,比如打开 iCloud 容器存储,会自动生成一个 xxx.entitlements,就是这个 app 的权限文件,如果这个能力是不允许免费开发者用的,编译时会报错:
Your development team does not support the iCloud capability. 如果这个能力是访问相机的,则可以编译成功。

现在要手动生成这个文件,首先文件类型是 property list,XML 子格式,随便找一个 plist 文件复制内容,然后修改吧。。。内容是一个字典,先添加两个键值对:
application-identifier -> App IDget-task-allow -> YES 然后添加其他能力。
用 Xcode 编辑可以快一点,如果手写 XML,比如添加苹果登陆能力:

<key>com.apple.developer.applesignin</key>
	<array>
		<string>使用苹果 ID 登陆</string>
	</array>

执行签名:

codesign
		-fs '证书名'
		--entitlements entitlements文件
		xxx.app路径

此时如果再修改 app 包内容,需要重新签名。此时 xxx.app 内会出现一个 _CodeSignature 文件夹,里面包含了资源文件的签名;目标文件(可执行文件、动态库)的签名会写到目标文件中,后面 Mach-O 文件格式中有详细介绍 。
执行后,新建 Payload 文件夹,把 xxx.app 放进去。把 Payload 压缩成 zip,改后缀为 ipa,即打包完成。

命令行构建实践

如果要构建多个项目,用命令行每次都得输入同样的命令。所以用 makefile 或其他脚本提高效率。
我写了一个测试项目,名字 CA_MaskLayer,makefile 如下。(各个步骤没能连起来一次性执行,只是单独的各个步骤的目标)

这个 makefile 里包含了上文大部分步骤,但有的步骤的命令没写,比如压缩命令。

PROJECT 	=$(PWD)
BUILD_DIR   =$(PROJECT)/output
PRODUCT_NAME=CA_MaskLayer
APP_NAME  	=$(PRODUCT_NAME).app

BIN_DIR 	=$(BUILD_DIR)/$(APP_NAME)
SRC_DIR 	=$(PROJECT)/CA_MaskLayer
SB_DIR		=$(SRC_DIR)/Base.lproj
ASSETS_DIR	=$(SRC_DIR)/Assets.xcassets
ENTITLEMENTS=$(BUILD_DIR)/entitlements.plist


SOURCES	 	=$(wildcard $(SRC_DIR)/*.m)
OBJ			=$(patsubst %.m, %.o, $(SOURCES))
TARGET		=$(PRODUCT_NAME)
 
SDK 		=iphoneos
CC			=@xcrun -sdk $(SDK) clang

STORYBOARDS =$(wildcard $(SB_DIR)/*.storyboard)
STORYBOARDCS=$(patsubst %.storyboard, %.storyboardc, $(STORYBOARDS))
ASSETS_CAR	=$()
IBTOOL 		=@ibtool
IBFLAGS		= --compile
ACTOOL 		=@/Applications/Xcode.app/Contents/Developer/usr/bin/actool
ACFLAGS		=--platform iphoneos --minimum-deployment-target 8.0

DSYM 		= dsymutil
CERTIFICATE ='iPhone Developer: erisl@icloud.com (AFB945DAJK)'

CFLAGS		= -Wall -arch arm64 -mios-version-min=11.0 -F UIKit -F QuartzCore -fobjc-arc
LDFLAGS		= -arch arm64 -mios-version-min=11.0 -framework UIKit -framework QuartzCore -fobjc-arc

# 链接生成可执行文件
link:$(TARGET)
$(TARGET):$(OBJ)
	@mkdir -p $(BIN_DIR)
	$(CC) $(OBJ) $(LIB_PATH) $(LIB_NAMES) $(LDFLAGS) -o $(BIN_DIR)/$(TARGET)
	
# 编译为可重定位文件
compile:$(OBJ)
%.o: %.m
	$(CC) $(CFLAGS) $(INCLUDES) -c  $< -o $@

# 编译 sb 文件
storyboard:$(STORYBOARDCS)
%.storyboardc: %.storyboard
	$(IBTOOL) $(IBFLAGS) $@ $<

assets:$(ASSETS_DIR)
 	$(ACTOOL) $< --compile $(BIN_DIR) $(ACFLAGS) 
 
dsymbol:$(TARGET)
	$(DSYM) $(BIN_DIR)/$(TARGET)

# TODO: - fix dependency
codesign:$(TARGET)
	@codesign -fs $(CERTIFICATE) --entitlements $(ENTITLEMENTS) $(BIN_DIR)

rm-obj:
	@rm $(OBJ)
rm-sb:
	@rm $(STORYBOARDCS)
rm-all: rm-obj rm-sb

clean:
	rm -rf $(BUILD_DIR) rm-all

按照上面的过程构建时,只需要:
make compile
make link
make storyboard
make assets
复制并修改 info.plist
复制各种资源文件
make dsymbol
make codesign
移动到 Payload
压缩

效率勉强提高了一些。

5 重签名

前面文章介绍了 app 包内容、打包过程和签名过程。同时也可以得出,一旦改变了应用的二进制文件,或者修改了应用里面的资源文件,应用的签名就会验证失败,无法安装到手机上。这时就需要对应用重新签名。这部分属于后面文章的内容,不过上面提到了打包签名,这里趁热打铁把重签名一并介绍。

对于我们自己开发的 App,直接执行上面的 make codesign 就行了。那么第三方的 App 呢?我们没有 App 开发者的私钥怎么签名?可以换一种思路来实现。

重新签名首先本地需要有一个开发者证书。可以通过钥匙串打开查看,也可以执行 security find-identity -v -p codesigning 在终端打印出来。新建一个 iOS 工程,让 Xcode 帮忙生成一个 Provisioning Profile 文件 xxx.mobileprovision。执行 security cms -D -i xxx.mobileprovision > xxx.plist 生成一个 xxx.plist 文件备用。

将一个砸壳后的 app 中 Framework 目录下的 framework、Plugins 目录下的插件、Watch 目录下的 extension 都重新签名:codesign -fs "iPhone Developer: xxx" xxx.framework。修改此 app 内的 Info.plist 中的 Bundle Identifier 为新建的项目的 Bundle Identifier。

对 app 包重新签名:codesign -fs "iPhone Developer: xxx" --no-strict --entitlements=xxx.plist xxx.app。然后打包,即可成功安装到手机上。

我个人理解,这个流程就像是是把 App 的所有文件“据为己有”,盗版了一个项目签了自己的名。