8.2 搭建tweak原型

Mail的初始界面如图8-3所示。

把白名单按钮添加在哪个位置比较直观呢?在图8-3所示的All Inboxes界面中,可以看到,其左下角是空缺的,或许可以把按钮添加在这里,试试看效果吧,如图8-4所示。

虽然添加的白名单按钮与右下方的“编写”按钮在方位上对齐了,但前者是文字,后者是图标,形式不统一,不够美观,可见,左下角不适合添加文字按钮。如果改成一个图标按钮呢?可能也会有问题,因为“白名单”没有约定俗成的图形表达方式,找不到一个比较有代表性的图标来表示白名单,所以用图标按钮表示白名单不够直观。直观、美观不可兼得,看来在这个界面上不大适合添加白名单按钮。点击左上角的“Mailboxes”,去上一级界面看看,如图8-5所示。

图8-5所示的界面左上和左下均是空着的,其中左下不适合添加白名单按钮,刚才已经讨论过了。把按钮添加在左上看看效果,如图8-6所示。

图8-3 Mail初始界面

图8-4 在左下角添加白名单按钮

图8-5 Mailboxes界面

图8-6 在左上角添加白名单按钮

看起来效果还不错,就把白名单按钮放在这里吧!要达到图8-6所示的效果,只需要找到Mailboxes界面的controller,然后通过[controller.navigationItem setLeftBarButtonItem:]来添加白名单按钮就可以了。通过V找C的过程前面已经重复过很多遍,是可行的。搞定了按钮,接下来就是对白名单工作逻辑的梳理了,可以分为以下3步:

1)拿到所有邮件;

2)提取出它们的地址;

3)根据白名单决定是否将它们标记为已读。

下面来一步步分析,一起来思考:

·要如何才能拿到所有邮件呢?图8-3所示的界面可以下拉刷新收件箱,如图8-7所示。

图8-7 下拉刷新

在刷新的过程中,Mail会去邮件服务器上获取最新邮件;刷新完成后,界面恢复到图8-3所示的样子,此时收件箱里存放的就是所有邮件。如果能捕获“刷新完成”事件,然后读取收件箱,就可以拿到所有邮件了。因此,“拿到所有邮件”可以分为2步:一是捕获“刷新完成”事件;二是读取收件箱。其中,“刷新完成”的响应函数一般是定义在protocol里的,在分析class-dump头文件的时候,要留意各种protocol里有没有出现didRefresh、didUpdate、didReload之类名字中含有完成时态动词的函数,钩住(hook)它,然后寻找读取收件箱的方法,从而拿到所有邮件。

·一封邮件就是一个对象,它一般会用一个类来表示,从这个类中可以提取出邮件的收件人、发件人、标题、内容和是否已读等信息。如果能拿到邮件对象,就可以一石二鸟,完成后两步操作。

看上去整体思路并不复杂,下面就来各个击破。

8.2.1 定位Mail的可执行文件并class-dump它

通过ps命令很容易就能定位到Mail的可执行文件“/Applications/MobileMail.app/MobileMail”。因为MobileMail是iOS原生系统App,没有加壳,所以不需要砸壳,直接class-dump即可,如下:


snakeninnys-MacBook:~ snakeninny$ class-dump -S -s -H /Users/snakeninny/Code/iOSSystemBinaries/8.1.1_iPhone5/MobileMail.app/MobileMail -o /Users/snakeninny/Code/iOSPrivateHeaders/8.1.1/MobileMail

程序执行后,共得到393个头文件,如图8-8所示。

图8-8 class-dump头文件

8.2.2 把头文件导入Xcode

Xcode自带的查找功能和代码高亮显示能够较为美观整洁地展示大量头文件,如图8-9所示。

图8-9 把头文件导入Xcode

接下来,开始寻找线索,从App切入代码。

8.2.3 用Cycript找到Mailboxes界面的controller

首先用recursiveDescription打印出Mailboxes界面的UI布局,如下:


FunMaker-5:~ root# cycript -p MobileMail
cy# ?expand
expand == true
cy# [[UIApp keyWindow] recursiveDescription]
@"<UIWindow: 0x156bffe0; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x156bd390>; layer = <UIWindowLayer: 0x156c1be0>>
   | <UIView: 0x15611490; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x15618e70>; layer = <CALayer: 0x15611420>>
   |    | <UIView: 0x15611210; frame = (0 0; 320 568); layer = <CALayer: 0x15611280>>
   |    |    | <_MFActorItemView: 0x15614660; frame = (0 0; 320 568); layer = <CALayer: 0x15614840>>
   |    |    |    | <UIView: 0x156150f0; frame = (-0.5 -0.5; 321 569); alpha = 0; layer = <CALayer: 0x15615160>>
   |    |    |    | <_MFActorSnapshotView: 0x15614bb0; baseClass = UISnapshotView; frame = (0 0; 320 568); clipsToBounds = YES; hidden = YES; layer = <CALayer: 0x15614e00>>
   |    |    |    |    | <UIView: 0x15614f40; frame = (-1 -1; 322 570); layer = <CALayer: 0x15614fb0>>
   |    |    |    | <UILayoutContainerView: 0x1572ec40; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = LM+W+RM+TM+H+BM; layer = <CALayer: 0x1572ecc0>>
   |    |    |    |    | <UIView: 0x1683d890; frame = (0 0; 320 0); layer = <CALayer: 0x16848140>>
   |    |    |    |    | <UILayoutContainerView: 0x157246b0; frame = (0 0; 320 568); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x156088e0>; layer = <CALayer: 0x15724890>>
……
   |    |    |    |    |    |    |    |    |    | <MailboxTableCell: 0x1572ad50; baseClass = UITableViewCell; frame = (0 28; 320 44.5); autoresize = W; layer = <CALayer: 0x168299f0>>
   |    |    |    |    |    |    |    |    |    |    | <UITableViewCellContentView: 0x16829b70; frame = (0 0; 286 44); gestureRecognizers = <NSArray: 0x1682b060>; layer = <CALayer: 0x16829be0>>
   |    |    |    |    |    |    |    |    |    |    |    | <UILabel: 0x1682b0a0; frame = (55 12; 84.5 20.5); text = 'All Inboxes'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x1682b160>>
……

其中,最下方的这个UILabel上的文字是“All Inboxes”,其对应的MailboxTableCell自然就是图8-5中最上面的一个cell了。连续调用nextResponder,找出这个界面的controller,如下:


cy# [#0x1572ad50 nextResponder]
#"<UITableViewWrapperView: 0x1572fe60; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x15730370>; layer = <CALayer: 0x157301a0>; contentOffset: {0, 0}; contentSize: {320, 568}>"
cy# [#0x1572fe60 nextResponder]
#"<UITableView: 0x1585a000; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x1572fa20>; layer = <CALayer: 0x1572f540>; contentOffset: {0, -64}; contentSize: {320, 371}>"
cy# [#0x1585a000 nextResponder]
#"<MailboxPickerController: 0x156e9260>"

很轻松地拿到了MailboxPickerController。试试看用它能不能添加一个leftBarButtonItem,如下:


cy# #0x156e9260.navigationItem.leftBarButtonItem = #0x156e9260.navigationItem.rightBarButtonItem
#"<UIBarButtonItem: 0x15729f00>"

效果如图8-10所示。

图8-10 setLeftBarButtonItem:的效果

没有问题,MailboxPickerController就是Mailboxes界面的controller,可以通过它添加白名单按钮。

8.2.4 用Reveal和Cycript找到All Inboxes界面的delegate

搞定了白名单按钮,就要开始梳理白名单的工作逻辑了,先看看如何捕获“刷新完成”事件。因为“刷新完成”是直观表现在All Inboxes界面上的,所以“刷新完成”的响应函数很可能定义在这个界面的delegate中。转战到图8-3所示的All Inboxes界面,这里不再重复8.2.3节的方法,而是先通过Reveal定位这个界面的cell,再用Cycript找出它所在的UITableView,从而得知其delegate。

下面就用Reveal查看Mail,很容易就可以定位到最上方的那个cell,如图8-11所示。

图8-11 用Reveal查看Mail的UI布局

MailboxContentViewCell就是显示邮件发件人、标题和摘要的cell。接着用Cycript找出它所在的UITableView:因为我们知道当前界面一定存在MailboxContentViewCell对象,所以可以尝试通过choose命令获取这些对象,而不用劳烦recursiveDescription她老人家了,如下:


FunMaker-5:~ root# cycript -p MobileMail
cy# choose(MailboxContentViewCell)
[#"<MailboxContentViewCell: 0x161f4000> cellContent",#"<MailboxContentViewCell: 0x1621c400> cellContent",#"<MailboxContentViewCell: 0x1621d000> cellContent",#"<MailboxContentViewCell: 0x16234800> cellContent",#"<MailboxContentViewCell: 0x1623ee00> cellContent",#"<MailboxContentViewCell: 0x1623f200> cellContent",#"<MailboxContentViewCell: 0x159c2c00> cellContent"]

choose命令返回了一个由MailboxContentViewCell对象组成的NSArray,从中随便挑选一个MailboxContentViewCell对象,对其连续调用nextResponder,如下:


cy# [choose(MailboxContentViewCell)[0] nextResponder]
#"<UITableViewWrapperView: 0x15660b80; frame = (0 0; 320 612); gestureRecognizers = <NSArray: 0x16855170>; layer = <CALayer: 0x16888f20>; contentOffset: {0, 0}; contentSize: {320, 612}>"
cy# [#0x15660b80 nextResponder]
#"<MFMailboxTableView: 0x16095000; baseClass = UITableView; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x15607850>; layer = <CALayer: 0x16838210>; contentOffset: {0, -64}; contentSize: {320, 52364}>"

它所在的UITableView是一个MFMailboxTableView对象,看看它的delegate是什么,如下:


cy# [#0x16095000 delegate]
#"<MailboxContentViewController: 0x16106000>"

它的delegate是MailboxContentViewController。继续调用nextResponder,看看它的controller又是什么,如下:


cy# [#0x16095000 nextResponder]
#"<MailboxContentViewController: 0x16106000>"

也就是说,MFMailboxTableView的controller和delegate均是MailboxContentView-Controller。简单验证一下controller的正确性,如下:


cy# [#0x16106000 setTitle:@"iOSRE"]

效果如图8-12所示。

图8-12 setTitle:的效果

这个结果说明上述一系列推导没有问题,MailboxContent-ViewController中很可能既含有“刷新完成”的响应函数,又能找到读取收件箱的蛛丝马迹,接下来就把焦点放在它身上了。

8.2.5 在MailboxContentViewController中定位“刷新完成”的响应函数

与第7章一样,先来看看MailboxContentViewController实现了哪些protocol,能不能在其中找到可疑的响应函数,如下:


@interface MailboxContentViewController : UIViewController <MailboxContentSelectionModelDataSource, MFSearchTextParserDelegate, MessageMegaMallObserver, MFAddressBookClient, MFMailboxTableViewDelegate, UIPopoverPresentationControllerDelegate, UITableViewDelegate, UITableViewDataSource, UISearchDisplayDelegate, UISearchBarDelegate, TransferMailboxPickerDelegate, AutoFetchControllerDataSource>

先从名字上做一次简单的排查,MFSearchTextParserDelegate、MFAddressBookClient、UIPopoverPresentationControllerDelegate、UITableViewDelegate、UITableViewDataSource、UISearchDisplayDelegate、UISearchBarDelegate看起来跟“刷新完成”没什么关系,可以直接排除。剩下的MailboxContentSelectionModelDataSource、MessageMegaMallObserver、MFMailboxTableViewDelegate、TransferMailboxPickerDelegate和AutoFetchControllerDataSource还不好说,那就挨个过一遍。先看MailboxContentSelectionModelDataSource.h,内容如下:


@protocol MailboxContentSelectionModelDataSource <NSObject>
- (BOOL)selectionModel:(id)arg1 deleteMovesToTrashForTableIndexPath:(id)arg2;
- (void)selectionModel:(id)arg1 getConversationStateAtTableIndexPath:(id)arg2 hasUnread:(char *)arg3 hasUnflagged:(char *)arg4;
- (void)selectionModel:(id)arg1 getSourceStateHasUnread:(char *)arg2 hasUnflagged:(char *)arg3;
- (id)selectionModel:(id)arg1 indexPathForMessageInfo:(id)arg2;
- (id)selectionModel:(id)arg1 messageInfosAtTableIndexPath:(id)arg2;
- (id)selectionModel:(id)arg1 messagesForMessageInfos:(id)arg2;
- (BOOL)selectionModel:(id)arg1 shouldArchiveByDefaultForTableIndexPath:(id)arg2;
- (id)selectionModel:(id)arg1 sourceForMessageInfo:(id)arg2;
- (BOOL)selectionModel:(id)arg1 supportsArchivingForTableIndexPath:(id)arg2;
- (id)sourcesForSelectionModel:(id)arg1;
@end

这个protocol的作用看上去是读取数据源,跟“刷新数据源”没太大关系。接着看MessageMegaMallObserver.h,内容如下:


@protocol MessageMegaMallObserver <NSObject>
- (void)megaMallCurrentMessageRemoved:(id)arg1;
- (void)megaMallDidFinishSearch:(id)arg1;
- (void)megaMallDidLoadMessages:(id)arg1;
- (void)megaMallFinishedFetch:(id)arg1;
- (void)megaMallGrowingMailboxesChanged:(id)arg1;
- (void)megaMallMessageCountChanged:(id)arg1;
- (void)megaMallMessagesAtIndexesChanged:(id)arg1;
- (void)megaMallStartFetch:(id)arg1;
@end

这个类中的不少函数名含有完成时态动词,同时,从“LoadMessages”、“FinishedFetch”、“MessageCountChanged”等函数的名字上来看,它可能会在刷新完成的前后得到调用。接下来用LLDB在这3个函数的开头部分下断点,然后下拉刷新收件箱,看看它们的调用情况。首先用LLDB附加MobileMail,查看其ASLR偏移,如下:


(lldb) image list -o -f
[  0] 0x000b2000/private/var/db/stash/_.lnBgU8/Applications/MobileMail.app/MobileMail(0x00000000000b6000)
[   1] 0x003b7000/Library/MobileSubstrate/MobileSubstrate.dylib(0x00000000003b7000)
[   2] 0x090d1000/Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/usr/lib/libarchive.2.dylib
[  3] 0x090c3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1.1 (12B435)/Symbols/System/Library/Frameworks/CloudKit.framework/CloudKit
……

可以看到,ASLR偏移是0x000b2000。然后把MobileMail拖进IDA,待初始分析完成后,查看[MailboxContentViewController megaMallDidLoadMessages:]、[MailboxContentViewControllermegaMallFinishedFetch:]和[MailboxContentViewController megaMallMessageCountChanged:]的基地址,如图8-13、图8-14和图8-15所示。

图8-13 [MailboxContentViewController megaMallDidLoadMessages:]

图8-14 [MailboxContentViewController megaMallFinishedFetch:]

图8-15 [MailboxContentViewController megaMallMessageCountChanged:]

它们的基地址分别是0x3dce0、0x3d860和0x3de48。用LLDB在这些地址上下断点,然后下拉刷新,触发断点,如下:


(lldb) br s -a '0x000b2000+0x3dce0'
Breakpoint 1: where = MobileMail`___lldb_unnamed_function992$$MobileMail, addrss = 0x000efce0
(lldb) br s -a '0x000b2000+0x3d860'
Breakpoint 2: where = MobileMail`___lldb_unnamed_function987$$MobileMail, addrss = 0x000ef860
(lldb) br s -a '0x000b2000+0x3de48'
Breakpoint 3: where = MobileMail`___lldb_unnamed_function993$$MobileMail, addrss = 0x000efe48

可能有读者在这里会碰到跟笔者相同的情况:三个断点一个也没有触发。从事过网络编程的朋友可能会猜到原因——为了减轻邮件服务器的负担,节省iOS流量,并不是每次下拉刷新都会去服务器取数据。如果刷新时间间隔不长,收件箱的数据源就会是本地缓存,不会调用MessageMegaMallObserver中的方法。为了验证这个猜测,我们往自己的邮箱发一封邮件,然后下拉刷新,看看断点触发情况,如下:


Process 73130 stopped
* thread #44: tid = 0x14c10, 0x000ef860 MobileMail`___lldb_unnamed_function987$$ MobileMail, stop reason = breakpoint 2.1
    frame #0: 0x000ef860 MobileMail`___lldb_unnamed_function987$$MobileMail
MobileMail`___lldb_unnamed_function987$$MobileMail:
-> 0xef860:  push   {r7, lr}
   0xef862:  mov    r7, sp
   0xef864:  sub    sp, #24
   0xef866:  movw   r1, #44962
(lldb) c
Process 73130 resuming
Process 73130 stopped
* thread #44: tid = 0x14c10, 0x000ef860 MobileMail`___lldb_unnamed_function987$$ MobileMail, stop reason = breakpoint 2.1
    frame #0: 0x000ef860 MobileMail`___lldb_unnamed_function987$$MobileMail
MobileMail`___lldb_unnamed_function987$$MobileMail:
-> 0xef860:  push   {r7, lr}
   0xef862:  mov    r7, sp
   0xef864:  sub    sp, #24
   0xef866:  movw   r1, #44962
(lldb) c
Process 73130 resuming
Process 73130 stopped
* thread #1: tid = 0x11daa, 0x000efe48 MobileMail`___lldb_unnamed_function993$$ MobileMail, queue = 'MessageMiniMall.0x157c2d90, stop reason = breakpoint 3.1
    frame #0: 0x000efe48 MobileMail`___lldb_unnamed_function993$$MobileMail
MobileMail`___lldb_unnamed_function993$$MobileMail:
-> 0xefe48:  push   {r4, r5, r6, r7, lr}
   0xefe4a:  add    r7, sp, #12
   0xefe4c:  push.w {r8, r10, r11}
   0xefe50:  sub.w  r4, sp, #24
(lldb) 
Process 73130 resuming
Process 73130 stopped
* thread #1: tid = 0x11daa, 0x000efe48 MobileMail`___lldb_unnamed_function993$$ MobileMail, queue = 'MessageMiniMall.0x157c2d90, stop reason = breakpoint 3.1
    frame #0: 0x000efe48 MobileMail`___lldb_unnamed_function993$$MobileMail
MobileMail`___lldb_unnamed_function993$$MobileMail:
-> 0xefe48:  push   {r4, r5, r6, r7, lr}
   0xefe4a:  add    r7, sp, #12
   0xefe4c:  push.w {r8, r10, r11}
   0xefe50:  sub.w  r4, sp, #24
(lldb) 
Process 73130 resuming
Process 73130 stopped
* thread #44: tid = 0x14c10, 0x000ef860 MobileMail`___lldb_unnamed_function987$$ MobileMail, stop reason = breakpoint 2.1
    frame #0: 0x000ef860 MobileMail`___lldb_unnamed_function987$$MobileMail
MobileMail`___lldb_unnamed_function987$$MobileMail:
-> 0xef860:  push   {r7, lr}
   0xef862:  mov    r7, sp
   0xef864:  sub    sp, #24
   0xef866:  movw   r1, #44962
(lldb) c
Process 73130 resuming

果不其然,megaMallFinishedFetch:和megaMallMessageCountChanged:被交替调用。从名字上来看,一封邮件就是一个message,megaMallFinishedFetch:应该会在iOS成功地从服务器取回邮件之后得到调用,而megaMallMessageCountChanged:应该会在邮件数量发生变动,即收邮件和删邮件时得到调用,两者自然都会在“刷新完成”时得到调用,都可以看作“刷新完成”的响应函数。两者随便选其一,这里选择megaMallMessageCountChanged:,接下来的任务是寻找拿到所有邮件的方法。

8.2.6 从MessageMegaMall中拿到所有邮件

还记得第7章中说过的“协议方法被调用,一般是因为方法名中提到的那个事件发生了;而那件事发生的对象,一般是协议方法的参数”吗?删掉前2个断点,保留第3个,也就是megaMallMessageCountChanged:上的断点,看看它的参数是什么,如下:


Process 73130 stopped
* thread #1: tid = 0x11daa, 0x000efe48 MobileMail`___lldb_unnamed_function993$$ MobileMail, queue = 'MessageMiniMall.0x157c2d90, stop reason = breakpoint 3.1
    frame #0: 0x000efe48 MobileMail`___lldb_unnamed_function993$$MobileMail
MobileMail`___lldb_unnamed_function993$$MobileMail:
-> 0xefe48:  push   {r4, r5, r6, r7, lr}
   0xefe4a:  add    r7, sp, #12
   0xefe4c:  push.w {r8, r10, r11}
   0xefe50:  sub.w  r4, sp, #24
(lldb) po $r2
NSConcreteNotification 0x157e8af0 {name = MegaMallMessageCountChanged; object = <MessageMegaMall: 0x1576c320>; userInfo = {
    "added-message-infos" =     (
        "<MFMessageInfo: 0x157c86d0> uid=1185, conversation=2777228998582613276"
    );
    destination = "{(\n)}";
    inserted = "{(\n    <NSIndexPath: 0x157e8ac0> {length = 2, path = 0 - 0}\n)}";
    relocated = "{(\n)}";
    updated = "{(\n)}";
}}

可以看到,参数是一个NSConcreteNotification对象。查看其头文件,可知它继承自NSNotification。它的name是MegaMallMessageCountChanged,object是一个MessageMegaMall对象,userInfo是一些改动信息。“MegaMall”这个名字很值得玩味,“大型购物中心”,看似与邮件毫不相关,却又与“Message”寸步不离,与8.2.4节分析的MessageMegaMallObserver遥相呼应,疑似为一个存储“Message”的类。打开MessageMegaMall.h,看看它的内容,如下:


@interface MessageMegaMall : NSObject <MessageMiniMallObserver, Message SelectionDataSource>
……
- (id)copyAllMessages;
@property (retain, nonatomic) MFMailMessage *currentMessage;
- (void)loadOlderMessages;
- (unsigned int)localMessageCount;
- (unsigned int)messageCount;
- (void)markAllMessagesAsNotViewed;
- (void)markAllMessagesAsViewed;
- (void)markMessagesAsNotViewed:(id)arg1;
- (void)markMessagesAsViewed:(id)arg1;
……
@end

线索有些明朗了:复制所有邮件、当前邮件、读取早期邮件、本地邮件计数、邮件计数、标为已读……MessageMegaMall应该就是一个管理所有邮件对象的M,它被苹果形象地比喻为“大型购物中心”。那么到底能不能通过copyAllMessages拿到所有的邮件呢?在LLDB里试一下,如下:


Process 73130 stopped
* thread #1: tid = 0x11daa, 0x000efe48 MobileMail`___lldb_unnamed_function993$$ MobileMail, queue = 'MessageMiniMall.0x157c2d90, stop reason = breakpoint 3.1
    frame #0: 0x000efe48 MobileMail`___lldb_unnamed_function993$$MobileMail
MobileMail`___lldb_unnamed_function993$$MobileMail:
-> 0xefe48:  push   {r4, r5, r6, r7, lr}
   0xefe4a:  add    r7, sp, #12
   0xefe4c:  push.w {r8, r10, r11}
   0xefe50:  sub.w  r4, sp, #24
(lldb) po [[$r2 object] copyAllMessages]
{(
    <MFLibraryMessage 0x15612030: library id 89, remote id 13020, 2014-11-25 20:32:16 +0000, 'Cydia/APT(A): LowPowerBanner (1.4.5)'>,
    <MFLibraryMessage 0x1572ef10: library id 604, remote id 12718, 2014-10-01 21:34:28 +0000, 'Asian Morning: Told to End Protests, Organizers in Hong Kong Vow to Expand Them'>,
    <MFLibraryMessage 0x168bd170: library id 906, remote id 13142, 2014-12-17 22:34:30 +0000, 'Asian Morning: Obama Announces U.S. and Cuba Will Resume Relations'>,
……
)}
(lldb) p (int)[[[$r2 object] copyAllMessages] count]
(int) $7 = 580
(lldb) p (int)[[$r2 object] localMessageCount]
(int) $8 = 580
(lldb) p (int)[[$r2 object] messageCount]
(int) $0 = 553
(lldb) po [[[$r2 object] copyAllMessages] class]
__NSSetM

copyAllMessages返回了一个NSSet,其中含有580个MFLibraryMessage对象,MFLibraryMessage对象中含有邮件摘要信息,且NSSet中对象的个数与localMessageCount的值相同。这个结果很好理解:为了节省带宽流量和本地空间,iOS没有必要一次性下载邮件服务器上的所有邮件,因此会先存储个百十来封,用户如果要看更多的邮件,再去服务器获取(即loadOlderMessages)。因此,copyAllMessages就是拿到所有邮件的方法,第二目标达成!同时,留意[MessageMegaMall markMessagesAsViewed:]函数,如果不出意外,它就是把邮件标记为已读的方法,而参数则很有可能是一个含有MFLibraryMessage对象的NSArray或NSSet。到底是不是这样呢?我们马上就会验证。

8.2.7 从MFLibraryMessage中提取发件人地址,用MessageMegaMall标记已读

从8.2.4节的分析可知,一封邮件就是一个MFLibraryMessage对象,它的description里显示的正是邮件摘要。不过,在MobileMail的头文件中是找不到它的身影的,想必你也能猜到大致原因——MFLibraryMessage来自一个外部dylib。在iOS 8的所有class-dump头文件里搜索MFLibraryMessage,发现它来自Messages私有库,如图8-16所示。

看看MFLibraryMessage.h的内容,如下:


@interface MFLibraryMessage : MFMailMessage
……
- (id)copyMessageInfo;
……
- (void)markAsNotViewed;
- (void)markAsViewed;
- (id)account;
……
- (unsigned long long)uniqueRemoteId;
- (unsigned long)uid;
- (unsigned int)hash;
- (id)remoteID;
- (void)_updateUID;
- (unsigned int)messageSize;
- (id)originalMailboxURL;
- (unsigned int)originalMailboxID;
- (unsigned int)mailboxID;
- (unsigned int)libraryID;
- (id)persistentID;
- (id)messageID;
@end

图8-16 定位MFLibraryMessage

MFLibraryMessage.h里充斥着各种ID,但没有我们要找的发件人地址等信息。这个结果不正常:我们已经在MFLibraryMessage的description里看到了邮件摘要信息,却没有在MFLibraryMessage.h里找到读取摘要信息的方法,说明分析过程有遗漏。重新审视MFLibraryMessage.h,这时,copyMessageInfo进入了我们的视线,看看它返回的“邮件信息”里会有什么数据,如下:


Process 73130 stopped
* thread #1: tid = 0x11daa, 0x000efe48 MobileMail`___lldb_unnamed_function993$$ MobileMail, queue = 'MessageMiniMall.0x157c2d90, stop reason = breakpoint 3.1
    frame #0: 0x000efe48 MobileMail`___lldb_unnamed_function993$$MobileMail
MobileMail`___lldb_unnamed_function993$$MobileMail:
-> 0xefe48:  push   {r4, r5, r6, r7, lr}
   0xefe4a:  add    r7, sp, #12
   0xefe4c:  push.w {r8, r10, r11}
   0xefe50:  sub.w  r4, sp, #24
 (lldb) po [[[[$r2 object] copyAllMessages] anyObject] copyMessageInfo]
<MFMessageInfo: 0x157c8040> uid=89, conversation=594030790676622907

从中拿到了一个8.2.5节出现过的MFMessageInfo对象,马上去看看MFMessageInfo.h里有没有邮件摘要信息,如下:


@interface MFMessageInfo : NSObject
{
    unsigned int _flagged:1;
    unsigned int _read:1;
    unsigned int _deleted:1;
    unsigned int _uidIsLibraryID:1;
    unsigned int _hasAttachments:1;
    unsigned int _isVIP:1;
    unsigned int _uid;
    unsigned int _dateReceivedInterval;
    unsigned int _dateSentInterval;
    unsigned int _mailboxID;
    long long _conversationHash;
    long long _generationNumber;
}
+ (long long)newGenerationNumber;
@property(readonly, nonatomic) long long generationNumber; // @synthesize generationNumber=_generationNumber;
@property(nonatomic) unsigned int mailboxID; // @synthesize mailboxID=_mailboxID;
@property(nonatomic) long long conversationHash; // @synthesize conversationHash= _conversationHash;
@property(nonatomic) unsigned int dateSentInterval; // @synthesize dateSentInterval=_ dateSentInterval;
@property(nonatomic) unsigned int dateReceivedInterval; // @synthesize dateReceivedInterval=_ dateReceivedInterval;
@property(nonatomic) unsigned int uid; // @synthesize uid=_uid;
- (id)description;
- (unsigned int)hash;
- (BOOL)isEqual:(id)arg1;
- (int)generationCompare:(id)arg1;
- (id)initWithUid:(unsigned int)arg1 mailboxID:(unsigned int)arg2 dateReceivedInterval:(unsigned int)arg3 dateSentInterval:(unsigned int)arg4 conversationHash:(long long)arg5 read:(BOOL)arg6 knownToHaveAttachments:(BOOL)arg7 flagged:(BOOL)arg8 isVIP:(BOOL)arg9;
- (id)init;
@property(nonatomic) BOOL isVIP;
@property(nonatomic, getter=isKnownToHaveAttachments) BOOL knownToHave Attachments;
@property(nonatomic) BOOL uidIsLibraryID;
@property(nonatomic) BOOL deleted;
@property(nonatomic) BOOL flagged;
@property(nonatomic) BOOL read;
@end

MFMessageInfo中含有已读信息,但不含有邮件摘要信息,说明分析仍不够严密。再回过头仔细观察MFLibraryMessage.h,发现它继承自MFMailMessage,从名字上看,MailMessage用来代表邮件显然比LibraryMessage更贴切。打开MFMailMessage.h,看看它的内容,如下:


@interface MFMailMessage : MFMessage
……
- (BOOL)shouldSetSummary;
- (void)setSummary:(id)arg1;
- (void)setSubject:(id)arg1 to:(id)arg2 cc:(id)arg3 bcc:(id)arg4 sender:(id)arg5 dateReceived:(double)arg6 dateSent:(double)arg7 messageIDHash:(long long)arg8 conversationIDHash:(long long)arg9 summary:(id)arg10 withOptions:(unsigned int)arg11;
- (id)subject;
@end

summary、subject、sender、cc、bcc等邮件常用词汇出现在我们面前,但除了subject,MFMailMessage.h中只出现了setter,而不见getter。还记得刚才我们的注意力是怎么从MFLibraryMessage.h转移到MFMailMessage.h上的吗?想必你一定也注意到了MFMailMessage的父类MFMessage。在查看它的头文件前,先用LLDB看看[MFMailMessage subject]的返回,验证一下到目前为止的分析,如下:


Process 73130 stopped
* thread #1: tid = 0x11daa, 0x000efe48 MobileMail`___lldb_unnamed_function993$$MobileMail, queue = 'MessageMiniMall.0x157c2d90, stop reason = breakpoint 3.1
    frame #0: 0x000efe48 MobileMail`___lldb_unnamed_function993$$MobileMail
MobileMail`___lldb_unnamed_function993$$MobileMail:
-> 0xefe48:  push   {r4, r5, r6, r7, lr}
   0xefe4a:  add    r7, sp, #12
   0xefe4c:  push.w {r8, r10, r11}
   0xefe50:  sub.w  r4, sp, #24 
(lldb) po [[[[$r2 object] copyAllMessages] anyObject] subject]
Asian Morning: Told to End Protests, Organizers in Hong Kong Vow to Expand Them

可以看到,[MFMailMessage subject]返回的正是邮件的标题。打开MFMessage.h(注意,MFMessage是MIME.framework里的类),看看它的内容,如下:


@interface MFMessage : NSObject <NSCopying>
……
- (id)headerData;
- (id)bodyData;
- (id)summary;
- (id)bccIfCached;
- (id)bcc;
- (id)ccIfCached;
- (id)cc;
- (id)toIfCached;
- (id)to;
- (id)firstSender;
- (id)sendersIfCached;
- (id)senders;
- (id)dateSent;
- (id)subject;
- (id)messageData;
- (id)messageBody;
- (id)headers;
……
@end

其中,邮件的收件人、发件人、标题、内容等信息一应俱全。用LLDB简单看看它们的返回值,如下:


Process 73130 stopped
* thread #1: tid = 0x11daa, 0x000efe48 MobileMail`___lldb_unnamed_function993$$MobileMail, queue = 'MessageMiniMall.0x157c2d90, stop reason = breakpoint 3.1
    frame #0: 0x000efe48 MobileMail`___lldb_unnamed_function993$$MobileMail
MobileMail`___lldb_unnamed_function993$$MobileMail:
-> 0xefe48:  push   {r4, r5, r6, r7, lr}
   0xefe4a:  add    r7, sp, #12
   0xefe4c:  push.w {r8, r10, r11}
   0xefe50:  sub.w  r4, sp, #24 
 (lldb) po [[[[$r2 object] copyAllMessages] anyObject] firstSender]
NYTimes.com <nytdirect@nytimes.com>
(lldb) po [[[[$r2 object] copyAllMessages] anyObject] sendersIfCached]
<__NSArrayI 0x16850850>(
NYTimes.com <nytdirect@nytimes.com>
)
(lldb) po [[[[$r2 object] copyAllMessages] anyObject] senders]
<__NSArrayI 0x16850850>(
NYTimes.com <nytdirect@nytimes.com>
)
(lldb) po [[[[$r2 object] copyAllMessages] anyObject] to]
<__NSArrayI 0x16850840>(
snakeninny@gmail.com
)
(lldb) po [[[[$r2 object] copyAllMessages] anyObject] dateSent]
2014-10-01 21:30:32 +0000
(lldb) po [[[[$r2 object] copyAllMessages] anyObject] subject]
Asian Morning: Told to End Protests, Organizers in Hong Kong Vow to Expand Them
(lldb) po [[[[$r2 object] copyAllMessages] anyObject] messageBody]
<MFMimeBody: 0x16852fc0> 

打印的信息含义都很明显,想必不用解释了。其中,firstSender返回了一个发件人,而sendersIfCached和senders都返回了一个NSArray,说明默认情况下,在iOS中一封邮件是有可能存在多个发件人的。尽管多个发件人的情况在现实生活中不常见(笔者没见过),但为了避免遗漏,这里仍采用senders函数来提取一封邮件中所有可能的发件人地址。最后的任务就是把邮件标记为已读——还记得8.2.5节末尾出现的[MessageMegaMall markMessagesAsViewed:]吗?它是不是把邮件标为已读的方法呢?在这个方法上下一个断点,看看在把一封邮件标记为已读时,它会不会得到调用。

先在IDA里定位到[MessageMegaMall markMessagesAsViewed:],看看它的基地址,如图8-17所示。

图8-17 [MessageMegaMall markMessagesAsViewed:]

它的基地址是0x13b648。因为已知MobileMail的ASLR偏移是0xb2000,所以可以直接下断点,如下:


(lldb) br s -a '0x000b2000+0x0013B648'
Breakpoint 4: where = MobileMail`___lldb_unnamed_function7357$$MobileMail, address = 0x001ed648
Process 103910 stopped
* thread #1: tid = 0x195e6, 0x001ed648 MobileMail`___lldb_unnamed_function7357$$MobileMail, queue = 'com.apple.main-thread, stop reason = breakpoint 4.1
    frame #0: 0x001df648 MobileMail`___lldb_unnamed_function7357$$MobileMail
MobileMail`___lldb_unnamed_function7357$$MobileMail:
-> 0x1ed648:  push   {r4, r5, r6, r7, lr}
   0x1ed64a:  add    r7, sp, #12
   0x1ed64c:  str    r8, [sp, #-4]!
   0x1ed650:  mov    r8, r0
(lldb) po $r2
{(
    <MFLibraryMessage 0x157b70b0: library id 906, remote id 13142, 2014-12-17 22:34:30 +0000, 'Asian Morning: Obama Announces U.S. and Cuba Will Resume Relations'>
)}
(lldb) po [$r2 class]
__NSSetI

LLDB的输出验证了之前的猜测,[MessageMegaMall markMessagesAsViewed:]就是把邮件标为已读的方法,且其参数是一个由MFLibraryMessage对象组成的NSSet。至此,我们成功地在界面上添加了白名单按钮,捕获到了“刷新完成”事件,拿到了所有邮件,提取了其中的发件人地址,并能将它们标为已读。tweak原型搭建完毕,在写代码前,先整理一下逆向结果。