[iOS 逆向 10] 实践一

  • 设备:iPhone 6 Plus with iOS 12.4
  • 目标:解除番茄ToDo 会员功能限制

1 脱壳

使用 dumpdecrypted 工具,把可执行文件和 framework 都脱壳。

2 导出头文件

使用 class-dump 工具将可执行文件的头文件导出备用。

3 分析界面

连上 Cycript,执行:
[[UIApp keyWindow] recursiveDescription].toString()
意思是递归打印当前 window 上的视图树。
[iOS 逆向 10] 实践一_第1张图片
也可以连接 Reveal 看,更直观:
[iOS 逆向 10] 实践一_第2张图片
图中开关按钮”计入 iOS 正念时间“点击后就告诉我们没开会员不能用,那么我们只需要让判断是否是会员的函数返回 true 就可以打开了。这是一个 UISwitch 控件,可以得到地址为 0x13dde0c20。

找到响应事件

在 Cycript 中执行:

cy# [#0x13dde0c20 allTargets]
[NSSet setWithArray:@[#""]]] 
 
cy# [#0x13dde0c20 valueForKey:@"targetActions"]
@[#""]

有 iOS 开发经验的话,第一句获取 allTargets 属性是没有疑问的,官方文档中明确写了;第二句利用 iOS Runtime KVC 特性,获取名为 targetActions 的属性,但是 targetActions 属性没在官方文档中提到过,是怎么知道的?在正向开发中,我们可以给一个按钮添加多个响应事件,由此可以想到肯定有一个数组保存了所有的响应关系,但是在官方文档中并没有发现。因此该属性极有可能是一个 private 属性,为了验证这一点,我们得先搞到系统动态库。

设备第一次连接到Xcode时,会自动提取系统库等数据到 ~/Library/Developer/Xcode/iOS DeviceSupport/ 目录下,进入某一版本的Symbols/System/Library/PrivateFrameworks/ 目录即可找到原始的动态库。
模拟器也是iOS系统,可以直接从电脑上找到模拟器的所有系统库,但由于模拟器是运行在x86_64的CPU上,所以都是x86_64架构的动态库,不过对于逆向分析来说并不重要。模拟器的动态库位置非常刁钻,如图:在这里插入图片描述
找到 UIKit 框架中的 UIKitCore,用 class-dump 提取头文件。UISwitch 的父类为 UIControl,查看 UIControl 类的头文件,发现果然有一个私有数组属性 _targetActions!利用 KVC 获得到对象的私有属性 targetActions 为 0x2823ab780,打印该属性的内容

cy# *(#0x2823ab780)
{
	isa:UIControlTargetAction,
	_target:#"",
	_action:@selector(onMindClicked:),
	_eventMask:64,
	_cancelled:false
}

isa 指向 UIControlTargetAction 类;也得到了 UISwitch 的响应事件:MyController 类的 onMindClicked: 方法。

分析代码

打开 Hopper,打开之前脱壳的可执行文件,搜索 onMindClick
[iOS 逆向 10] 实践一_第3张图片
在 IDA 中也能找到:
在这里插入图片描述

动态调试

前面介绍过连接 debugserver,连上后,执行
image list -o -f
可以看到所有 image 信息,分别是加载的序号,基地址,实际加载地址(模块的基地址+加载的起始地址)

第一个 image 是 App 的可执行文件,基地址是 0x0000000001084000,而 onMindClicked 函数偏移量为 0x0000000100060968,所以打断点:
breakpoint set -a 0x0000000001084000+0x0000000100060968
继续执行,我点击界面上的开关按钮,程序暂停,LLDB 输出:

Process 625 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
    frame #0: 0x00000001010e4968 TomatoTime`___lldb_unnamed_symbol1415$$TomatoTime
TomatoTime`___lldb_unnamed_symbol1415$$TomatoTime:
->  0x1010e4968 <+0>:  sub    sp, sp, #0xb0             ; =0xb0 
    0x1010e496c <+4>:  stp    x24, x23, [sp, #0x70]
    0x1010e4970 <+8>:  stp    x22, x21, [sp, #0x80]
    0x1010e4974 <+12>: stp    x20, x19, [sp, #0x90]
Target 0: (TomatoTime) stopped.

当前指令停在 sub sp, sp, #0xb0,和软件里反汇编的结果一致:
[iOS 逆向 10] 实践一_第4张图片
解读开始的几条指令,已知 OC 的消息机制固定了 X0 是 self 指针,X1 是 SEL 即 const char*。先获取 self->_mindSwitch 放到 X0,然后把 isOn 函数名放到 X1,然后发送消息。如果按钮状态之前是打开的,点击后是关闭,isOn 会返回 false,于是会 CBZ 跳转到 loc_100060B2C:
[iOS 逆向 10] 实践一_第5张图片
第一个 objc_msgSend 是调用了 MTA 类的一个静态方法,我百度了一下,MTA 是腾讯的一个移动数据分析的工具。这里是上传一些日常数据,暂时不用管。第二个 objc_msgSend 的参数中,X0 是GVUserDefaults 类对象,X1 是字符串 standardUserDefaults,可以联想到系统类 NSUserDefaults,但类名对应不上,于是查找之前提取出来的头文件,发现只是对 NSUserDefaults的封装,负责 App 内持久化存储的类。第三个 objc_msgSend 给 isOpenMind 属性设置值为 0,在头文件中也找到了相应的动态属性:@property(nonatomic) _Bool isOpenMind; // @dynamic isOpenMind;
到这里,我感觉只需要给 GVUserDefaults 类添加 isOpenMind 的 getter 实现,且始终返回 true,这样别的地方判断这个属性进而执行某些功能时,始终能够执行。但这样不完美,因为理想情况下是 hook 一个判断是否为会员的函数,这样当我想关闭功能时也可以正常关闭。那就只能看开关按钮的另一个分支,也是当前情况下将要实际执行的分支。中间一段不重要的代码跳过,找到核心的分支语句,如图

[iOS 逆向 10] 实践一_第6张图片
这段代码相当于判断:
[SystemUtil shouldSH] || [CommonUtil isActive]
二者任意一个正确都可以,那么分别查看这两个函数的代码:
在这里插入图片描述
[iOS 逆向 10] 实践一_第7张图片
可以看出,第一个是读内存中的值;第二个是从 GVUserDefaults 里面读 isActive 键的值,是从硬盘里读(我猜测内存中的值应该是初始化时读硬盘值做缓存),我在第一个函数内打断点:
breakpoint set -a 0x0000000001084000+0x0000000100024794
然后执行,我点击开关按钮后,程序停在:

TomatoTime`___lldb_unnamed_symbol429$$TomatoTime:
->  0x1010a4794 <+0>: adrp   x8, 1756
    0x1010a4798 <+4>: ldrb   w0, [x8, #0xc60]
    0x1010a479c <+8>: ret     

单步执行到 ret 指令时查看寄存器,register read x0 结果不出所料是 0,因为我没开会员。然后在这里修改寄存器,register write x0 1,修改后继续程序,此时按钮被成功打开了,没有弹窗提示没有权限,并且跳转到请求系统授权访问健康。我用上面同样的方法尝试了其他几个会员功能,发现 [SystemUtil shouldSH] 只能开启上面这个功能,而 [CommonUtil isActive] 可以打开任意功能。现在确定了,[CommonUtil isActive] 就是要 hook 的目标函数。
再回顾一下有没有疏忽的点。前面提到过,该 App 中有用 MTA 提交用户日志的代码,因此为了不被发现异常操作,我们还需要把 MTA 记录用户操作日志的方法 hook 掉。

Hook

使用 theos 创建一个项目,编写 Tweak。本项目一共要拦截3个方法,前两个是权限判断函数,直接返回true;第三个是记录用户日志函数,直接返回。要写的hook代码十分简单: [iOS 逆向 10] 实践一_第8张图片
然后依次执行编译、打包、安装命令,即可快速安装到越狱设备。如果要安装到非越狱设备,先用第三种动态库注入方式修改可执行文件,然后对 App 重签名后才能安装。会员权限使用成功的两个例子如图 ,分别是可以无限使用海报、自动写入 iOS 健康。

总结

  • dumpdecrypted 砸壳
  • class-dump 导出头文件
  • Cycript / Reveal 分析界面
  • 对照 IDA 反汇编出的代码进行 LLDB 调试,摸清逻辑
  • Theos 写插件,和普通 iOS 开发一样。

注意

有可能番茄 ToDo 作者看到本文就会修改逻辑,不能保证过一段时间后本文的代码还能用,但是思路肯定是能用的;

第十一章是另一个 app 的逆向实践,没意思,没写,直接看第12章

你可能感兴趣的:(iOS逆向,iOS逆向,逆向工程)