6.2 tweak的编写套路

在第5章的“tweak的编写套路”一节里,归纳总结了5个步骤,分别是寻找灵感、定位目标文件、定位目标函数、测试函数功能,以及解析函数参数。这些步骤没问题,但“定位目标函数”这个关键环节的水分太大——在class-dump的头文件里搜索自己感兴趣的关键词,可以称为“定位目标函数”吗?非也。

一般情况下,一个软件之所以能引起我们的兴趣,无非是2个元素:功能和数据。如果发现了自己感兴趣的功能,但class-dump的头文件里找不到可疑的关键词,怎么办?如果看到了自己感兴趣的数据,我们该怎么去寻找它的生成算法?对此,class-dump一点辙都没有。因此,通过class-dump及关键词搜索的方式只是“定位目标函数”中的一种情况,不能以偏概全。那么针对更普遍的情况,该怎么定位目标函数呢?

我们感兴趣的功能和数据,都是以软件中产生的某种现象为形式,直观地呈现在我们面前的,我们能看到、感受到。例如,图6-10所示的是邮件应用(以下简称Mail),右下角的那个书写图标代表了“编写邮件”功能;图6-11所示的是设置应用中的电话设置(以下简称MobilePhoneSettings),第一个cell中的内容代表了“本机号码”数据。功能是由函数提供的,数据是由函数生成的,也就是说,外在现象的内在本质,其实是函数。所以,“定位目标函数”实际上是如何从我们感兴趣的外在现象,定位到其内在函数的过程。

图6-10 Mail

图6-11 MobilePhoneSettings

面对这样的需求,class-dump明显已经不够用了。好在我们现在了解了Cycript、IDA、LLDB的基本用法,对ARM汇编也有了初步印象,有了它们的辅助,“定位目标函数”变得有规律可循了。iOS上最常见的是一个个App,我们对这种类型的文件也最熟悉,把它们作为初学阶段的练习对象再合适不过了。接下来,就以App为目标,用ARM汇编级别的逆向工程完善“定位目标函数”环节,强化tweak的编写套路。

6.2.1 从现象切入App,找出UI函数

对于App来说,我们感兴趣的现象往往体现在UI上,UI展示了函数的执行过程和结果。函数和UI之间的关联非常紧密,如果能拿到感兴趣的UI对象,就可以找到它所对应的函数,我们称该函数为UI函数。这个过程,一般是利用Cycript,结合UIView中的神奇私有函数recursiveDescription和UIResponder中的nextResponder来实现的。下面先以Mail为例讲解过程,然后把总结出来的方法用在MobilePhoneSettings上加深印象。这部分内容是在iPhone 5,iOS 8.1中完成的。

1.用Cycript注入Mail

先用dumpdecrypted小节中提及的技巧,定位Mail的进程名并注入,命令如下:


FunMaker-5:~ root# ps -e | grep /Applications
  363 ??         0:06.94 /Applications/MobileMail.app/MobileMail
  596 ??         0:01.50 /Applications/MessagesNotificationViewService.app/MessagesNotificationViewService
  623 ??         0:08.50 /Applications/InCallService.app/InCallService
  713 ttys000    0:00.01 grep /Applications
FunMaker-5:~ root# cycript -p MobileMail

2.查看当前界面的UI层次结构,定位“编写邮件”按钮

UIView中的私有函数recursiveDescription可以返回这个view的UI层次结构。一般来说,当前界面是由至少一个UIWindow构成的,而UIWindow继承自UIView,因此可以利用这个私有函数来查看当前界面的UI层次结构。它的用法如下:


cy# ?expand
expand == true

首先执行Cycript的?expand命令开启expand功能,Cycript会把格式符号翻译成相应的格式,如“\n”会被翻译成一个换行,让输出的可读性更高。接着输入如下命令:


cy# [[UIApp keyWindow] recursiveDescription]

UIApp是[UIApplication sharedApplication]的简写,两者等价。调用上面的方法即可打印keyWindow的视图结构,输出类似下面的信息:


@"<UIWindow: 0x14587a70; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x147166b0>; layer = <UIWindowLayer: 0x14587e30>>
   | <UIView: 0x146e6180; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x146e98d0>; layer = <CALayer: 0x146e61f0>>
   |    | <UIView: 0x146e5f60; frame = (0 0; 320 568); layer = <CALayer: 0x1460ec40>>
   |    |    | <_MFActorItemView: 0x14506a30; frame = (0 0; 320 568); layer = <CALayer: 0x14506c10>>
   |    |    |    | <UIView: 0x145074b0; frame = (-0.5 -0.5; 321 569); alpha = 0; layer = <CALayer: 0x14507520>>
   |    |    |    | <_MFActorSnapshotView: 0x14506f70; baseClass = UISnapshotView; frame = (0 0; 320 568); clipsToBounds = YES; hidden = YES; layer = <CALayer: 0x145071c0>>
……
   |    | <MFTiltedTabView: 0x146e1af0; frame = (0 0; 320 568); userInteractionEnabled = NO; gestureRecognizers = <NSArray: 0x146f2dd0>; layer = <CALayer: 0x146e1d50>>
   |    |    | <UIScrollView: 0x146bfa90; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x146e1e90>; layer = <CALayer: 0x146c8740>; contentOffset: {0, 0}; contentSize: {320, 77.5}>
   |    |    | <_TabGradientView: 0x146e7010; frame = (-320 -508; 960 568); alpha = 0; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x146e7d80>>
   |    |    | <UIView: 0x146e29c0; frame = (-10000 568; 10320 10000); layer = <CALayer: 0x146e2a30>>"

keyWindow的每个subview及二级subview的description会被完整展示在<……>里,包括每个view对象在内存中的地址,以及它的坐标、尺寸等基本信息。其中,缩进的多少体现了视图间的关系,同一缩进量的视图是平级的,如最下面的UIScrollView、_TabGradientView及UIView;缩进少的视图是缩进多的视图的superview,如UIScrollView、_TabGradientView和UIView都是MFTiltedTabView的subview。通过Cycript的“#”操作符,就可以拿到这个window上的任意view,如:


cy# tabView = #0x146e1af0
#"<MFTiltedTabView: 0x146e1af0; frame = (0 0; 320 568); userInteractionEnabled = NO; gestureRecognizers = <NSArray: 0x146f2dd0>; layer = <CALayer: 0x146e1d50>>"

当然,也可以通过UIApplication和UIView的其他方法,获取我们感兴趣的其他view,如:


cy# [UIApp windows]
@[#"<UIWindow: 0x14587a70; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x147166b0>; layer = <UIWindowLayer: 0x14587e30>>",#"<UITextEffectsWindow: 0x15850570; frame = (0 0; 320 568); opaque = NO; gestureRecognizers = <NSArray: 0x147503e0>; layer = <UIWindowLayer: 0x1474ff10>>"]

上面的代码可以拿到这个App的所有window;


cy# [#0x146e1af0 subviews]
@[#"<UIScrollView: 0x146bfa90; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x146e1e90>; layer = <CALayer: 0x146c8740>; contentOffset: {0, 0}; contentSize: {320, 77.5}>",#"<_TabGradientView: 0x146e7010; frame = (-320 -508; 960 568); alpha = 0; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x146e7d80>>",#"<UIView: 0x146e29c0; frame = (-10000 568; 10320 10000); layer = <CALayer: 0x146e2a30>>"]
cy# [#0x146e29c0 superview]
#"<MFTiltedTabView: 0x146e1af0; frame = (0 0; 320 568); userInteractionEnabled = NO; gestureRecognizers = <NSArray: 0x146f2dd0>; layer = <CALayer: 0x146e1d50>>"

上面的代码可以拿到subview和superview。总之,综合利用这几个函数,就可以拿到UI上的任意view,为下一步操作奠定基础。

要定位“编写邮件”按钮,就要寻找与这个按钮相关的控件。对此,一般采用的方法是排查法,对于形如<UIView:viewAddress;…>的view来说,对其逐个调用[#viewAddress setHidden:YES]函数,UI上消失的那个控件就可以跟它对应起来。当然,一些小技巧可以加快排查的速度——因为这个按钮的左边是上下两排字,所以可以猜测,这个按钮跟两排字是共用一个superview的,如果找到这个superview,那么只排查这个superview的subview就好了,减少了工作量。因为文字一般是会出现在description里的,所以可在recursiveDescription里搜索“3 Unsent Messages”,如下:


   |    |    |    |    |    |    |    | <MailStatusUpdateView: 0x146e6060; frame = (0 0; 182 44); opaque = NO; autoresize = W+H; layer = <CALayer: 0x146c8840>>
   |    |    |    |    |    |    |    |    | <UILabel: 0x14609610; frame = (40 21.5; 102 13.5); text = '3 Unsent Messages'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x146097f0>>

从而获取到它的superview,即MailStatusUpdateView。如果按钮是MailStatusUpdateView的一个subview,那么通过调用setHidden:函数隐藏MailStatusUpdateView,按钮也会被隐藏。下面试试看:


cy# [#0x146e6060 setHidden:YES]

执行之后,发现两排字被隐藏了,而按钮没有被隐藏,如图6-12所示。

图6-12 两排字被隐藏

这说明MailStatusUpdateView的级别低于或者等于按钮所在的view,对吧?因此,接下来要做的就是排查MailStatus UpdateView的superview。从recursiveDescription可知,它的superview是MailStatusBarView,如下:


   |    |    |    |    |    |    | <MailStatusBarView: 0x146c4110; frame = (69 0; 182 44); opaque = NO; autoresize = BM; layer = <CALayer: 0x146f9f90>>
   |    |    |    |    |    |    |    | <MailStatusUpdateView: 0x146e6060; frame = (0 0; 182 44); opaque = NO; autoresize = W+H; layer = <CALayer: 0x146c8840>>

试着隐藏它,看看按钮受不受影响,如下:


cy# [#0x146e6060 setHidden:NO]
cy# [#0x146c4110 setHidden:YES]

效果跟刚才一样,两排字被隐藏,按钮还是没有被隐藏,说明MailStatusBarView的级别仍然不够高,继续找它的superview,即UIToolBar,如下:


   |    |    |    |    |    | <UIToolbar: 0x146f62a0; frame = (0 524; 320 44); opaque = NO; autoresize = W+TM; layer = <CALayer: 0x146f6420>>
   |    |    |    |    |    |    | <_UIToolbarBackground: 0x14607ed0; frame = (0 0; 320 44); autoresize = W; userInteractionEnabled = NO; layer = <CALayer: 0x14607d40>>
   |    |    |    |    |    |    |    | <_UIBackdropView: 0x15829590; frame = (0 0; 320 44); opaque = NO; autoresize = W+H; userInteractionEnabled = NO; layer = <_UIBackdropViewLayer: 0x158297e0>>
   |    |    |    |    |    |    |    |    | <_UIBackdropEffectView: 0x14509020; frame = (0 0; 320 44); clipsToBounds = YES; opaque = NO; autoresize = W+H; userInteractionEnabled = NO; layer = <CABackdropLayer: 0x145a68d0>>
   |    |    |    |    |    |    |    |    | <UIView: 0x147335c0; frame = (0 0; 320 44); hidden = YES; opaque = NO; autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x145f3ab0>>
   |    |    |    |    |    |    | <UIImageView: 0x14725730; frame = (0 -0.5; 320 0.5); autoresize = W+BM; userInteractionEnabled = NO; layer = <CALayer: 0x1472be40>>
   |    |    |    |    |    |    | <MailStatusBarView: 0x146c4110; frame = (69 0; 182 44); opaque = NO; autoresize = BM; layer = <CALayer: 0x146f9f90>>

模仿之前的操作,隐藏UIToolBar,命令如下:


cy# [#0x146c4110 setHidden:NO]
cy# [#0x146f62a0 setHidden:YES]

效果如图6-13所示。

图6-13 UIToolBar被隐藏

此时,按钮被隐藏了,说明按钮是这个UIToolBar的一个subview。在这个UIToolBar的subview里面寻找带有“button”字样的view,很容易就定位到了UIToolbarButton,如下:


   |    |    |    |    |    |    | <MailStatusBarView: 0x146c4110; frame = (69 0; 182 44); opaque = NO; autoresize = BM; layer = <CALayer: 0x146f9f90>>
   |    |    |    |    |    |    |    | <MailStatusUpdateView: 0x146e6060; frame = (0 0; 182 44); opaque = NO; autoresize = W+H; layer = <CALayer: 0x146c8840>>
   |    |    |    |    |    |    |    |    | <UILabel: 0x14609610; frame = (40 21.5; 102 13.5); text = '3 Unsent Messages'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x146097f0>>
   |    |    |    |    |    |    |    |    | <UILabel: 0x145f3020; frame = (43 8; 96.5 13.5); text = 'Updated Just Now'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x145f2e50>>
   |    |    |    |    |    |    | <UIToolbarButton: 0x14798410; frame = (285 0; 23 44); opaque = NO; gestureRecognizers = <NSArray: 0x14799510>; layer = <CALayer: 0x14798510>>

下面看看它是不是“编写邮件”按钮,命令如下:


cy# [#0x146f62a0 setHidden:NO]
cy# [#0x14798410 setHidden:YES]

按钮被成功隐藏,如图6-14所示。

图6-14 按钮被隐藏

至此,我们成功定位到了“编写邮件”按钮,它的description是<UIToolbarButton:0x14798410;frame=(2850;2344);opaque=NO;gestureRecognizers=<NSArray:0x14799510>;layer=<CALayer:0x14798510>>。接下来要找出它的UI函数。

3.找出“编写邮件”按钮的UI函数

按钮的UI函数,就是点击它之后的响应函数。给UIView对象加上响应函数,一般是通过[UIControl addTarget:action:forControlEvents:]实现的(笔者还没有碰到过例外);而UIControl提供了一个actionsForTarget:forControlEvent:方法,来获得这个UIControl的响应函数。基于这个条件,只要第2步里定位到的view是UIControl的子类(笔者也还没有碰到过例外),就可以通过这种方式找到它的响应函数。具体到书中的例子,是这样操作的:


cy# button = #0x14798410
#"<UIToolbarButton: 0x14798410; frame = (285 0; 23 44); hidden = YES; opaque = NO; gestureRecognizers = <NSArray: 0x14799510>; layer = <CALayer: 0x14798510>>"
cy# [button allTargets]
[NSSet setWithArray:@[#"<ComposeButtonItem: 0x14609d00>"]]]
cy# [button allControlEvents]
64
cy# [button actionsForTarget:#0x14609d00 forControlEvent:64]
@["_sendAction:withEvent:"]

因此,按下“编写邮件”按钮,Mail会调用[ComposeButtonItem_sendAction:withEvent:],我们成功找到了它的响应函数。用Cycript注入,定位UI控件,找出UI函数,就这么简单。如果你还不理解,下面会用类似的套路分析MobilePhoneSettings,请注意总结。

4.用Cycript注入MobilePhoneSettings

下面的操作大家应该都很熟悉了:


FunMaker-5:~ root# ps -e | grep /Applications
  596 ??         0:01.50 /Applications/MessagesNotificationViewService.app/MessagesNotificationViewService
  623 ??         0:08.55 /Applications/InCallService.app/InCallService
  748 ??         0:01.36 /Applications/MobileMail.app/MobileMail
  750 ??         0:01.82 /Applications/Preferences.app/Preferences
  755 ttys000    0:00.01 grep /ApplicationsFunMaker-5:~ root# cycript -p Preferences

注意,桌面上Settings的应用名叫Preferences,下面会频繁出现,请大家留意。

5.查看当前界面的UI层次结构,定位第一个cell

打印出当前界面的UI层次结构如下:


cy# ?expand
expand == true
cy# [[UIApp keyWindow] recursiveDescription]
@"<UIWindow: 0x17d62e00; frame = (0 0; 320 568); autoresize = H; gestureRecognizers = <NSArray: 0x17d589b0>; layer = <UIWindowLayer: 0x17d21c60>>
   | <UILayoutContainerView: 0x17d86620; frame = (0 0; 320 
568); autoresize = W+H; layer = <CALayer: 0x17d863b0>>
   |    | <UIView: 0x17ef2430; frame = (0 0; 320 0); layer = <CALayer: 0x17ef24a0>>
   |    | <UILayoutContainerView: 0x17d7eb80; frame = (0 0; 320 568); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x17eb6400>; layer = <CALayer: 0x17d7ed60>>
……
   |    |    |    |    |    |    |    |    |    |    | <PSTableCell: 0x17f92890; baseClass = UITableViewCell; frame = (0 35; 320 44); text = 'My Number'; autoresize = W; tag = 2; layer = <CALayer: 0x17f92a60>>
   |    |    |    |    |    |    |    |    |    |    |    | <UITableViewCellContentView: 0x17f92ad0; frame = (0 0; 287 43.5); gestureRecognizers = <NSArray: 0x17f92ce0>; layer = <CALayer: 0x17f92b40>>
   |    |    |    |    |    |    |    |    |    |    |    |   | |<UITableViewLabel: 0x17f92d30; frame = (15 12; 90 20.5); text = 'My Number'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x17f92df0>>
   |    |    |    |    |    |    |    |    |    |    |   |   | <UITableViewLabel: 0x17f93060; frame = (132.5 12; 152.5 20.5); text = '+86PhoneNumber'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x17f93120>>

很容易就可以定位到显示“+86PhoneNumber”的地方,而且几乎不需要测试,就可以知道它所在的cell是PSTableCell。尝试隐藏这个cell,验证一下猜测,命令如下:


cy# [#0x17f92890 setHidden:YES]

此时,MobilePhoneSettings变成了如图6-15所示的这个样子。

所以第一个cell的description是<PSTableCell:0x17f92890;baseClass=UITableView-Cell;frame=(035;32044);text='My Number';autoresize=W;tag=2;layer=<CALayer:0x17f92a60>>。与刚才“编写邮件”按钮不同的是,这次的目标不是这个cell的响应函数(功能),而是它上面显示的内容(数据),actionsForTarget:forControlEvent:不再适用。面对这种情况,该怎么办呢?

在绝大多数情况下,我们感兴趣的数据不会是一个常量。如果这个数据永远显示1,笔者相信你看都不会多看它一眼。当目标是一个变量时,则要思考一个问题:这个变量来自哪里?

图6-15 隐藏第一个cell

任何变量都不是凭空出现的,它是由数据源,经过一定的算法生成的,而我们感兴趣的一般是这个算法,也就是数据源生成变量的这个过程,这个过程往往是由一个或多个函数串联而成的,它们形成了一个调用链,类似于下面的伪代码:


id dataSource = ?; // head
id a = function(dataSource);
id b = function(a);
id c = function(b);
…
id z = function(y);
NSString *myPhoneNumber = function(z); // tail

变量是已知的,也就是说,我们位于链条的尾部。逆向工程,自然就能够让我们从尾部顺着链条回溯到头部,找出这个调用链上的一个个函数,从而还原一整套算法。总的来说,还原变量的生成算法,就要在回溯的过程中记录其数据源(的数据源的数据源……,以下简称N重数据源)和函数的调用轨迹,当它的N重数据源是一个你可以决定的数据时(比如本例的数据源是——SIM卡),从N重数据源到变量之间这段链条上的函数,就是变量的生成算法。有点不知所云?看完下面的内容,你就明白了。

6.找出第一个cell的UI函数

按照MVC设计标准(如图6-16所示),M代表model,即数据源,是未知的;V代表view,即第一个cell,是已知的;C代表controller,是未知的。M和V之间没有直接联系,而C既可以访问M又可以访问V,是三者的交流中枢。如果能够利用已知的V,获得C,不就可以访问M,找到自己的数据源了吗?这种方式从逻辑上是说得通的,在实际操作中可行吗?

图6-16 MVC设计标准(来自Stanford CS 193P)

从笔者目前的职业经历来看,从V得到C,是100%可行的,用到的关键函数,就是在笔者心目中与recursiveDescription具有同等地位的公开函数[UIResponder nextResponder],它的描述是这样的:

“The UIResponder class does not store or set the next responder automatically,instead returning nil by default.Subclasses must override this method to set the next responder.UIView implements this method by returning the UIViewController object that manages it(if it has one)or its superview(if it doesn’t);UIViewController implements the method by returning its view’s superview;UIWindow returns the application object,and UIApplication returns nil.”

也就是说,对于一个V,调用nextResponder,要么返回它对应的C,要么返回它的superview。因为MVC三者缺一不可,所以C是一定存在的,也就是说,一定有一个V的nextResponder是C;又因为通过recursiveDescription可以拿到所有的V,所以从已知的V获得C是可行的,进一步就可以访问M了。

因此,我们现在的目标是拿到cell的C,操作起来很简单——从cell处开始调用nextResponder,一直到返回一个C为止,命令如下:


cy# [#0x17f92890 nextResponder]
#"<UITableViewWrapperView: 0x17eb4fc0; frame = (0 0; 320 504); gestureRecognizers = <NSArray: 0x17ee5230>; layer = <CALayer: 0x17ee5170>; contentOffset: {0, 0}; contentSize: {320, 504}>"
cy# [#0x17eb4fc0 nextResponder]
#"<UITableView: 0x16c69e00; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x17f4ace0>; layer = <CALayer: 0x17f4ac20>; contentOffset: {0, -64}; contentSize: {320, 717.5}>"
cy# [#0x16c69e00 nextResponder]
#"<UIView: 0x17ebf2b0; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x17ebf320>>"
cy# [#0x17ebf2b0 nextResponder]
#"<PhoneSettingsController 0x17f411e0: navItem <UINavigationItem: 0x17dae890>, view <UITableView: 0x16c69e00; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x17f4ace0>; layer = <CALayer: 0x17f4ac20>; contentOffset: {0, -64}; contentSize: {320, 717.5}>>"

拿到了C,就可以从C所在的头文件出发,踏上寻找M的旅途了。对于本例的情况,首先要定位PhoneSettingsController所在的目标文件,我们不确定它是来自Preferences.app本身,还是来自一个PreferenceBundle。对于这种情况,简单验证一下就好了,命令如下:


FunMaker-5:~ root# grep -r PhoneSettingsController /Applications/Preferences.app/ 
FunMaker-5:~ root# grep -r PhoneSettingsController /System/Library/
Binary file /System/Library/Caches/com.apple.dyld/dyld_shared_cache_armv7s matches
grep: /System/Library/Caches/com.apple.dyld/enable-dylibs-to-override-cache: No such file or directory
grep: /System/Library/Frameworks/CoreGraphics.framework/Resources/libCGCorePDF.dylib: No such file or directory
grep: /System/Library/Frameworks/CoreGraphics.framework/Resources/libCMSBuiltin.dylib: No such file or directory
grep: /System/Library/Frameworks/CoreGraphics.framework/Resources/libCMaps.dylib: No such file or directory
grep: /System/Library/Frameworks/System.framework/System: No such file or directory
Binary file /System/Library/PreferenceBundles/MobilePhoneSettings.bundle/Info.plist matches

看来这个类来自MobilePhoneSettings.bundle。下面class-dump它的二进制文件,然后打开PhoneSettingsController.h,命令如下:


@interface PhoneSettingsController :PhoneSettingsListController <TPSetPINView-ControllerDelegate>
……
- (id)myNumber:(id)arg1;
- (void)setMyNumber:(id)arg1 specifier:(id)arg2;
……
- (id)tableView:(id)arg1 cellForRowAtIndexPath:(id)arg2;
@end

从上面的代码可以看到,前两个方法明显跟本机号码相关,而第3个方法是用来初始化所有cell的数据源函数,每个cell显示的数据一般也都与这个方法有着千丝万缕的联系。从这3个方法入手,一定可以找到第一个cell的数据源。我们用LLDB在[PhoneSettingsController tableView:cellForRowAtIndexPath:]的末尾下个断点,打印出返回值,也就是cell,看看有没有本机号码的踪迹。下面用debugserver附加Preferences,然后用LLDB连接,查看MobilePhoneSettings的ASLR偏移,如下:


(lldb) image list -o -f
[0] 0x00078000 /private/var/db/stash/_.29LMeZ/Applications/Preferences.app/Preferences(0x000000000007c000)
[1] 0x00231000 /Library/MobileSubstrate/MobileSubstrate.dylib(0x0000000000231000)
[2] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/PrivateFrameworks/BulletinBoard.framework/BulletinBoard
[3] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
……
[322] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/PreferenceBundles/MobilePhoneSettings.bundle/MobilePhoneSettings
……

可以看到,MobilePhoneSettings的ASLR偏移是0x6db3000。然后在IDA中看看[Phone-SettingsController tableView:cellForRowAtIndexPath:]末尾指令的地址,如图6-17所示。

图6-17 [PhoneSettingsController tableView:cellForRowAtIndexPath:]

因为返回值存放在R0中,所以把断点下在“ADD SP,SP,#8”上,然后返回上一级目录,再重新进入MobilePhoneSettings,待断点触发后打印R0,其中应该存放了已经初始化的cell,如下:


(lldb) br s -a 0x2c965c2c
Breakpoint 2: where = MobilePhoneSettings`-[PhoneSettingsController tableView:cellForRowAtIndexPath:] + 236, address = 0x2c965c2c
Process 115525 stopped
* thread #1: tid = 0x1c345, 0x2c965c2c MobilePhoneSettings`-[PhoneSettingsController tableView:cellForRowAtIndexPath:] + 236, queue = 'com.apple.main-thread, stop reason = breakpoint 2.1
    frame #0: 0x2c965c2c MobilePhoneSettings`-[PhoneSettingsController tableView:cellForRowAtIndexPath:] + 236
MobilePhoneSettings`-[PhoneSettingsController tableView:cellForRowAtIndexPath:] + 236:
-> 0x2c965c2c:  add    sp, #8
   0x2c965c2e:  pop    {r4, r5, r6, r7, pc}
MobilePhoneSettings`-[PhoneSettingsController applicationWillSuspend]:
   0x2c965c30:  push   {r7, lr}
   0x2c965c32:  mov    r7, sp
(lldb) po $r0
<PSTableCell: 0x15f41440; baseClass = UITableViewCell; frame = (0 0; 320 44); text = 'My Number'; tag = 2; layer = <CALayer: 0x15f4c930>>
(lldb) po [$r0 subviews]
<__NSArrayM 0x17060e50>(
<UITableViewCellContentView: 0x15ed0660; frame = (0 0; 320 44); gestureRecognizers = <NSArray: 0x15f491e0>; layer = <CALayer: 0x15ed06d0>>,
<UIButton: 0x15f26f50; frame = (302 16; 8 13); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x15f27050>>
)
(lldb) po [$r0 detailTextLabel]
<UITableViewLabel: 0x15eb3480; frame = (0 0; 0 0); text = '+86PhoneNumber'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x15eb3540>>

可以看到,第一个cell的UI函数确实是[PhoneSettingsController tableView:cellForRow-AtIndexPath:],我们成功完成了本节的任务。我们有信心,通过PhoneSettingsController类一定可以拿到访问M的方法,在tableView:cellForRowAtIndexPath:内部也一定有M的线索,在下一小节中就会见证。

注意,游戏一般不是采用UIKit来构建UI的,recursiveDescription和nextResponder不适用于游戏。在逆向工程初期,不建议把游戏作为练习目标。如果你在熟悉了本书的内容后想要逆向游戏,可以来http://bbs.iosre.com 参与讨论。

6.2.2 以UI函数为起点,寻找目标函数

拿到UI函数,预示着首战告捷。但是,UI函数与UI是密切相关的,也就是说,要想调用[ComposeButtonItem_sendAction:withEvent:]来编写邮件,或者调用[PhoneSettingsController tableView:cellForRowAtIndexPath:]来获取本机号码,会关联很多UI操作,比如刷新界面、尺寸布局等,有一种牵一发而动全身的感觉。在绝大多数情况下,我们不想搞得这么大张旗鼓,希望只是安静地牵一发,而不会动全身。面对这种挑战,我们该何去何从呢?

作为工程师,一定要具备基本的代码常识:最底层的函数通常是直接用汇编代码编写的,我们还接触不到;而这层以上的函数全都是嵌套调用的。UI函数也不例外——它嵌套调用了我们的目标函数。用伪代码表示如下:


drink GetRegular(water arg)
{
      Functions();
      return MakeRegular(arg);
}
drink GetDiet(void)
{
      Functions();
      return MakeDiet();
}
drink GetZero(void)
{
      Functions();
      return MakeZero();
}
drink GetCoke(sugar arg1, water arg2, color arg3)
{
      if (arg1 > 0 && arg1 < 3) return GetDiet();
      else if (arg1 == 0) return GetZero();
      return GetRegular(arg2);
}
drink Get7Up(void)
{
      Functions();
      return Make7Up();
}
drink GetMirinda(void)
{
      Functions();
      return MakeMirinda();
}
drink GetPepsi(sugar arg1, water arg2, color arg3)
{
      if (arg3 == clear) Get7Up();
      else if (arg3 == orange) GetMirinda();
      return GetRegular(arg2);
}
array GetDrinks(sugar arg1, color arg2) // UIFunction
{
      drink coke = GetCoke(arg1, 100, arg3);
      drink pepsi = GetPepsi(arg1, 105, arg3);
      return ArrayWithComponents(coke, pepsi)
}

我们不想每次都喝两种饮料(UI函数),如果只想喝七喜(数据),就要找到Get7Up(生成数据的目标函数);如果想知道零度是怎么制作的(功能),就要找到MakeZero(提供功能的目标函数)。嵌套调用的函数之间其实也是一个链条,只要已知链条上的一个环节,就可用通过逆向工程还原整个链条。这个过程主要用到的工具是IDA和LLDB,我们接着上面2个App例子,看看如何以[ComposeButtonItem_sendAction:withEvent:]和[PhoneSettingsController tableView:cellForRowAtIndexPath:]这2个UI函数为线索,寻找“编写邮件”和“获取本机号码”的目标函数。

1.寻找“编写邮件”的目标函数

把MobileMail丢进IDA开始分析,然后在Functions window里搜索[ComposeButtonItem_sendAction:with Event:],如图6-18所示。

图6-18 找不到[ComposeButtonItem_sendAction:withEvent:]

说好的[ComposeButtonItem_sendAction:withEvent:]呢?既然ComposeButtonItem没有实现这个方法,那么去它的父类里看看。打开ComposeButtonItem.h,看看它继承自哪个类,如下:


@interface ComposeButtonItem : LongPressableButtonItem
+(id)composeButtonItem;
@end

然后打开LongPressableButtonItem.h,看看它有没有实现_sendAction:withEvent:方法,如下:


@interface LongPressableButtonItem : UIBarButtonItem
{
    id _longPressTarget;
    SEL _longPressAction;
}
- (void)_attachGestureRecognizerToView:(id)arg1;
- (id)createViewForNavigationItem:(id)arg1;
- (id)createViewForToolbar:(id)arg1;
- (void)longPressGestureRecognized:(id)arg1;
- (void)setLongPressTarget:(id)arg1 action:(SEL)arg2;
@end

它也没有实现这个方法,那就再到它的父类里去看看。打开UIBarButtonItem.h,如下:


@interface UIBarButtonItem : UIBarItem <NSCoding>
……
- (void)_sendAction:(id)arg1 withEvent:(id)arg2;
……
@end

原来这个函数是在UIBarButtonItem类中实现的,那么把UIKit的二进制文件拖到IDA里开始分析。UIKit二进制文件较大,IDA分析耗时较长,在等待的间隙,来http://bbs.iosre.com 跟大家聊聊吧!

UIKit初始分析结束后,定位到[UIBarButtonItem_sendAction:withEvent:],如图6-19所示。

图6-19 [UIBarButtonItem_sendAction:withEvent:]

第一个调用的函数是objc_msgSend。官方文档的注释是这样的:

“When it encounters a method call,the compiler generates a call to one of the functions objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,or objc_msgSendSuper_stret.Messages sent to an object’s superclass(using the super keyword)are sent using objc_msgSendSuper;other messages are sent using objc_msgSend.Methods that have data structures as return values are sent using objc_msgSendSuper_stret and objc_msgSend_stret.”

依据第5章中“对象”、“方法”和“实现”的关系来进一步探索,[receiver message]在编译后变成了objc_msgSend(receiver,@selector(message));当方法有参数时,则由[receiver message:arg1 foo:arg2 bar:arg3]变成objc_msgSend(receiver,@selector(message),arg1,arg2,arg3),依此类推。因此,第一个objc_msgSend其实是执行了一个Objective-C方法。那么它具体执行的是什么方法呢?调用者是谁,参数又是什么呢?

还记得我们的金句吗?

“函数的前4个参数存放在R0到R3中,其他参数存放在栈中;返回值放在R0中。”

依照金句来看,objc_msgSend调用时的参数应该是objc_msgSend(R0,R1,R2,R3,*SP,*(SP+sizeOfLastArg),...)的形式,还原成等价的Objective-C方法,就是[R0 R1:R2 foo:R3 bar:*SP baz:*(SP+sizeOfLastArg)qux:...]。把这个套路运用在第一个objc_msgSend上,想知道它的等价Objective-C方法,就要看在“BLX.W_objc_msgSend”之前,R0~R3及SP都是什么。这是个从下往上倒推的分析过程,是名副其实的逆向工程。一起来看一下。

在“BLX.W_objc_msgSend”之前,R0最近的一次赋值来自“MOV R0,R10”,即R0来自R10;R10的最近一次赋值来自“MOV R10,R0”,即R10来自R0。在“MOV R10,R0”之前,R0没有被赋值就直接取值了;这显然是不合逻辑的,汇编语言不可能出现这么严重的设计漏洞。那么R0肯定还是在某个地方被赋值了——问题来了,“某个地方”是哪个地方呢?

既然在[UIBarButtonItem_sendAction:withEvent:]的内部,R0没有被赋值,那么唯一的可能就是它在[UIBarButtonItem_sendAction:withEvent:]的调用者中被赋值。[UIBarButtonItem_sendAction:withEvent:]在编译后变成了objc_msgSend(UIBarButtonItem,@selector(_sendAction:withEvent:),action,event),四个参数分别放在了R0~R3中。因此,[UIBarButtonItem_sendAction:withEvent:]得到调用时,R0的值就是UIBarButtonItem,进而调用“MOV R10,R0”时的R0也是UIBarButtonItem,即调用“BLX.W_objc_msgSend”时的R0是UIBarButtonItem。有点迷糊?对照着图6-20再想一想就明白了。

同理,在“BLX.W_objc_msgSend”之前,R1最近的一次赋值来自“MOV R1,R4”,即R1来自R4;R4最近的一次赋值来自“LDR R4,[R0]”,R4来自*R0,即IDA已经标出的“action”。R1的演变过程如图6-21所示。

图6-20 R0的演变过程

图6-21 R1的演变过程

因此,第一个objc_msgSend还原成Objective-C方法后,是[self action],返回值存放在接下来的R0中。没问题吧?接着进程判断[self action]是否为0,如果是0,则不执行任何操作;否则到达图6-22。

图6-22 [UIBarButtonItem_sendAction:withEvent:]

又是4个objc_msgSend,从上到下逐个分析:

第一个objc_msgSend的R0来自“LDR R0,[R2]”,IDA已经分析出[R2]是UIApplication类;R1来自“LDR R1,[R0]”,即“sharedApplication”,因此第一个objc_msgSend还原成Objective-C方法就是[UIApplication sharedApplication],且返回值放入R0。

第二个objc_msgSend的R0来自“MOV R0,R10”,即R10;在图6-20中,我们知道R10的值是UIBarButtonItem;R1来自“MOV R1,R4”,即R4;在图6-21中,R4的值是“action”。因此第二个objc_msgSend还原成Objective-C方法就是[UIBarButtonItem action],并将返回值存放在R0中。

第三个objc_msgSend的R0仍来自“MOV R0,R10”,即UIBarButtonItem;R1来自“LDR R1,[R0]”,即“target”。因此第三个objc_msgSend还原成Objective-C方法就是[UIBarButtonItem target],并将返回值保存在R0中。

第四个objc_msgSend的R0来自“MOV R0,R5”,即R5;R5来自第一个objc_msgSend下方的“MOV R5,R0”,即R0;R0是什么呢?因为第一个objc_msgSend执行之后,把返回值存放在了R0里,所以这个R0就是[UIApplication sharedApplication]的返回值,它是objc_msgSend的第一个参数。R1来自“LDR R1,[R0]”,即“sendAction:to:from:forEvent:”,这是一个有4个参数的方法,加上objc_msgSend的前2个参数,一共6个参数,因此R0~R3寄存器不够用了,有2个参数要放在栈上。R2来自“MOV R2,R4”,即R4;R4来自第二个objc_msgSend下方的“MOV R4,R0”,即R0;R0来自第二个objc_msgSend执行之后的返回值,即[UIBarButtonItem action],这是第3个参数。R3来自第三个objc_msgSend下方的“MOV R3,R0”,即R0;R0来自第三个objc_msgSend执行之后的返回值,[UIBarButtonItem target],这是第4个参数。接下来的2个参数来自栈,而在第四个objc_msgSend以前,栈的最近一次改动来自“STRD.W R10,R11,[SP]”,即先后把R10和R11入栈,因此接下来的2个参数就是R10和R11。R10是刚才已经分析了好几遍的UIBarButtonItem,而R11来自图6-21的“MOV R11,R3”,即R3;R3又是一个没有被赋值就直接取值的寄存器,因此它也是来自[UIBarButtonItem_sendAction:withEvent:]的调用者。根据之前的分析,R11就是_sendAction:withEvent:的第二个参数,即event。这4个objc_msgSend的参数关系可以用图6-23和图6-24表示。

这样看来,[UIBarButtonItem_sendAction:withEvent:]内最关键的就是[[UIApplication sharedApplication]sendAction:[self action]to:[self target]from:self forEvent:event]这个方法了。因为已经知道[UIBarButtonItem_sendAction:withEvent:]会执行“编写邮件”操作,所以[[UIApplication sharedApplication]sendAction:[self action]to:[self target]from:self forEvent:event]肯定会得到调用。虽然上面用IDA厘清了每个参数的来源,但是这些参数在运行时的值是什么,用IDA仍看不出来;是时候借助LLDB的威力了,一起来看看在运行时这段代码都做了些什么。

图6-23 objc_msgSend的参数关系(1)

图6-24 objc_msgSend的参数关系(2)

用debugserver附加MobileMail,然后用LLDB连过去,打印出UIKit的ASLR偏移,如下:


(lldb) image list -o -f
[0] 0x0008e000/private/var/db/stash/_.29LMeZ/Applications/MobileMail.app/MobileMail(0x0000000000092000)
[1] 0x00393000/Library/MobileSubstrate/MobileSubstrate.dylib(0x0000000000393000)
[2] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/usr/lib/libarchive.2.dylib
……
[45] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOSDeviceSupport/8.1(12B411)/Symbols/System/Library/Frameworks/UIKit.framework/UIKit
……

UIKit的ASLR偏移是0x6db3000。再看看第四个objc_msgSend地址是多少,如图6-25所示。

图6-25 查看objc_msgSend的地址

在0x6db3000+0x2501F6F8=0x2BDD26F8上下个断点,然后按下“编写邮件”按钮触发断点,看看[[UIApplication sharedApplication]sendAction:[self action]to:[self target]from:self forEvent:eventFromArg2]的几个参数都是什么,如下:


(lldb) br s -a 0x2BDD26F8
Breakpoint 4: where = UIKit`-[UIBarButtonItem(UIInternal) _sendAction:withEvent:] + 116, address = 0x2bdd26f8
Process 44785 stopped
* thread #1: tid = 0xaef1, 0x2bdd26f8 UIKit`-[UIBarButtonItem(UIInternal) _sendAction:withEvent:] + 116, queue = 'com.apple.main-thread, stop reason = breakpoint 4.1
    frame #0: 0x2bdd26f8 UIKit`-[UIBarButtonItem(UIInternal) _sendAction:withEvent:] + 116
UIKit`-[UIBarButtonItem(UIInternal) _sendAction:withEvent:] + 116:
-> 0x2bdd26f8:  blx    0x2c3539f8                ; symbol stub for: roundf$shim
   0x2bdd26fc:  add    sp, #8
   0x2bdd26fe:  pop.w  {r10, r11}
   0x2bdd2702:  pop    {r4, r5, r7, pc}
(lldb) p (char *)$r1
(char *) $48 = 0x2c3de501 "sendAction:to:from:forEvent:"
(lldb) po $r0
<MailAppController: 0x176a8820>
(lldb) po $r2
[no Objective-C description available]
(lldb) p (char *)$r2
(char *) $51 = 0x2d763308 "composeButtonClicked:"
(lldb) po $r3
<nil>
(lldb) x/10 $sp
0x00391198: 0x1776d640 0x176a8ce0 0x1760f5e0 0x00000000
0x003911a8: 0x2c4140f2 0x1776ff50 0x003911cc 0x2bc6ec2b
0x003911b8: 0x176a8ce0 0x00000001
(lldb) po 0x1776d640
<ComposeButtonItem: 0x1776d640>
(lldb) po 0x176a8ce0
<UITouchesEvent: 0x176a8ce0> timestamp: 58147.4 touches: {(
    <UITouch: 0x1895e2b0> phase: Ended tap count: 1 window: <UIWindow: 0x17759c30; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x1775c7a0>; layer = <UIWindowLayer: 0x1752e190>> view: <UIToolbarButton: 0x1776ff50; frame = (285 0; 23 44); opaque = NO; gestureRecognizers = <NSArray: 0x17758670>; layer = <CALayer: 0x17770160>> location in window: {308, 534} previous location in window: {304.5, 534} location in view: {23, 10} previous location in view: {19.5, 10}
)}

其中,objc_msgSend的参数R0~R3很容易理解,分别是self、@selector(sendAction:to:from:forEvent:)、sendAction:的参数和to:的参数,直接打印寄存器就可以了。注意,在执行“po$r2”的时候,LLDB提示“no Objective-C description available”,即R2不是一个Objective-C对象,结合“action”的含义,笔者猜测它是一个SEL,就用“p(char*)$r2”打印了它。如何解析栈中的参数呢?因为SP是指向栈底的指针,而我们知道余下的2个参数都在栈中,且大小均为1个字,所以,可用“x/10$sp”打印从栈底开始的连续10个字,前2个字就是from:和forEvent:的参数。Objective-C方法的大多数参数都是1个字长度的指针,指向一个Objective-C对象,因此我们“po”了前2个字,把参数打印了出来。为了更便于理解,这里SP、栈上存储的值和参数的关系,可以参考图6-26。

图6-26 SP、栈值和参数的关系

一般情况下,Objective-C方法在栈中的参数不会超过10个,“x/10$sp”就足够了,挨个打印,就能找到栈上的所有参数。

结合IDA和LLDB,我们知道[UIBarButtonItem_sendAction:withEvent:]的核心在于[MailAppController sendAction:@selector(composeButtonClicked:)to:nil from:ComposeButtonItem forEvent:UITouchesEvent],离“编写邮件”的目标函数又近了一层。下面在IDA里看看[UIApplication sendAction:to:from:forEvent:]的内部做了些什么,如图6-27所示。

图6-27 [UIApplication sendAction:to:from:forEvent:]

无论如何,loc_24ebbc10中的“performSelector:withObject:withObject:”都会得到执行,我们自然猜测它就是做出实际操作的地方。跟刚才一样,用LLDB看看这个方法到底执行了什么操作。UIKit的ASLR偏移是0x6db3000,最下面的那个objc_msgSend地址是0x24EBBC26,故而在0x6db3000+0x24EBBC26=0x2BC6EC26上下断点,然后按下“编写邮件”按钮触发断点,再看看这个方法的参数,如下:


(lldb) br s -a 0x2BC6EC26
Breakpoint 1: where = UIKit`-[UIApplication sendAction:to:from:forEvent:] + 66, address = 0x2bc6ec26
Process 226191 stopped
* thread #1: tid = 0x3738f, 0x2bc6ec26 UIKit`-[UIApplication sendAction: to:from:forEvent:] + 66, queue = 'com.apple.main-thread, stop reason = breakpoint 1.1
    frame #0: 0x2bc6ec26 UIKit`-[UIApplication sendAction:to:from:forEvent:] + 66
UIKit`-[UIApplication sendAction:to:from:forEvent:] + 66:
-> 0x2bc6ec26:  blx    0x2c3539f8                ; symbol stub for: roundf$shim
   0x2bc6ec2a:  cmp    r6, #0
   0x2bc6ec2c:  it     ne
   0x2bc6ec2e:  movne  r6, #1
(lldb) p (char *)$r1
(char *) $0 = 0x2c3dac95 "performSelector:withObject:withObject:"
(lldb) po $r0
<ComposeButtonItem: 0x14ddf5f0>
(lldb) p (char *)$r2
(char *) $2 = 0x2c4140f2 "_sendAction:withEvent:"
(lldb) po $r3
<UIToolbarButton: 0x14d73c90; frame = (285 0; 23 44); opaque = NO; gesture Recognizers = <NSArray: 0x14d22ec0>; layer = <CALayer: 0x14d73ea0>>
(lldb) x/10 $sp
0x003735a8: 0x160a6120 0x00000001 0x14d73c90 0x160a6120
0x003735b8: 0x2c3d9be5 0x003735d4 0x2bc6ebd1 0x14d73c90
0x003735c8: 0x160a6120 0x00000040
(lldb) po 0x160a6120
<UITouchesEvent: 0x160a6120> timestamp: 73509.2 touches: {(
    <UITouch: 0x14ff2f20> phase: Ended tap count: 1 window: <UIWindow: 0x14d878b0; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x14dba890>; layer = <UIWindowLayer: 0x14d87a30>> view: <UIToolbarButton: 0x14d73c90; frame = (285 0; 23 44); opaque = NO; gestureRecognizers = <NSArray: 0x14d22ec0>; layer = <CALayer: 0x14d73ea0>> location in window: {308, 545} previous location in window: {308, 545} location in view: {23, 21} previous location in view: {23, 21}
)}

这是怎么回事?performSelector:withObject:withObject:调用了[ComposeButtonItem_sendAction:withEvent:],而[ComposeButtonItem_sendAction:withEvent:]又会调用performSelector:withObject:withObject:,如果它再次调用[ComposeButtonItem_sendAction:withEvent:],那这段代码就出现循环调用了,与观察到的现象不符,也是不合常理的。那我们执行一下“c”命令,断点一定会被再次触发,看看performSelector:withObject:withObject:有没有发生变化,如下:


(lldb) c
Process 226191 resuming
Process 226191 stopped
* thread #1: tid = 0x3738f, 0x2bc6ec26 UIKit`-[UIApplication sendAction:to:from:forEvent:] + 66, queue = 'com.apple.main-thread, stop reason = breakpoint 1.1
    frame #0: 0x2bc6ec26 UIKit`-[UIApplication sendAction:to:from:forEvent:] + 66
UIKit`-[UIApplication sendAction:to:from:forEvent:] + 66:
-> 0x2bc6ec26:  blx    0x2c3539f8                ; symbol stub for: roundf$shim
   0x2bc6ec2a:  cmp    r6, #0
   0x2bc6ec2c:  it     ne
   0x2bc6ec2e:  movne  r6, #1
(lldb) p (char *)$r1
(char *) $6 = 0x2c3dac95 "performSelector:withObject:withObject:"
(lldb) po $r0
<MailAppController: 0x14e7a7a0>
(lldb) p (char *)$r2
(char *) $7 = 0x2d763308 "composeButtonClicked:"
(lldb) po $r3
<ComposeButtonItem: 0x14ddf5f0>
(lldb) x/10 $sp
0x0037356c: 0x160a6120 0x160a6120 0x2d763308 0x14e7a7a0
0x0037357c: 0x14ddf5f0 0x003735a0 0x2bdd26fd 0x14ddf5f0
0x0037358c: 0x160a6120 0x160fbdf0
(lldb) po 0x160a6120
<UITouchesEvent: 0x160a6120> timestamp: 73509.2 touches: {(
    <UITouch: 0x14ff2f20> phase: Ended tap count: 1 window: <UIWindow: 0x14d878b0; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x14dba890>; layer = <UIWindowLayer: 0x14d87a30>> view: <UIToolbarButton: 0x14d73c90; frame = (285 0; 23 44); opaque = NO; gestureRecognizers = <NSArray: 0x14d22ec0>; layer = <CALayer: 0x14d73ea0>> location in window: {308, 545} previous location in window: {308, 545} location in view: {23, 21} previous location in view: {23, 21}
)}

可以看到,performSelector:withObject:withObject:的参数发生了变化,[MailAppController composeButtonClicked:ComposeButtonItem]得到了调用,如果再“c”一下,发现断点不再触发,所以可以确定执行实际操作的是composeButtonClicked:。因为在MobileMail内部,调用[UIApplication sharedApplication]可以拿到MailAppController对象;而在本小节开始的时候,我们在ComposeButtonItem.h里看到了可以通过一个类方法+composeButtonItem来拿到ComposeButtonItem对象;所以我们可以拿到调用[MailAppController composeButtonClicked:ComposeButtonItem]所需的全部对象,且在MobileMail的内部任何地方都可以调用这个方法,它可以算作是“编写邮件”的目标函数了。

在Cycript里做最后测试,看看这个目标函数是否好用,命令如下:


FunMaker-5:~ root# cycript -p MobileMail
cy# [UIApp composeButtonClicked:[ComposeButtonItem composeButtonItem]]

执行后成功调出“编写邮件”界面。在本例中,我们用IDA追踪函数的调用链,找到目标函数,然后用LLDB解析出了它的参数,虽然有点复杂,但其实不难,不是吗?接下来,将用类似的套路来找出“获取本机号码”的目标函数,请大家注意总结。

2.寻找“获取本机号码”的目标函数

接着上面的内容,根据找到的UI函数[PhoneSettingsController tableView:cellForRowAtIndexPath:]继续往下分析。因为UI函数的返回值存放在R0中,而从图6-17的“MOV R0,R4”可知,R0来自R4。在[PhoneSettingsController tableView:cellForRowAtIndex Path:]里,R4只在图6-28里的“MOV R4,R0”处被赋值了一次,这里的R0来自objc_msgSendSuper2执行后的返回值。objc_msgSendSuper2没有出现在文档中,由图6-29可知,它来自“/usr/lib/libobjc.A.dylib”。

按字面意思理解,objc_msgSendSuper2的作用应该跟objc_msgSendSuper类似,即向调用者的父类发送消息。不用做过多猜测,在这个objc_msgSendSuper2下个断点,看看它的参数和返回值就知道了。用debugserver附加Preferences,用LLDB连接,然后打印出MobilePhoneSettings的ASLR偏移,如下:

图6-28 R4的来源

图6-29 objc_msgSendSuper2的来源


(lldb) image list -o -f
[  0] 0x00079000 /private/var/db/stash/_.29LMeZ/Applications/Preferences.app/Preferences(0x000000000007d000)
[  1] 0x00232000 /Library/MobileSubstrate/MobileSubstrate.dylib (0x0000000000232000)
[  2] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/PrivateFrameworks/BulletinBoard.framework/BulletinBoard
[  3] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
……
[330] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/PreferenceBundles/MobilePhoneSettings.bundle/MobilePhoneSettings
……

MobilePhoneSettings的ASLR偏移是0x6db3000。然后看看objc_msgSendSuper2的地址,如图6-30所示。

断点的地址应该是0x6db3000+0x25BB2B68=0x2C965B68。返回上一级目录,再进入MobilePhoneSettings触发断点,如下:

图6-30 查看objc_msgSendSuper2的地址


(lldb) br s -a 0x2C965B68
Breakpoint 1: where = MobilePhoneSettings`-[PhoneSettingsController tableView:cellForRowAtIndexPath:] + 40, address = 0x2c965b68
Process 268587 stopped
* thread #1: tid = 0x4192b, 0x2c965b68 MobilePhoneSettings`-[PhoneSettings Controller tableView:cellForRowAtIndexPath:] + 40, queue = 'com.apple.main-thread, stop reason = breakpoint 1.1
    frame #0: 0x2c965b68 MobilePhoneSettings`-[PhoneSettingsController tableView:cellForRowAtIndexPath:] + 40
MobilePhoneSettings`-[PhoneSettingsController tableView:cellForRowAtIndexPath:] + 40:
-> 0x2c965b68:  blx    0x2c975fb8                ; symbol stub for: CTSettingRequest$shim
   0x2c965b6c:  mov    r4, r0
   0x2c965b6e:  movw   r0, #54708
   0x2c965b72:  movt   r0, #2697
(lldb) p (char *)$r1
(char *) $0 = 0x2c3daf33 "tableView:cellForRowAtIndexPath:"
(lldb) po $r0
[no Objective-C description available]
(lldb) ni
Process 268587 stopped
* thread #1: tid = 0x4192b, 0x2c965b6c MobilePhoneSettings`-[PhoneSettings Controller tableView:cellForRowAtIndexPath:] + 44, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x2c965b6c MobilePhoneSettings`-[PhoneSettingsController tableView:cellForRowAtIndexPath:] + 44
MobilePhoneSettings`-[PhoneSettingsController tableView:cellForRowAtIndexPath:] + 44:
-> 0x2c965b6c:  mov    r4, r0
   0x2c965b6e:  movw   r0, #54708
   0x2c965b72:  movt   r0, #2697
   0x2c965b76:  mov    r2, r5
(lldb) po $r0
<PSTableCell: 0x15fc6b00; baseClass = UITableViewCell; frame = (0 0; 320 44); text = 'My Number'; tag = 2; layer = <CALayer: 0x15fbbe40>>
(lldb) po [$r0 detailTextLabel]
<UITableViewLabel: 0x15fb5590; frame = (0 0; 0 0); text = '+86PhoneNumber'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x15fd87e0>>

值得一提的是,objc_msgSendSuper2的第一个参数并不是一个Objective-C对象,我不清楚这到底是LLDB的bug,还是情况确实如此,但这不影响本节的分析,忽略这个细节就好。感兴趣的朋友可以继续研究,然后在http://bbs.iosre.com 分享你的发现。

话说回来,LLDB的输出结果预示着objc_msgSendSuper2的返回结果就是初始化好的cell,里面已经含有了本机号码信息。跟上一节类似,到PhoneSettingsController的父类里看看tableView:cellForRowAtIndexPath:的实现。首先打开PhoneSettingsController.h,看看它的父类是谁,如下:


@interface PhoneSettingsController : PhoneSettingsListController <TPSetPINViewControllerDelegate>
……
@end

可以看到,PhoneSettingsController继承自PhoneSettingsListController,再打开Phone-SettingsListController.h,看看它有没有实现tableView:cellForRowAtIndexPath:方法,如下:


@interface PhoneSettingsListController : PSListController
{
}
- (id)bundle;
- (void)dealloc;
- (id)init;
- (void)pushController:(Class)arg1 specifier:(id)arg2;
- (id)setCellEnabled:(BOOL)arg1 atIndex:(unsigned int)arg2;
- (id)setCellLoading:(BOOL)arg1 atIndex:(unsigned int)arg2;
- (id)setControlEnabled:(BOOL)arg1 atIndex:(unsigned int)
arg2;
- (id)sheetSpecifierWithTitle:(id)arg1 controller:(Class)arg2 
detail:(Class)arg3;
- (void)simRemoved:(id)arg1;
- (id)specifiers;
- (void)updateCellStates;
- (void)viewWillAppear:(BOOL)arg1;
@end

可见,PhoneSettingsListController没有实现tableView:cellForRowAtIndexPath:,继续去它的父类PSListController里看看。PSListController已经不在MobilePhoneSettings.bundle里了,用上一章介绍的搜索方法,很容易就可以在所有class-dump头文件里定位PSListController.h,如图6-31所示。

图6-31 定位PSListController.h

注意,PSListController.h来自与Preferences.app同名的Preferences.framework,请大家注意分辨。打开它,看看有没有实现tableView:cellForRowAtIndexPath:方法,如下:


@interface PSListController : PSViewController <UITableViewDelegate, UITableView DataSource, UIActionSheetDelegate, UIAlertViewDelegate, UIPopoverControllerDelegate, PSSpecifierObserver, PSViewControllerOffsetProtocol>
……
- (id)tableView:(id)arg1 cellForRowAtIndexPath:(id)arg2;
……
@end

可以看到,它确实实现了这个方法,在IDA中打开Preferences.framework里的二进制文件,定位到tableView:cellForRowAtIndexPath:,如图6-32所示。

图6-32 [PSListController tableView:cellForRowAtIndexPath:]

它的实现逻辑有些复杂,为了保险起见,先在它的尾部下一个断点,看看返回值里是否含有“本机号码”信息,确认objc_msgSendSuper2是否调用了[PSListController tableView:cellForRowAtIndexPath:]。先看看Preferences.framework的ASLR偏移,如下:


(lldb) image list -o -f
[  0] 0x00079000 /private/var/db/stash/_.29LMeZ/Applications/Preferences.app/Preferences(0x000000000007d000)
[  1] 0x00232000 /Library/MobileSubstrate/MobileSubstrate.dylib(0x0000000000232000)
[  2] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/PrivateFrameworks/BulletinBoard.framework/BulletinBoard
[  3] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation 
……
[ 42] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/PrivateFrameworks/Preferences.framework/Preferences
……

它的ASLR偏移是0x6db3000。然后看看[PSListController tableView:cellForRowAtIndexPath:]尾部指令的地址,如图6-33所示。

图6-33 [PSListController tableView:cellForRowAtIndexPath:]

因为返回值存放在R0中,而R0来自“MOV R0,R6”,即R6,所以在这条指令上下一个断点,然后打印R6。这条指令的地址是0x2A9F79E6,因此断点的地址是0x6db3000+0x2A9F79E6=0x317AA9E6。返回上一页再重新进入MobilePhoneSettings,触发断点,如下:


(lldb) br s -a 0x317AA9E6
Breakpoint 5: where = Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 1026, address = 0x317aa9e6
Process 268587 stopped
* thread #1: tid = 0x4192b, 0x317aa9e6 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 1026, queue = 'com.apple.main-thread, stop reason = breakpoint 5.1
    frame #0: 0x317aa9e6 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 1026
Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 1026:
-> 0x317aa9e6:  mov    r0, r6
   0x317aa9e8:  add    sp, #28
   0x317aa9ea:  pop.w  {r8, r10, r11}
   0x317aa9ee:  pop    {r4, r5, r6, r7, pc}
(lldb) po $r6
<PSTableCell: 0x15f8c6a0; baseClass = UITableViewCell; frame = (0 0; 320 44); text = 'My Number'; tag = 2; layer = <CALayer: 0x15f7c0b0>>
(lldb) po [$r6 detailTextLabel]
<UITableViewLabel: 0x15f7b8d0; frame = (0 0; 0 0); text = '+86PhoneNumber'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x15f7b990>>

从LLDB的输出可以确认objc_msgSendSuper2调用了[PSListController tableView:cellForRowAtIndexPath:],且它的返回值来自于R6。那R6来自于哪里呢?当我们往上回溯,查找R6来源的时候,可以看到R6作为objc_msgSend的第一个参数,多次出现在了这个方法内部,如图6-34所示。

图6-34 R6出现频率很高

再往上一点,会发现往R6里写入的,都是刚刚初始化的各种对象,如图6-35、图6-36、图6-37所示。

图6-35 R6被赋值(1)

这个现象很好理解,tableView:cellForRowAtIndexPath:的作用本来就是返回一个可用的cell。因此,常规的做法是在方法内部先创建一个空的cell,然后调用别的函数来配置它。那么,从一个空的PSTableCell到含有“本机号码”信息的这个配置过程发生在哪里呢?现在已知头部的PSTableCell不含有本机号码,尾部的PSTableCell含有本机号码,所以这个设置过程一定是发生在tableView:cellForRowAtIndexPath:内部的,且是通过一个objc_msgSend函数完成的。因此,现在的问题变成了,在一堆objc_msgSend函数中,怎么去定位那个设置“本机号码”的objc_msgSend?

图6-36 R6被赋值(2)

图6-37 R6被赋值(3)

如果不考虑效率,可以从头开始一个个排查。table-View:cellForRowAtIndexPath:内部的objc_msgSend个数毕竟有限,在执行objc_msgSend之前和之后各打印一次[$r6 detailText-Label],对比两者的异同,就一定可以找到这个objc_msgSend;数学比较好的朋友可能用二分法,从tableView:cellForRow-AtIndexPath:中间部分的某个objc_msgSend开始找,不断缩小排查范围。这就是见仁见智的问题了,大家选择一种自己喜欢的方式就好。在这里,笔者采取了折中的二分法,如图6-38所示。

图6-38 [PSListControllertableView: cellForRowAtIndexPath:]

采用二分查找法固然效率高,但[PSListController tableV-iew:cellForRowAtIndexPath:]的分支很多,从哪个地方分,可以保证不遗漏每一个分支呢?因为[PSListController tableVi-ew:cellForRowAtIndexPath:]的执行一定会通过图6-38所示的深色方块,所以以这个地方为二分点肯定不会遗漏任何分支,然后从它的第一个objc_msgSend开始排查,如果[$r6 detailTextLabel]含有本机号码信息,那么就往上找,否则往下找。我们去看看这个深色方块包含的汇编指令,如图6-39所示。

图6-39 深色方块所在的loc_2a9f7966

这里有2个objc_msgSend,就从最上面这一个开始吧,看看它的地址,如图6-40所示。

图6-40 查看objc_msgSend的地址

Preferences的ASLR偏移是0x6db3000,刚才已经用到了,所以断点的地址是0x6db3000+0x2A9F797E=0x317AA97E。触发它,看看此时PSTableCell是否含有本机号码信息,如下:


(lldb) br s -a 0x317AA97E
Breakpoint 10: where = Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 922, address = 0x317aa97e
Process 268587 stopped
* thread #1: tid = 0x4192b, 0x317aa97e Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 922, queue = 'com.apple.main-thread, stop reason = breakpoint 10.1
    frame #0: 0x317aa97e Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 922
Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 922:
-> 0x317aa97e:  blx    0x31825f04                ; symbol stub for: ____NETRBClientResponseHandler_block_invoke
   0x317aa982:  mov    r2, r0
   0x317aa984:  movw   r0, #59804
   0x317aa988:  movt   r0, #1736
(lldb) po [$r6 detailTextLabel]
<UITableViewLabel: 0x15f7e490; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x15fd1c90>>

它还不含有本机号码信息,说明本机号码信息一定是在图6-38深色方块下方的3个方块里生成的。接着执行“ni”命令,在每个objc_msgSend的前后各“po[$r6 detailTextLabel]”一次,如下:


(lldb) ni
Process 268587 stopped
* thread #1: tid = 0x4192b, 0x317aa982 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 926, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x317aa982 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 926
Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 926:
-> 0x317aa982:  mov    r2, r0
   0x317aa984:  movw   r0, #59804
   0x317aa988:  movt   r0, #1736
   0x317aa98c:  add    r0, pc
(lldb) po [$r6 detailTextLabel]
<UITableViewLabel: 0x15f7e490; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x15fd1c90>>
(lldb) ni
……
Process 268587 stopped
* thread #1: tid = 0x4192b, 0x317aa992 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 942, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x317aa992 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 942
Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 942:
-> 0x317aa992:  blx    0x31825f04                ; symbol stub for: ____NETRBClientResponseHandler_block_invoke
   0x317aa996:  tst.w  r0, #255
   0x317aa99a:  beq    0x317aa9e6                ; -[PSListController tableView:cellForRowAtIndexPath:] + 1026
   0x317aa99c:  movw   r0, #60302
(lldb) po [$r6 detailTextLabel]
<UITableViewLabel: 0x15f7e490; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x15fd1c90>>
(lldb) ni
Process 268587 stopped
* thread #1: tid = 0x4192b, 0x317aa996 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 946, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x317aa996 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 946
Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 946:
-> 0x317aa996:  tst.w  r0, #255
   0x317aa99a:  beq    0x317aa9e6                ; -[PSListController tableView:cellForRowAtIndexPath:] + 1026
   0x317aa99c:  movw   r0, #60302
   0x317aa9a0:  mov    r2, r11
(lldb) po [$r6 detailTextLabel]
<UITableViewLabel: 0x15f7e490; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x15fd1c90>>
(lldb) ni
……
Process 268587 stopped
* thread #1: tid = 0x4192b, 0x317aa9ac Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 968, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x317aa9ac Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 968
Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 968:
-> 0x317aa9ac:  blx    0x31825f04                ; symbol stub for: ____NETRBClientResponseHandler_block_invoke
   0x317aa9b0:  movw   r0, #60822
   0x317aa9b4:  mov    r2, r11
   0x317aa9b6:  movt   r0, #1736
(lldb) po [$r6 detailTextLabel]
<UITableViewLabel: 0x15f7e490; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x15fd1c90>>
(lldb) ni
Process 268587 stopped
* thread #1: tid = 0x4192b, 0x317aa9b0 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 972, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x317aa9b0 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 972
Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 972:
-> 0x317aa9b0:  movw   r0, #60822
   0x317aa9b4:  mov    r2, r11
   0x317aa9b6:  movt   r0, #1736
   0x317aa9ba:  add    r0, pc
(lldb) po [$r6 detailTextLabel]
<UITableViewLabel: 0x15f7e490; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x15fd1c90>>
(lldb) ni
……
Process 268587 stopped
* thread #1: tid = 0x4192b, 0x317aa9c0 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 988, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x317aa9c0 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 988
Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 988:
-> 0x317aa9c0:  blx    0x31825f04                ; symbol stub for: ____NETRBClientResponseHandler_block_invoke
   0x317aa9c4:  movw   r0, #4312
   0x317aa9c8:  movt   r0, #1737
   0x317aa9cc:  add    r0, pc
(lldb) po [$r6 detailTextLabel]
<UITableViewLabel: 0x15f7e490; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x15fd1c90>>
(lldb) ni
Process 268587 stopped
* thread #1: tid = 0x4192b, 0x317aa9c4 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 992, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x317aa9c4 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 992
Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 992:
-> 0x317aa9c4:  movw   r0, #4312
   0x317aa9c8:  movt   r0, #1737
   0x317aa9cc:  add    r0, pc
   0x317aa9ce:  ldr    r0, [r0]
(lldb) po [$r6 detailTextLabel]
<UITableViewLabel: 0x15f7e490; frame = (0 0; 0 0); text = '+86PhoneNumber'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x15fd1c90>>

在0x317aa9c0处的objc_msgSend前后PSTableCell的本机号码信息发生了变化,0x317aa9c0-0x6db3000=0x2A9F79C0,在IDA中定位到这个objc_msgSend,如图6-41所示。

图6-41 设置本机号码的objc_msgSend

“用specifier刷新cell的内容”,这个方法的作用显而易见,我们看看这个specifier是什么。在这个objc_msgSend上下个断点,触发后,打印它的参数,如下:


(lldb) br s -a 0x317AA9C0
Breakpoint 11: where = Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 988, address = 0x317aa9c0
Process 268587 stopped
* thread #1: tid = 0x4192b, 0x317aa9c0 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 988, queue = 'com.apple.main-thread, stop reason = breakpoint 11.1
    frame #0: 0x317aa9c0 Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 988
Preferences`-[PSListController tableView:cellForRowAtIndexPath:] + 988:
-> 0x317aa9c0:  blx    0x31825f04                ; symbol stub for: ____NETRBClientResponseHandler_block_invoke
   0x317aa9c4:  movw   r0, #4312
   0x317aa9c8:  movt   r0, #1737
   0x317aa9cc:  add    r0, pc
(lldb) p (char *)$r1
(char *) $97 = 0x318362d2 "refreshCellContentsWithSpecifier:"
(lldb) po $r2
My Numbe        ID:myNumberCell 0x170ece60      target:<PhoneSettingsController 0x170ed760: navItem <UINavigationItem: 0x170d0b40>, view <UITableView: 0x16acb200; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x15d232d0>; layer = <CALayer: 0x15fc9110>; contentOffset: {0, -64}; contentSize: {320, 717.5}>>
(lldb) po [$r2 class]
PSSpecifier

可以看到,specifier是一个PSSpecifier对象,而且与本机号码相关。如果你在第5章的PreferenceBundle部分仔细阅读过preferences specifier plist标准,就知道PSTableCell的内容是由PSSpecifier指定的,因此可以通过[PSSpecifier propertyForKey:@”set”]和[PSSpecifier propertyForKey:@”get”]拿到PSSpecifier的setter和getter,如下:


(lldb) po [$r2 propertyForKey:@"set"]
setMyNumber:specifier:
(lldb) po [$r2 propertyForKey:@"get"]
myNumber:

还可以通过[PSSpecifier target]拿到它们的target,如下:


(lldb) po [$r2 target]
<PhoneSettingsController 0x170ed760: navItem <UINavigationItem: 0x170d0b40>, view <UITableView: 0x16acb200; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x15d232d0>; layer = <CALayer: 0x15fc9110>; contentOffset: {0, -64}; contentSize: {320, 717.5}>>

非常好,现在我们知道PSTableCell的本机号码是通过[PhoneSettingsController setMy Number:specifier:]方法设置的,通过[PhoneSettingsController myNumber:]读取的(你对它俩还有印象吗?),那么,在myNumber:内部,就一定有获取本机号码的方法,如图6-42所示。

图6-42 [PhoneSettingsController myNumber:]

[PhoneSettingsController myNumber:]的逻辑比较简单,就是看[[PhoneSettingsTelephony telephony]myNumber]的长度是否为0,如果不为0,它就是本机号码,否则返回一个“未知号码”,告诉用户无法读取本机号码。用Cycript测试一下这个方法,如下:


FunMaker-5:~ root# cycript -p Preferences
cy# [[PhoneSettingsTelephony telephony] myNumber]
@"+86PhoneNumber"

现在,退出Preferences,把它从后台彻底关掉后重新打开,不要进入MobilePhoneSettings界面,再测试一次这个方法,如下:


FunMaker-5:~ root# cycript -p Preferences
cy# [[PhoneSettingsTelephony telephony] myNumber]
ReferenceError: Can't find variable: PhoneSettingsTelephony

出现了错误,这是怎么回事?那是因为PhoneSettingsTelephony是MobilePhoneSettings.bundle中的一个类,如果不进入MobilePhoneSettings界面,这个bundle是不会加载的,所以这个类也是不存在的。也就是说,要调用这个方法,需要先加载MobilePhoneSettings.bundle。Preference.app加载MobilePhoneSettings.bundle的方式被称为延迟加载(lazy load),在iOS逆向工程中出现类似状况的时候很多,当你碰到时,欢迎来http://bbs.iosre.com 跟大家交流心得。

其实到此为止,可以认为我们已经找到了目标函数,因为我们拿到了这个方法的调用者和参数,而且这个方法不涉及UI操作,调用起来干净利落。但有一点让人不爽的是,调用这个方法前必须加载MobilePhoneSettings.bundle。有没有办法去掉这个硬指标,让我们不需要加载这个bundle就能拿到本机号码呢?应该存在这么一个方法。因为本机号码是存储在SIM卡上的,所以[PhoneSettingsTelephony myNumber]的原始数据源应该来自SIM卡,而能够访问SIM卡的显然不止MobilePhoneSetting.bundle,因此底层一定存在更通用的访问SIM卡的库,如果能定位到这个库,估计就可以直接读取本机号码了。既然是一个更底层的库,那么自然要从[PhoneSettingsTelephony myNumber]入手,看看它的内部是如何读取本机号码的,如图6-43所示。

它的逻辑也比较简单,先取出实例变量_myNumber,如果它不是nil,则走左边并记录“My Number requested,returning cached value:%@”,即返回一个缓存中的数据;否则走右边,先调用PhoneSettingsCopyMyNumber函数取得本机号码,再记录“My Number requested,no cached value,fetched:%@”,即没有在缓存中找到本机号码,返回一个现取的数据。因此,调用PhoneSettingsCopyMyNumber可以取得本机号码,但从名字来看,它仍然是MobilePhoneSettings.bundle里的一个函数,在这个bundle外不能调用,看来我们挖得还不够深。继续看看这个函数内部做了些什么,如图6-44所示。

图6-43 [PhoneSettingsTelephony myNumber]

图6-44 PhoneSettingsCopyMyNumber

这段代码先调用CTSettingCopyMyPhoneNumber函数,把返回值给autorelease掉,然后再调用PhoneSettingsCopyFormattedNumberBySIMCountry,看其函数名好像是根据SIM卡所在的国家把号码给格式化了。那么CTSettingCopyMyPhoneNumber函数无论是从名字还是上下文来看,都非常疑似获取本机号码的函数,而且CT前缀说明它来自CoreTelephony,而不是MobilePhoneSettings。双击这个函数,看看它的内部实现,如图6-45所示。

图6-45 CTSettingCopyMyPhoneNumber

果然是一个外部函数,再次双击“__imp__CTSettingCopyMyPhoneNumber”,看看它来自哪个库——正是CoreTelephony。退出Preferences,把它从后台彻底关掉后重新打开,不要进入MobilePhoneSettings界面,然后用debugserver附加,用LLDB打印出image list,你会发现CoreTelephony赫然名列其中。这意味着,我们不需要加载MobilePhoneSettings.bundle就可以调用CTSettingCopyMyPhoneNumber获取未经格式化的本机号码,它就是我们要找的目标函数。那么还剩最后一个问题——它的参数和返回值是什么?

从图6-44看来,CTSettingCopyMyPhoneNumber不像是有参数——它的前面甚至没有出现R0~R3寄存器。如果它有参数,那么R0~R3也是来自它的调用者,即PhoneSettingsCopyMyNumber。但从图6-43看来,PhoneSettingsCopyMyNumber之前也只出现了R0,且如果进程走右边,R0一定是0,PhoneSettingsCopyMyNumber看起来也没有参数。为了保险起见,还是去CoreTelephony里看看CTSettingCopyMyPhoneNumber的实现,如图6-46所示。

根据Objective-C函数的命名惯例,CTTelephonyCenterGetDefault是有返回值的;在“BL_CTTelephonyCenterGetDefault”下面,R0被CTTelephonyCenterGetDefault的返回值覆盖掉了;而在图6-46的最下面,R1也被“MOV R1,R4”中的R4覆盖掉了。如果R0和R1是参数,那么这2个参数就没有起任何作用,不合常理,因此说明CTSettingCopyMyPhoneNumber没有参数。那么它的返回值呢?我们会很自然地猜测它的返回值是一个字符串,但为了保险起见,还是在CTSettingCopyMyPhoneNumber的尾部下个断点,把R0打印出来看看吧。先在IDA中看看它的地址,如图6-47所示。

图6-46 CTSettingCopyMyPhoneNumber

然后退出Preferences,把它从后台彻底关掉后重新打开,不要进入MobilePhoneSettings界面,然后用debugserver附加,用LLDB查看CoreTelephony的ASLR偏移,如下:

图6-47 CTSettingCopyMyPhoneNumber


(lldb) image list -o -f
[  0] 0x000b3000 /private/var/db/stash/_.29LMeZ/Applications/Preferences.app/Preferences(0x00000000000b7000)
[  1] 0x0026c000 /Library/MobileSubstrate/MobileSubstrate.dylib (0x000000000026c000)
[  2] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/PrivateFrameworks/BulletinBoard.framework/BulletinBoard [ 51] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/Frameworks/CoreTelephony.framework/CoreTelephony
……

我们就把断点下在0x6db3000+0x2226763A=0x2901A63A上吧。然后进入MobilePhone-Settings界面,触发断点,如下:


(lldb) br s -a 0x2901A63A
Breakpoint 1: where = CoreTelephony`CTSettingCopyMyPhoneNumber + 78, address = 0x2901a63a
Process 330210 stopped
* thread #1: tid = 0x509e2, 0x2901a63a CoreTelephony`CTSettingCopyMyPhoneNumber + 78, queue = 'com.apple.main-thread, stop reason = breakpoint 1.1
    frame #0: 0x2901a63a CoreTelephony`CTSettingCopyMyPhoneNumber + 78
CoreTelephony`CTSettingCopyMyPhoneNumber + 78:
-> 0x2901a63a:  add    sp, #28
   0x2901a63c:  pop.w  {r8, r10, r11}
   0x2901a640:  pop    {r4, r5, r6, r7, pc}
   0x2901a642:  nop    
(lldb) po $r0
+86PhoneNumber
(lldb) po [$r0 class]
__NSCFString

它就是一个NSString,这样就可以还原这个函数的原型啦——


NSString *CTSettingCopyMyPhoneNumber(void);

它就是我们的目标函数,也就是PSTableCell的数据源,我们通过分析[PhoneSettings Controller tableView:cellForRowAtIndexPath:]所在的函数调用链找到了它。在调用它的时候,注意释放返回值就好了。写一个小tweak测测这个函数,确保它是正确的。

(1)用Theos新建tweak工程“iOSREGetMyNumber”,命令如下:


snakeninnys-MacBook:Code snakeninny$ /opt/theos/bin/nic.pl
NIC 2.0 - New Instance Creator
------------------------------
  [1.] iphone/application
  [2.] iphone/cydget
  [3.] iphone/framework
  [4.] iphone/library
  [5.] iphone/notification_center_widget
  [6.] iphone/preference_bundle
  [7.] iphone/sbsettingstoggle
  [8.] iphone/tool
  [9.] iphone/tweak
  [10.] iphone/xpc_service
Choose a Template (required): 9
Project Name (required): iOSREGetMyNumber
Package Name [com.yourcompany.iosregetmynumber]: com.iosre.iosregetmynumber
Author/Maintainer Name [snakeninny]: snakeninny
[iphone/tweak] MobileSubstrate Bundle filter [com.apple.springboard]: com.apple.Preferences
[iphone/tweak] List of applications to terminate upon 
installation (space-separated, '-' for none) [SpringBoard]: Preferences
Instantiating iphone/tweak in iosregetmynumber/...
Done.

(2)编辑Tweak.xm,代码如下:


extern "C" NSString *CTSettingCopyMyPhoneNumber(void); // 来自CoreTelephony
%hook PreferencesAppController
- (BOOL)application:(id)arg1 didFinishLaunchingWithOptions:
(id)arg2
{
        BOOL result = %orig;
        NSLog(@"iOSRE: my number = %@", 
[CTSettingCopyMyPhoneNumber() autorelease]);
        return result;
}
%end

(3)编辑Makefile及control

编辑后的Makefile内容如下:


THEOS_DEVICE_IP = iOSIP
ARCHS = armv7 arm64
TARGET = iphone:latest:8.0
include theos/makefiles/common.mk
TWEAK_NAME = iOSREGetMyNumber
iOSREGetMyNumber_FILES = Tweak.xm
iOSREGetMyNumber_FRAMEWORKS = CoreTelephony # CTSettingCopyMyPhoneNumber来自这里
include $(THEOS_MAKE_PATH)/tweak.mk
after-install::
      install.exec "killall -9 Preferences"

编辑后的control内容如下:


Package: com.iosre.iosregetmynumber
Name: iOSREGetMyNumber
Depends: mobilesubstrate, firmware (>= 8.0)
Version: 1.0
Architecture: iphoneos-arm
Description: Get my number just like MobilePhoneSettings!
Maintainer: snakeninny
Author: snakeninny
Section: Tweaks
Homepage: http://bbs.iosre.com

(4)测试

将写好的tweak编译打包安装到iOS后,打开Preferences,不要进入MobilePhoneSettings界面。然后ssh到iOS上看看syslog,如下:


FunMaker-5:~ root# grep iOSRE: /var/log/syslog
Nov 29 23:23:01 FunMaker-5 Preferences[2078]: iOSRE: my number = +86PhoneNumber

(5)补充

因为笔者的iPhone 5将地区设置为了美国,所以格式化之前的本机号码是“+86PhoneNumber”,被PhoneSettingsCopyFormattedNumberBySIMCountry格式化之后变成了“+86 Pho-neNu-mber”,即美国电话号码格式。

在逆向其他目标碰到CTSettingCopyMyPhoneNumber时,随着iOS逆向工程熟练度的增加,你就会慢慢发现,它的正确原型其实是:


CFStringRef CTSettingCopyMyPhoneNumber();

因为NSString*和CFStringRef是等价的,所以我们的写法也没问题。

因为CTSettingCopyMyPhoneNumber的函数名中含有“copy”字样,且它返回了一个CoreData对象,所以根据苹果的“Ownership Policy”(Google搜索“apple ownership policy”),我们要负责释放这个函数的返回值。

本节用大量篇幅,用ARM汇编完善了“定位目标函数”环节,并将其细分为“从现象切入App,找出UI函数”和“以UI函数为起点,寻找目标函数”两步,结合Cycript、IDA和LLDB,既定位了目标函数,又解析了一些不够直观的函数参数。两个例子中演示的套路基本可以应付现在95%的App,如果你有幸碰到了那5%搞不定的,欢迎来http://bbs.iosre.com 提供案例,我们一起来寻求解决方案。