前言
本文是自己对iOS逆向工程领域所学的一个总结,文中所用的例子仅为学习研究使用。
1. 逆向工程是什么?
从iOS应用的功能或行为入手,通过脱壳、运行时分析、静态分析、动态调试、Hook、注入等一系列技术手段,推导出目标文件的结构、流程、算法、代码等,称为“iOS应用逆向工程。
iOS逆向工程包括但不限于以下内容:
2. 为什么要学习逆向工程?
通常我们对某个APP进行逆向是为了在没有源代码的情况下了解这个APP的结构或该APP某个关键功能的代码实现等,比如可以针对某个第三方APP进行界面布局或者功能逻辑的分析等。
本文将介绍iOS逆向领域中的一些常用攻防技术,运用这些技术来分析第三方APP中存在的一些安全问题并且给出防御方案。
3. 如何开始逆向?
首先我们需要准备一台越狱手机。由于iOS平台存在着沙盒机制,沙盒的原理是把应用程序生成和修改的文件重定向到自身文件夹中,在沙盒机制下,每个程序之间的文件夹不能互相访问。所以非越狱手机无法了解它的内部结构、运行时机制等。
越狱后推荐安装的插件 (越狱后会自动安装一个叫做 Cydia的应用,通过Cydia安装插件) :
Cydia Substrate :逆向必备基本框架,封装了很多函数,加入了安全的防崩溃保护机制,使插件开发变得简单高效。
AFC2:用于激活助手类工具对iOS设备所有路径的访问权限。
adv-cmds:提供ps命令,用于查看当前运行进程的PID以及应用的路径。
AppSync Unified:用来绕过系统对应用的签名验证,可以随意安装和运行脱壳后的ipa。
Filza File Manager:可以在iPhone上自由访问iOS文件系统。
NewTerm 2:能运行在手机上的终端工具。
4. 脱壳
iOS端App在上线之前会由苹果商店进行FairPlayDRM数字版权加密保护(称为“加壳”)。
4.1. 什么是加壳?
利用特殊的算法,对可执行文件的编码进行改变(比如压缩、加密),以达到保护程序代码的目的。
要对应用进行分析,就必须先解密(称为“脱壳”),从而得到原始未加密的二进制文件。
4.2. 怎么脱壳?
iOS中有很多好用的脱壳工具,比如Clutch、dumpdecrypted、AppCrackr、Crackulous。
以dumpdecrypted为例进行脱壳,流程如下:
a. 首先我们要生成“砸壳”用的动态库dumpdecrypted.dylib;
b. 到越狱手机上寻找要“砸壳”的app路径(通过ssh连接设备,在终端用ps -e | grep wordios命令获取设备当前运行的所有应用信息中名字包括wordios的应用,我们可以找到wordios的运行路径;
c. 使用Cycript注入wordios进程,然后输入以下指令获取Document目录
d. 然后通过爱思助手进入该目录,将dumpdecrypted.dylib拷贝到该目录下;
e. 终端进入该目录,执行下方命令:
DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /var/containers/Bundle/Application/48C8B703-726F-4304-89E8-7D6F725792F4/wordios.app/wordios
f. 执行命令后会生成已脱壳的可执行文件,后缀名为.decrypted;
g. 通过MachOView工具分析,LC_ENCRYPTION_INFO_64 中的Crypt ID 为0,说明脱壳成功。
5. 攻
5.1. 攻击手段一:运行时分析
运行时分析是常用的攻击手段,具体方案或工具有Cycript / Reveal / LookinLoader / Flex / class-dump等。
5.1.1. Cycript
Cycript是一款脚本语言,混合了OC与JS语法解释器,能够探测和修改运行中的应用程序,主要用于注入目标程序来实现运行时调试,在重启程序后所有的修改会失效。
利用Cycript向目标App注入代码,从表面现象入手来获取当前的界面布局及控制器,从而定位可疑方法或函数,然后进行各种调用测试,最终定位被逆向功能的入口。
安装方法: 越狱设备直接在Cydia中安装
使用方法:
a. 注入目标程序,例如注入运行中的wordios;
b. 使用Cycript语法进行运行时分析。
实例一:
查看wordios的BundleId / APP运行路径 / DocumentPath / CachePath / 当前的KeyWindow / 当前控制器 / 视图层级等等
其中jjcript是我事先封装好的一个接口文件,部分接口实现如下:
实例二:获取按钮点击事件
Cycript还可以获取某个按钮的点击事件,也就是方法名,这对我们分析代码逻辑有很大的帮助,比如我们在使用某社交APP时,使用某个付费功能,如果你是非会员就会弹出一个会员引导弹框,这时候我们可以通过分析这个点击事件,看能否绕过付费的代码逻辑直接使用付费功能。
5.1.2. Reveal / LookinLoader
Reveal / LookinLoader 是一款强大的UI调试工具,可以调试任何一个iOS应用。它可以在运行时查看App的界面层级关系,还可以实时修改程序界面,不用重新运行程序就可以看到修改之后的效果,免去了每次修改代码后又重新启动的过程。逆向工程里面通常用Reveal / LookinLoader来快速定位感兴趣的控件,进而找到控制器,再用Cycript进行事件分析。
(Reveal)
(LookinLoader)
5.1.3. Flex
Flex是一个iOS应用的内部调试工具。当它加载时,会向目标程序上方添加一个悬浮的工具栏,通过这个工具栏可以查看和修改视图的层级结构、动态修改类的属性、动态调用实例和方法、动态查看类和框架以及动态修改UI等。
5.1.4. class-dump
class-dump是一个命令行工具,它利用Object-C语言的运行时特性将二进制文件中的类、方法及属性等信息导出为头文件。
下载方法:从http://stevenygard.com/projects/class-dump/下载最新安装包,双击打开,将“class-dump”拖动到/usr/local/bin目录下即可使用。
以wordios为例来讲解它的使用方法:
class-dump -S -s -H wordios.decrypted -o ./Headers
5.2. 攻击手段二:监听和拦截函数调用
Frida是一个跨平台的轻量级Hook框架,支持所有主流操作系统,它可以帮助逆向研究人员对指定的进程进行分析。它主要提供了精简的Python接口和功能丰富的JS接口,除了使用自身的控制台交互以外,还可以利用Python将JS脚本库注入目标进程。使用Frida可以获取进程详细信息、拦截和调用指定函数、注入代码、修改函数、从iOS应用程序中dump类和类方法信息等。
Frida能实现很多功能,以下介绍几种比较常用的功能:
5.2.1. 获取可用设备列表
● frida-ls-devices
图中显示有一个连接usb接口名为iPhone的设备。
5.2.2. 获取设备进程列表
● frida-ps -U //-U 代表连接到 USB 设备
图中显示该设备正在运行的进程有文档表格编辑,其id为2432。
5.2.3. 结束设备上的某个进程
frida-kill -U 2432//2432就是上面所获取的文档表格编辑的id
在终端运行之后就会立即杀死文档表格编辑进程。
5.2.4. 跟踪函数 / 方法调用
跟踪函数: 比如跟踪id为9218的进程中的compress函数:
frida-trace -U -i compress 9218
当触发该方法时会在终端输出。
跟踪 OC 方法: 比如跟踪文档表格编辑中CourseVC 控制器中的viewWillAppear方法
frida-trace -U -m "-[wordios.CourseVC viewWillAppear*]" 文档表格编辑 //由于该app做了代码混淆处理,经过分析,类名需要wordios.CourseVC这样调用才行。
每次进入CourseVC 界面的时候就会触发viewWillAppear,如图终端中输出了2次,代表CourseVC 触发了2次viewWillAppear方法。
5.2.5. 拦截某个类的所有方法
比如以下场景,文档表格编辑的教程界面,点击Word文档之后,如果是非会员就会弹出解锁功能界面;那么如果我想知道从点击Word文档到跳转解锁功能界面的这个过程中会调用到哪些函数,我们就可以利用frida进行跟踪。
首先使用LookinLoader找到Word文档按钮所在的控制器。
如上图所示,所在控制器为CourseVC。
编写js脚本对CourseVC类的所有方法进行拦截
然后用frida加载该js脚本
frida -U -l wordios.js 文档表格编辑
运行脚本后,效果如上图所示。
关于Frida更多的功能请参考官网https://frida.re/docs/home/
5.3. 攻击手段三:静态分析
静态分析是在程序尚未运行的状态下进行的逆向分析。主要使用反汇编、伪代码、流程图、代码修改等方式,常用的静态分析工具有Hopper、IDA Pro等。
Hopper / IDA Pro能够将Mach-O文件的机器语言代码反编译成汇编代码、OC伪代码或者swift伪代码。
Hopper/IDA Pro的地址都是未使用ASLR的VM Address。
以wordios为例,通过Hopper我们可以查看wordios可执行文件里的所有方法。如果我们对某个方法感兴趣,只要一搜就能看到,但只能看到汇编代码或者OC伪代码实现;我们看到该app有很多比如sub_10004f850这样的函数名,说明该app对大量方法名使用了代码混淆的防护手段。
5.4. 攻击手段四:动态分析
动态分析,简单来说就是,我们可以直接使用LLDB调试别人的程序,在静态分析中必须联系上下文进行分析与猜测,而动态分析时只需要把相关寄存器或栈内容打印出来便一目了然,甚至可以直接修改返回值来验证结果的准确性。
LLDB只能运行在macOS上,若要调试iOS应用,需要先在设备上运行一个叫做debugserver的工具。
debugserver扮演服务器的角色,把在macOS端执行的各种LLDB指令传递给设备端,并把设备端的结果反馈到macOS终端,这种调试方式称为远程调试。
5.4.1. 如何配置debugserver?
如果当前的iOS设备进行过真机调试,那么debugserver会被自动安装到Develop/usr/bin目录,可以通过爱思助手将其导出到macOS上
由于debugserver缺少task_for_pid权限,所以只能调试自己的应用,如果要调试第三方应用,就需要对debugserver进行处理,在iOS11运行还需要加上platform-application权限。
具体步骤:
a. 在导出的debugserver同级目录下创建一个名为“ent.xml”的文件,并输入以下内容
b. 在终端使用codesign进行签名
codesign -f -s - --entitlements ent.xml ./debugserver
c. 将签名后的debugserver复制到设备 /usr/bin/ 目录下,并添加执行权限
chmod +x /usr/bin/debugserver
把debugserber复制到“/usr/bin/”目录的好处是,在任意目录下都可以直接运行debugserver。
- 与LLDB建立连接
开启debugserer
// *:端⼝号
// *代表接受任意IP的连接,使⽤iPhone的某个端⼝启动debugserver服务(只要不是保留端⼝号就⾏)
// -a 进程
// 输⼊APP的进程信息(进程ID或者进程名称)
$ debugserver *:端⼝号 -a 进程
a. 10011端口映射
b. 让设备中的debugserver附加到 wordios 进程
c. 打开一个新的终端,使LLDB连接到debugserer
比如调试 [wordios.CourseVC showMore:],只要知道方法的起始地址,就可以给方法设置断点,我们可以在Hopper中找到这个方法的地址。
但是由于ASLR机制,使得进程每次启动时,地址空间都会被简单地随机化,相当于在Mach-O起始地址前会增加一段随机地址值,所以Hopper中的方法地址并不是真正的地址
真正的地址 = ASLR偏移值 + Hopper方法地址
用以下命令查看应用的ASLR偏移值
image list -f -o
如下图,偏移值为 0x0000000000398000
在Hopper找到showMore:的静态地址 0x000000010006b9ec
计算出方法的真实地址,打断点 (breakpoint set --address ASLR偏移地址+方法静态地址)。
breakpoint set --address 0x0000000000398000+0x000000010006b9ec
5.5. 攻击手段五:Theos
Theos是一个跨平台的越狱开发工具包,它为开发者准备好了一些代码模板、预置了一些基本的Makefile脚本,方便开发插件。
Theos相当于对CydiaSubstrate的封装,因此能实现对Objective-C运行时的Hook,也能实现对C语言函数的Hook。
简单来说,Theos可以通过修改二进制文件里某个方法的实现,以达到破解的目的。
以上是逆向工程中常用的攻击手段,有一点需要注意的是,当我们成功修改完可执行文件之后,直接替换原文件安装到手机上是运行不了,这时候就需要用到重签名了。
5.5.1. 重签名
一旦修改了应用的二进制文件,或者增加、修改了应用里面的资源,应用本身的签名就会被破坏,由于iOS内核中强制部署了许多代码签名机制,运行修改后的应用会闪退甚至安装不成功,这样就可以避免攻击者在用户的设备上传播并运行不受信的代码。
如果希望将破坏了签名的安装包,安装到非越狱的手机上,需要对安装包进行重签名的操作。
有两个注意点:
a. 安装包中的可执行文件必须是经过脱壳的,重签名才会有效。
b. .app包内部的所有动态库(.framework、.dylib)、AppExtension(PlugIns文件夹,拓展名是appex)、WatchApp(Watch文件夹)都需要重新签名
重签名步骤:
a. 准备一个embedded.mobileprovision文件(必须是付费证书产生的,appid、device一定要匹配),并放入.app包中。可以通过Xcode自动生成,然后在编译后的APP包中找到,也可以去开发者证书网站生成下载。
b. 从embedded.mobileprovision文件中提取出entitlements.plist权限文件。
在终端敲以下命令即可提取:
security cms -D -i embedded.mobileprovision > temp.plist
/usr/libexec/PlistBuddy -x -c 'Print :Entitlements' temp.plist > entitlements.plist
查看可用的证书:
security find-identity -v -p codesigning
对.app内部的动态库、AppExtension等进行签名:
codesign -fs 证书ID xxx.dylib
对.app包进行签名:
codesign -fs 证书ID --entitlements entitlements.plist xxx.app
也可以使用一些带有 UI 的重签名工具。例如:
iOS App Signer (https://github.com/DanTheMan827/ios-app-signer)
iOS App Signer可以对.app重签名打包成ipa,需要在.app包中提供对应的embedded.mobileprovision文件。
iReSign (https://github.com/maciekish/iReSign)
iReSign可以对ipa进行重签名,需要提供entitlements.plist、embedded.mobileprovision文件的路径。
重签名的过程中遇到的问题与注意点:
a. 只能对脱壳的可执行文件进行重签名。
b. 工具只能对ipa进行重签名,app内的动态库、AppExtension需要先手动重签名,最后再使用工具进行ipa重签名。
c. 重签名打包成ipa安装到手机时如出现DeviceNotSupportB...错误,应该是info.plist文件里面的UISupportedDevices字段的问题,需要把这个字段去掉再重新打包。
重签名步骤总结(以文档表格编辑为例)
a. 通过爱思助手等工具把文档表格编辑的app包拷贝到电脑上
b. 把已脱壳的可执行文件放到app里面替换原来的未脱壳的可执行文件
c. 把新的描述性文件、需要注入的动态库放进app包里面(与可执行文件同一路径)
d. 对app包里的所有动态库、extension、framework、PlugIns进行重签名(自己加的动态库,改完CydiaSubstrate路径再重签名)
codesign -fs 证书ID 动态库名字
e. 修改所有注入动态库的查找路径,例如注入动态库wordiosfree,需要将wordiosfree的查找路径改成@executable_path/wordiosfree.dylib,如果动态库wordiosfree还依赖其他动态库,一般都要依赖CydiaSubstrate(libsubstrate)这个库,所以还需要将CydiaSubstrate的查找路径改为@loader_path/CydiaSubstrate.dylib(@loader_path/libsubstrate.dylib,需要注意的是,一般是用这个,因为从手机终端导出来的CydiaSubstrate自动改名为libsubstrate,如果使用@loader_path/CydiaSubstrate.dylib会导致路径错误闪退)
otool -L wordiosfree.dylib //查找动态库所依赖的库及其路径
install_name_tool -change /Library/Frameworks/CydiaSubstrate.framework/CydiaSubstrate @loader_path/CydiaSubstrate wordiosfree.dylib //修改CydiaSubstrate的执行路径
f. 把动态库wordiosfree注入到可执行文件里面
insert_dylib @executable_path/wordiosfree.dylib wordios wordios --all-yes --weak
g. 把info.plist里的UISupportedDevices字段删掉
使用工具对app包进行重签名并生成ipa文件,再通过爱思助手安装到手机上
6. 防
前面谈到这么多种攻击手段,那么对应的也存在很多的防御手段,比如数据加密(字符串、网络数据、敏感数据等)、应用加壳(二进制加密,iOS上传appstore会自动加壳,但早已被破解)、代码混淆(类名、方法名、代码逻辑等)、反调试、反注入、Hook检测、越狱检测、完整性检测等。
通过逆向大量市面上很多的APP,我发现大多数都没有做太多的防护措施,不过也有少数像抖音、微信这样的知名APP就做了大量的防护措施。总之,没有防御的APP就像把源代码暴露在别人面前一样,漏洞百出,存在着大量的安全隐患,所以我认为对APP做一些防护是非常有必要的,学习一些逆向知识,还能提高我们的安全意识,在平时的开发过程中编写代码也会时时注意到什么样的写法是有漏洞的,什么样的写法才是安全的。
接下来我会介绍几种逆向领域里常用的防御手段,也称应用加固。
6.1. 防御手段一:代码混淆
iOS程序可以通过class-dump、hopper、IDA等获取类名、方法名、以及分析程序的执行逻辑。如果很容易被别人看到类名、方法名是干嘛用的,就很容易被人破解。所以进行代码混淆,将加大别人的分析难度。
在iOS中,代码混淆主要还是对源码进行混淆,比如类名、方法名、协议名、属性名。通过宏定义混淆,例如把Person这个类的名字混淆成abcd:
define Person abcd
如果直接将类名、方法名改成abcd什么的,确实可以混淆,但是自己维护也很麻烦。
注意点:
● 不能用宏定义混淆系统方法
● 不能混淆init开头的方法
● 如果xib、storyboard用到的内容,需要修改一下,比如说控制器名用到了混淆,那么xib、storyboard中用到名称需要手动去修改一下,因为系统是不会帮你换的(使用某些工具可以自动化替换)
● 不要什么都去混淆,因为混淆代码太多上架会被拒,只混淆关键代码
● 可以考虑将需要混淆的代码就加个前缀,这样就跟正常代码好区分
6.2. 防御手段二:反调试-ptrace
ptrace是一种对运行中的进程进行跟踪和控制的手段,通过ptrace,一个ptrace可以动态地读写另一个进程的内存和寄存器,包括指令空间、数据空间、堆栈以及所有的寄存器。同时,ptrace还提供了一个非常很有用的PT_DENY_ATTACH参数,用来告诉系统阻止调试器附加,所以,最常用的反调试就是通过调用ptrace来实现。
参数1:要做的事情
参数2:要控制的进程ID
参数3:地址
参数4:数据
参数3和参数4都由参数1决定要传递的地址和数据。
参数1的列表
调用方式:
效果:
当我们尝试使用动态调试时,会直接报segmentation fault:11错误。
6.3. 防御手段三:反注入
当自己编写的dylib无法加载到目标应用的时候,就表明此应用做了反注入保护。我们可以通过Build Settings -> Signing -> Enable Hardened Runtime 改为YES,或者Capabilities来设置。这样在runtime的时候,会检查注入库的签名与ipa的签名是否一致,签名不一致就无法注入。
以下链接是官方给出的解决方案:
https://developer.apple.com/documentation/security/hardened_runtime
6.4. 防御手段四:Hook检测
iOS上的Hook方式主要就两种,Method Swizzing和Inline Hook。
Method Swizzing的原理是替换IMP,所以可以通过dladdr得到IMP地址所在的模块信息进行判断,示例如下:
Inline Hook的原理是通过修改函数的前N字节内存,使程序跳转到自己编写的函数,我们要做的就是检测函数头部若干字节是不是被修改。
6.5. 防御手段五:越狱检测
很多应用为了确保安全性需要对越狱环境进行检测,如果机器越狱了则不启用某些敏感功能(比如微信的指纹支付功能,在越狱状态下就会自动关闭)。
具体方案:
6.5.1. 检测越狱商店及其附属文件
越狱环境几乎都安装了Cydia、Sileo及其附属文件,可以用下列代码检测:
6.5.2. 尝试读取系统应用列表
如果设备已经越狱,就可以读取到系统应用列表的内容,以此来作为检测依据,可以用下列代码检测:
6.5.3. 检测URLSchemes是否有效
URLSchemes是苹果公司提供的用来跳转到系统应用或跳转到其他应用的一种机制。
这种机制提供了一种检测越狱的思路:
如果能打开Cydia或其他非官方应用,则说明已经越狱,利用“- (BOOL)canOpenUrl:(NSURL *)url”方法来测试相应应用的URLSchemes是否有效就能判定。
6.6. 防御手段六:完整性检测
完整性检测可以确认目标文件是否被脱壳、是否被静态注入、内容是否被修改、是否被重签名等。常见的方式有:
6.6.1. 加载命令检测
为了使未越狱的设备也能顺利使用插件,通常会在LoadCommand(加载命令)中增加一个LC_LOAD_DYLIB或者LC_LOAD_WEAK_DYLIB的方式来指向自己的动态库路径,从而实现注入功能,这种方式通常也被称为“静态注入”。
记载命令检测即检测Mach-O文件的LoadCommand中的LC_LOAD_DYLIB或LC_LOAD_WEAK_DYLIB是否存在异常,代码如下:
6.6.2. 代码段检测
代码段检测技术主要用来检测“_TEXT, _text”是否被修改(当然也可以检查其他的section),示例如下:
其中,&_mh_execute_header指向Mach-O在内存中的起始位置,getsectiondata函数返回指定Section的内存地址及大小。
实际应用中可以将正确的Hash值存到服务器或者加密后存到本地,在进程启动时进行比对,如果不匹配则可以做一些逻辑处理。
6.6.3. 签名信息检测
被修改的应用若要在非越狱机器上使用,必要的一个环节就是对其重签名。
签名信息检测就是检测LC_CODE_SIGNATURE中的特征信息(比如TeamID)是否和用户自己的相匹配。
7. 实战案例:某即时通信软件 - 猜拳和扔骰子代码实现分析
7.1. 目标 App 介绍
本文的实战目标是一款著名的即时通讯App,可以通过手机网络发送表情包,比如猜拳和扔骰子,这两个是官网自带的表情包,发出去之后先是有一个不停跳动的动画,最后会停留在一个随机的剪刀/石头/布或者点数,这一切看起来都是随机的,但是通过逆向我们是能够实现自动操控并且投出想要的结果的。
7.2. 寻找切入点
逆向一个App首先需要寻找切入点,在本案例中,要想分析猜拳和扔骰子的实现,我们首先得从UI层面入手,找出当前控制器,然后监听点击表情包之后的方法调用,找到关键方法,分析该方法的代码实现,并且寻找代码漏洞,一旦找到可以hook的代码逻辑,那么就可以实现我们想要的代码逻辑了。
7.3. 分析过程
使用Lookin我们可以知道当前控制器是BaseMsgContentViewController,然后进一步分析界面:
可以看到,这是一个UICollectionViewCell,自然而然就会想到点击它会触发
以下代理方法:
- (void)collectionView:
(UICollectionView *)collectionView didSelectItemAtIndexPath:
(NSIndexPath *)indexPath;
我们可以通过断点BaseMsgContentViewController类的didSelectItem...方法来分析代码逻辑,但还有另一种更加便捷的方法是直接监听BaseMsgContentViewController的所有方法调用,这里我使用的是Frida + js脚本去跟踪发送骰子时该类所有的方法调用
-[BaseMsgContentViewController useTransparentNavibar]
-[BaseMsgContentViewController useTransparentNavibar]
-[BaseMsgContentViewController shouldInteractivePop]
-[BaseMsgContentViewController toolView]
-[BaseMsgContentViewController SendEmoticonMesssageToolView:]
-[BaseMsgContentViewController findNodeDataByLocalId:]
-[BaseMsgContentViewController addMessageNode:layout:addMoreMsg:]
-[BaseMsgContentViewController findNodeDataByLocalId:]
-[BaseMsgContentViewController getCurContentSizeHeight]
仔细观察我们可以发现[BaseMsgContentViewController sendEmoticonMessageToolView:]这个方法有很大的几率就是我们要找的关键方法,从字面上看是发送表情信息的意思。然后继续编写脚本
使用frida加载该脚本,就可以打印出该方法的堆栈信息(去除了不必要的地址信息):
-[BaseMsgContentViewController SendEmoticonMesssageToolView:]
Backtrace:
-[MMInputToolView didSelectorSelfDefinedEmotcion:]
-[EmoticonBoardView onTapEmoticonWrap:atIndex:maxCountPerLine:fromSection:]
-[EmoticonBoardCrossCollectionController onEmoticonPageCellTapEmoticonWrap:atIndex:pid:maxCountPerLine:]
-[EmoticonBoardCrossCollectionEmoticonPageCell collectionView:didSelectItemAtIndexPath:]
-[UICollectionView _selectItemAtIndexPath:animated:scrollPosition:notifyDelegate:deselectPrevious:]
-[UICollectionView touchesEnded:withEvent:]
UIKitCore!forwardTouchMethod
UIKitCore!-[UIResponder touchesEnded:withEvent:]
UIKitCore!forwardTouchMethod
UIKitCore!-[UIResponder touchesEnded:withEvent:]
UIKitCore!forwardTouchMethod
UIKitCore!-[UIResponder touchesEnded:withEvent:]
UIKitCore!_UIGestureEnvironmentUpdate
UIKitCore!-[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:]
UIKitCore!-[UIGestureEnvironment _updateForEvent:window:]
UIKitCore!-[UIWindow sendEvent:]
经过动态分析之后,我们顺利找出点击骰子时的堆栈调用信息,接下来我们就运用静态分析的方式来分析这几个方法的源代码实现,使用hopper去加载已脱壳的二进制文件。
我们首先分析-[EmoticonBoardCrossCollectionController onEmoticonPageCellTapEmoticonWrap:atIndex:pid:maxCountPerLine:]
这个方法,因为它是didSelectItemAtIndexPath之后调用的。
通过分析该方法的伪代码,我们可以知道这个方法没做什么实事,只是做了一个代理转发,接着看-[EmoticonBoardView onTapEmoticonWrap:atIndex:maxCountPerLine:fromSection:]
这个方法的伪代码实现,由于这个方法的代码逻辑比较多,所以简单描述下这个方法做了什么事情,该方法判断选择的表情包是否为自定义表情,如果是就异步上传,最终调用-[MMInputToolView didSelectorSelfDefinedEmotcion:]方法,
我们知道了传入的参数是r27,具体r27是什么类型我们并不清楚,但从伪代码的上下文中可以发现r27有调用以下几个方法:
[r27 m_emojiInfo]
[r27 m_isAsyncUpload]
[r27 attachObject:r20 forKey:@"emoticonpageid"]
再通过hopper去寻找这些方法对应的类
由此可知传入的参数是CEmoticonWrap类,而骰子的结果应该是保存在这个类中,接着继续分析-[MMInputToolView didSelectorSelfDefinedEmotcion:]方法的伪代码实现:
这个方法的实现比较简单,也只是进行了一个代理转发操作,接着分析-[BaseMsgContentViewController SendEmoticonMesssageToolView:]
这个方法仍然是进行了代理转发,继续分析-[BaseMsgContentLogicController SendEmoticonMessage]
这里用到了m_uiGameType来判断,而m_uiGameType是arg2的属性,由上可知,arg2是CEmoticonWrap类,使用class-dump把该二进制文件所有的头文件导出来,然后找到CEmoticonWrap这个类的头文件:
我们打印一下arg2,编写脚本,打印该参数的所有属性值:
{
activityId = ;
aesKey = ;
attachedText = ;
attachedTextColor = ;
authkey = ;
designerId = ;
disableExtern = 0;
encryptUrl = ;
externMd5 = ;
externUrl = ;
lensId = ;
linkId = ;
md5 = "dice_emoticon_md5";
productId = "custom_emoticon_pid";
thumbUrl = ;
tpUrlString = ;
url =
}
其中m_uiGameType等于2,而且这个类的其他大部分属性包括图片url都还没有赋值,可以判断到目前为止,骰子的结果还没有出来。
如果m_uiGameType不等于0就调用-[GameController sendGameMessage:toUsr:]方法
然后又调用了-[GameController SetGameContentForMsgWrap]
分析到这里,终于找到突破口了!
红圈中的代码逻辑是,首先m_uiGameType 不等于0,如果m_uiGameType等于1就走if,其他就走else,从上文可知,m_uiGameType为2,根据这个逻辑,我们可以大胆猜测,m_uiGameType其实就是猜拳和扔骰子的类型,其中猜拳是1,扔骰子是2,经过验证,点击猜拳时m_uiGameType确实是1,那么我们可以得出,这段代码就是这两种游戏结果的计算公式了。
如果m_uiGameType = 1(猜拳):
r20 = 1 + random() - random() / 3 * 3 = 1 + random() % 3
如果m_uiGameType = 2(扔骰子):
r20 = 4 + random() - random() / 6 * 6 = 4 + random() % 6
观察公式计算可得,猜拳的取值范围是1-3,而扔骰子的取值范围是4-9,刚好对应着石头剪刀布和6个点数。
到这里为止,我们已经完成了猜拳和扔骰子的代码逻辑分析了。如果想要进一步的修改最后的结果,只要把唯一的变量random()的值hook掉就可以了,由于random()是C语言函数,它不像hook OC方法那样可以利用运行时机制提供的API来直接替换方法实现,所以Method Swizzling对它是无效的,需要使用Inline Hook才能达到目的,一般常用的有MSHookFunction、fishhook、HookZz等,需要注意的是MSHookFunction只能在越狱手机上生效,如果想要在非越狱手机上生效,推荐使用fishhook。
7.4. 案例流程梳理
案例选择了一个即时通讯app,确定需要逆向的功能-猜拳和扔骰子,然后从UI层面下手寻找切入点,找到当前控制器以及对应的控件类型,接着利用frida监听当前控制器的方法调用,拿到方法调用之后找到关键方法,打印该方法的堆栈调用,使用hopper静态分析堆栈调用中涉及的每一个方法的实现代码,再结合动态分析打印方法的参数值,找到关键代码,最终还原了该功能的实现代码。
以上就是一次完整的逆向过程,看似简单,实际操作过程中会遇到大量的坑,并且逆向领域比较小众,遇到问题可能难以在网上找到答案,所以整个过程非常消耗时间,但是逆向成功之后会收获很多,逆向工程与正向开发相反,它让你从另外一个角度去看待软件开发,窥探代码的本质。
8. 总结
本文分享了iOS逆向工程的大部分知识和技术,由于篇幅有限,开篇中的思维导图有某些知识点没有展开细讲,主要还是讲解逆向工程中的攻击手段和防御手段。
攻击手段有应用脱壳、运行时分析、静态分析、动态调试、修改可执行文件、注入动态库等。
防御手段有代码混淆、反调试、反注入、Hook检测、越狱检测、完整性检测等。
总之,iOS逆向工程中攻击的基本流程就是:先想尽一切办法寻找切入点(突破口),接着通过静态分析与动态调试相结合、跟踪和分析目标程序的核心代码、理解其设计思路等,从而还原出相似代码。
几乎所有的防御手段都是根据攻击手段来布置的,需要注意的是,即使做了这些防御手段,也不代表一定不会被破解,世界上没有十全十美的APP,只要黑客技术够强,总能发现一些漏洞,我们能做的只有做尽可能多的防御措施,迫使攻击者放弃破解的念头。
在文章的最后,通过逆向第三方App中的一个有趣的功能去把所有介绍的逆向技术真正地运用到实战当中。
9. 参考资料
a. 书籍:《iOS应用逆向与安全之道》
b. 视频:《iOS底层原理》
c. 参考链接:https://iosre.com/