微博送书抽奖插件iOSRELottery的编写及逆向思路

前言
在2014年2月14日元宵节当天,@iOS应用逆向工程 进行了书上市后的第一次抽奖活动。为了体现书的特点,我们没有采用各种抽奖转发平台,而是采用了自己编写的iOSRELottery插件,在新浪微博iOS客户端中进行了这次操作。距离第一批读者朋友们拿到书已经有半个多月的时间了,有些朋友可能已经在按照书上介绍的内容上手某些App了,但更多的朋友可能还处在初步学习的阶段。今天,我们就以iOSRELottery为主轴,从逆向工程的角度来一步一步还原这个抽奖插件的编写过程,希望朋友们能从中获得一些逆向工程的灵感(请朋友们自己先行dump出新浪微博的明文版并class-dump,具体过程可以参考我的另一个帖子“用dumpdecrypted给App砸壳”;然后把Weibo.decrypted丢到IDA里开始分析)。
一、根据抽奖规则,确定我们需要的数据
我们制定的规则是“关注@iOS应用逆向工程,转发此条微博并@ 3位好友,截止到下周一中午12:00,我们会抽出5名朋友”。粗看一眼这条规则,它所蕴含的信息有:

  1. 关注@iOS应用逆向工程;
  2. 转发此条微博;
  3. @ 3位好友;
  4. 时间节点是下周一中午12点;
    这些信息是没有问题的,但正如书中10.2.6节所说,“这个信息一点也没错,但太平民化了,也就是说,这是一名iPhone用户眼中的‘规则’,对于程序员来说,这样的概念太抽象,提供的信息不足以让我们把它转换成代码。”那么从程序员或iOS逆向工程师的角度来看,什么是“规则”呢?
  5. 关注@iOS应用逆向工程; <------ 我们要判断一个用户是不是@iOS应用逆向工程 的粉丝;此方法由新浪微博客户端提供,实现方法未知,需逆向;
  6. 转发此条微博; <------ 我们要获取所有转发抽奖微博的用户名;此方法由新浪微博客户端提供,实现方法未知,需逆向;
  7. @ 3位好友; <------ 我么要获取所有转发微博的内容;此方法由新浪微博客户端提供,实现方法未知,需逆向;
  8. 时间节点是下周一中午12点; <------ 我们要获取当前时间;此方法用NSDate类即可实现。

二、找寻数据来源
在经过第一步的分析,确定了我们需要的数据后,我们就要进一步思考,去哪里获取这些数据?你可能会说,这个问题还要问吗?点进抽奖微博,然后看转发,就可以获取上面2的数据;转发内容是@了3个人的,就可以获取上面3的数据;如果满足了这两个条件,那我再把这个人的用户名拿到我的粉丝里匹配,从而获取1的数据。没错,这个思路是完全正确的,所以,下一步我们要做的是……

三、确定抽奖微博转发页面的代码名(code name)
阅读了书上3.3节的朋友应该都会对Reveal的功能印象深刻,但Reveal只有短短30天的试用期,且购买价格并不便宜,因此我在逆向工程中一般都是直接采用Cycript来定位我感兴趣的UI控件,采用的方法是“[UIApp keyWindow] recursiveDescription]”,与Reveal如出一辙。如果你也舍不得购买Reveal,就可以跟随下面的分析思路,用Cycript达到跟Reveal相同的目的。
首先,打开新浪微博iOS客户端,进入一条微博,切换到转发页面(以抽奖微博为例)。ssh到你的iDevice(以笔者的iPhone 5为例)中,然后输入“ps ax | grep -i weibo”,看看新浪微博的进程名/PID是什么,如图所示:


可以看到,新浪微博的进程名就是Weibo(注意大小写),PID是7297。用Cycript注入这个进程,如图:


接着我们输入查看界面结构的”金句“,也就是刚才提到的“[UIApp keyWindow] recursiveDescription]”;在这条命令前,我们输入“?expand”来规范化Cycript的输出,如图:


上图已经把转发界面的UI结构给格式化打印了出来,所有UI对象的description一览无余。那么要怎么定位每一条转发的UI呢?因为description会打印出关于这个UI的几乎所有信息,包括它上面显示的文字,所以这就好办了。观察下图:


Cycript还不能很好地支持中文的显示(只能显示出unicode编码),因此我们可以用英文作为标准。根据上图中很有特点的“sophisticated”关键词,搜索刚才Cycript打印出来的结果,看看这条微博是显示在什么控件里的,如图所示:


这个“18岁的光亮sophisticated”看起来是个僵尸号,但不管他,僵尸号也给我们提供了很有用的信息——负责显示用户名的是WBUserScreenNameLabel这个控件。那这个控件又是显示在什么控件之上的呢?根据格式化之后的Cycript输出,我们很容易就根据这个树形结构找到WBUserScreenNameLabel的superView,即WBUserScreenNameLabelControl。按照这个思路继续往上回溯,是不是很容易就可以找到转发界面的控件类?没错,往上看,你很容易就会发现WBRepostListTableViewCell,而这个cell所在的view是WBPlaceholderTableView——占位符tableview?这名字是什么套路?虽然感觉不像,但是代码不会骗人,是不是这个tableview,我们试一试就知道了——我们知道,UITableView上的数据都是来自于UITableViewDataSource,那WBPlaceholderTableView的datasource是什么呢?看下图:


哼哼,WBRepostListViewController被我们揪出来了,而且从名字上来看,它极有可能含有我们需要的数据。我们在class-dump出的头文件里打开WBRepostListViewController.h,发现里面内容不多,但WBRepostListViewController是继承自WBStatusBusinessViewController,打开WBStatusBusinessViewController.h,内容就多起来了,而且我们看到了一个关键的方法- (void)viewDidLoad,如图所示:


这就预示着,我们通过hook上WBRepostListViewController的- (void)viewDidLoad方法,即可第一时间捕捉到打开微博转发界面的操作,进而尝试获取各种数据。那么,在进入这个界面后,如何获取到所有的转发呢?在新浪微博客户端中,我们只有不断上滑屏幕,才会加载更新的评论和转发,因此观察WBStatusBusinessViewController的所有方法,几个“scrollView”就显得特别可疑;loadMoreData、triggerLoadMore看似就是实际负责加载操作的方法。接下来,我们就去IDA中浏览这几个可疑方法的内部实现,看看能不能找到一些蛛丝马迹。

四、寻找“加载更多”功能的实现
实际操作过新浪微博iOS客户端的朋友就会知道,当上滑并松手时,才会“加载更多”。这个操作在UIScrollViewDelegate中的触发函数是scrollViewDidEndDragging:willDecelerate:。我们在IDA中看看[WBStatusBusinessViewController scrollViewDidEndDragging:willDecelerate:],如图:


看来,它首先获取了自己的rootViewController,然后调用了rootViewController的相同函数。那么它的rootViewController是谁呢?我们到Cycript中一探究竟:


好的,是MessageViewContentController,继续在IDA中定位到[MessageViewContentController scrollViewDidEndDragging:willDecelerate:],如图:


一个新的类WBPRLMTableViewWrapper出现了,[WBPRLMTableViewWrapper scrollViewDidEndDragging:]得到调用。当你继续在IDA的Function Window里寻找这个函数时,你会发现根本找不到。这是怎么回事呢?刨开IDA的bug外,只有一种可能:这个函数是由WBPRLMTableViewWrapper的超类实现的。是这样吗?我们打开WBPRLMTableViewWrapper.h,看看它的超类,如图:


我们看看PRLTableViewWrapper中是不是实现了scrollViewDidEndDragging:方法,如图:


果不其然,我们的猜测得到了验证。接下来,自然是去IDA中分析[PRLTableViewWrapper scrollViewDidEndDragging:],如图(下面的汇编语言涉及到了一些书上没有讲过的东西,请参考ARM官网文档):


从这张图就可以看出,[PRLTableViewWrapper scrollViewDidEndDragging:]拥有很多逻辑跳转,内部功能非常丰富,因此我们可以大胆预测,“加载更多”的操作很有可能就藏身其中。我们各个击破(简单地带着大家走一遍,可能会有不准确的地方,请大家指出)先看前3个模块:


在第一个模块中,判断[PRLTableViewWrapper scrollViewDidEndDragging:]的参数,也就是发生滑动操作的这个view是不是[self tableView]。如果两者不相等,那么这个函数直接就return掉了,其他操作就都跳过去了,因此我们可以先不做验证,假设两者相等,这个函数会做哪些操作。到第二个模块,查看实例变量(instance variable,即ivar)supportPullRefresh的值,如果“支持拉动刷新”,则到第三个模块,查看实例变量enablePullRefresh的值,如果“可以拉动刷新”,则顺着绿色的线,前进至下图:


“支持拉动刷新”?是;“本函数参数为nil”?不是;到下图:


“拉动幅度够”?够;到下图:


“支持加载更多”?是;“可以加载更多”?是;到下图:


“参数为nil”?不是;到下图:


最下面是一个分支,它的判断操作在“VCMPE.F32 S16, S0”,貌似又是调用跟拉动幅度相关的一个函数baseContentInsetTop,比较其返回值跟另一个值的大小。如果你感兴趣,这个地方可以分析细致一些,这里我们就不纠结了,分别看看红绿两条线去哪。你会发现,绿色的线直接指向函数的结尾,而红色的线到达下图:


“正在加载?”不是;“触发加载操作”,然后返回!好了,虽然七拐八拐,但“加载更多”的函数还是没能逃过我们的魔爪,[PRLMTableViewWrapper triggerLoadMore]是也~

五、获取所有转发人名和转发内容
当持续调用“加载更多”后,所有的转发就都从服务器取回到了本地,显示在客户端中。那么,如何获取所有的转发微博呢?当然还是从数据源WBRepostListViewController入手啦!我们回过头来仔细看看WBRepostListViewController.h,如图:

看起来没什么东西,不像一个datasource啊!但是,WBRepostListViewController是继承自WBStatusBusinessViewController的,那WBStatusBusinessViewController.h里面有什么呢?


内容非常丰富,但我一眼就看到了一个函数,- (int)numberOfDataRows。数据一共有多少行?一个转发就是一行,难道这个数据代表了转发数?我们去Cycript里试试。


24?怎么这么少?在我继续验证时,我才意识到,这个数字显示的是当前已经完成加载的转发数。那么我们就多调用几次triggerLoadMore,加载所有转发吧:


在上图中,我们从WBRepostListViewController的一个实例,一步步地获取到了WBPRLMTableViewWrapper的实例,成功调用triggerLoadMore加载更多转发。每调用一次这个函数,iOS界面状态栏上的网络进度圈就会转一下,说明正在从服务器上获取更多数据。值得一提的是,[WBPRLMTableViewWrapper triggerLoadMore]其实是在完成一系列判断及界面操作后调用了[WBRepostListViewController triggerLoadMore]来连接服务器取数据,故在iOSRELottery的代码中直接使用了[WBRepostListViewController triggerLoadMore]的内部实现来获取全部转发。这个分析过程就不在此赘述了,有问题的朋友可以跟帖留言。


目前一共有327次转发,但在Cycript中调用[WBRepostListViewController numberOfDataRows],返回的数字却是328,如图所示:


为什么会多一次?因为numberOfDataRows并不是转发的数量,而是“数据”的数量——眼尖的朋友可能已经看到上上一张截图里的“相关微博”了,对,多出来的1,就是这条微博~
好了,转发数量我们可以拿到了,但转发的内容呢?我们在IDA中看看numberOfDataRows是怎么来的,如图:


[list count],而list是一个NSMutableArray *类型的实例变量。这么敏感的数据类型,这么明显的反汇编代码,我们把目标锁定这个可疑的实例变量list,所有的转发微博貌似就存放在其中。到底是不是呢?上Cycript验证。


仔细看可以发现,这个NSMutableArray中存放了大量的WBStatus,而WBStatus含有的信息也非常丰富,包括转发的内容(text)和一个WBUser实例,如图所示:


这个WBUser中含有我们需要的用户名(displayName),转发内容(text),和ta是不是我的粉丝(isFollower)。至此,我们收集到了所有需要的数据,可以开始写代码啦!

六、撰写代码
tweak工程的配置,Logos语法等等都可以从《iOS应用逆向工程》中找到详细的讲解,在这里就不废话了。能看到这里的,想必Objective-C语言都已经入门了,iOSRELottery的代码(配上我加的那些注释)应该是可以看懂的。如果还有不懂的,在下面留言咯!

结语
各种微博在功能上已没有区别,新浪微博的优势就在于其用户,如果这些用户数据被竞品拿走,造成的损失可想而知。但逆向工程不是山寨,更不是■■,而是一种透过现象看本质的问题解决思路。在本文中,我们从新浪微博客户端入手,盯住界面上的若干元素,通过Cycript和IDA的配合逐步挖掘出了它们的代码,还原了代码所处理的数据并简单利用。虽然我们的行为没有半点恶意,但新浪微博客户端最宝贵的内容已经毫无保留地展现在我们面前,凸显了iOS App潜在的安全隐患。相信随着时间的推移,大家的焦点从PC逐渐转换到智能移动终端,App的安全性一定会得到越来越多的重视。

顶!原来要登录才能看图

:smile::smile::smile::smile:顶,非常不错呀