9.2 搭建tweak原型

9.2.1 观察小视频播放窗口,寻找逆向切入点

首先在“微信”→“Me”→“Settings”→“General”→“Sights in Moments”中,将小视频的自动播放选项调整到“Never”,如图9-4所示。

图9-4 调整自动播放选项

再看一下图9-3所示的界面,长按小视频播放窗口,会弹出“Favorite”和“Report Abuse”两个选项。这个现象说明播放窗口已经可以响应长按手势,现在只要找到长按手势对应的函数,钩住(hook)它,就可以添加含有“保存到本地”和“复制URL”两个选项的自定义菜单了。

小视频播放窗口的播放按钮下有一行字,“Tap to download”,也就是说微信会先下载小视频到iOS中,再离线播放。这一现象说明小视频模块里本来就含有一个下载URL,和一个下载好的视频文件,要达到目标,只需通过逆向工程找到这个URL和视频文件就行了。经过前几章的洗礼,相信读者对MVC的理解一定比开发App更深入了,如果能够拿到小视频的V,那么含有URL和视频对象的M就近在咫尺了。

好了,本章的目标功能已经被微信实现,只需要找到它们在微信中的位置,拿来为我们所用就好了,没有必要重新发明轮子。为了追求性价比,用尽可能少的逆向工程达到目的,我们不会过分严格地推导微信的逻辑,而是尽可能地在通过class-dump导出的头文件中寻找关键字,然后用其他工具配合验证猜测,最终达到提取小视频信息的目的。

9.2.2 class-dump获取头文件

首先用dumpdecrypted给微信砸壳,过程比较简单,这里就不细致描述了。值得一提的是,微信的可执行文件名既不叫“WeiXin”,也不叫“WeChat”,而是叫“MicroMessenger”。拿到脱壳后的可执行文件时,先把它丢到IDA里开始分析,然后用class-dump导出它的头文件,如下:


snakeninnysiMac:~ snakeninny$ class-dump –S –s -H ~/MicroMessenger -o ~/header6.0

执行上述命令,发现一共生成了5225个头文件,如图9-5所示。

微信算是笔者见过的头文件数目最多的App了,5000+的数目如果真要让咱们一个个过,那得看到猴年马月去。即使是正向开发,这种级别的工程量也不大可能由一个团队单独完成——估计腾讯内部是把微信拆分成了若干模块,比如朋友圈是一个模块,IM是一个模块,漂流瓶是一个模块,小视频是一个模块,然后分别组建团队编写各自负责的模块,最后整合成了一个大工程,通过这样的分工协作最终实现了微信这样一个App。

图9-5 微信6.0头文件

9.2.3 把头文件导入Xcode

把微信的头文件导入一个空的Xcode工程中,如图9-6所示。

图9-6 把头文件导入Xcode

Xcode自带的查找功能和代码高亮显示能够较为美观整洁地展示大量头文件。接下来,我们开始寻找线索,从App切入代码。

9.2.4 用Reveal找到小视频播放窗口

配置Reveal查看微信的方法也很简单,此处不再赘述。启动微信并进入朋友圈,找一个小视频,用Reveal看看当前的UI布局,如图9-7所示。

图9-7 用Reveal查看微信UI布局

在图9-7中一眼就能看到左侧的树形结构图中出现了“LLBean shirt with nice fabric”的字眼,与UI中显示的文字吻合。继续查看这个RichTextView附近的view,很容易就可以定位小视频播放窗口,如图9-8所示。

图9-8 找到小视频播放窗口

小视频的播放窗口是一个WCContentItemViewTemplateNewSight对象。还记得前面在recursiveDescription部分讲过的缩进原则吗?依照“缩进多的view是缩进少的view的subview”的原则,可分析出WCContentItemViewTemplateNewSight的subview有WCSightView等,而WCSightView的subview则有UIImageView和SightPlayerView等。因为微信小视频的英文名就叫“sight”,所以这几个类是重点关注对象。

9.2.5 找到长按手势响应函数

要在iOS中添加长按手势,一般是通过addGestureRecognizer:方法来实现的,既然长按小视频播放窗口会弹出菜单,那么长按手势很有可能是直接添加在小视频播放窗口上的。这个播放窗口是一个WCContentItemViewTemplateNewSight对象,看看它的头文件里有些什么,如下:


@interface WCContentItemViewTemplateNewSight : WCContentItemBaseView <WCAction Sheet Delegate, SessionSelectControllerDelegate, WCSightView Delegate>
……
- (void)onMore:(id)arg1;
- (void)onFavoriteAdd:(id)arg1;
- (void)onLongTouch;
- (void)onShowSightAction;
- (void)onLongPressedWCSightFullScreenWindow:(id)arg1;
- (void)onLongPressedWCSight:(id)arg1;
- (void)onClickWCSight:(id)arg1;
……
@end

在上面的代码中,那几个含有“LongTouch”、“LongPressed”字眼的函数很可能就是我们寻找的长按手势响应函数。IDA对微信的初始分析应该已经结束了,在IDA里瞟一眼这几个函数都做了些什么,先看看onLongTouch,如图9-9所示。

图9-9 onLongTouch

这个函数的流程非常简单,从上往下浏览,很容易就可以看到“UIMenuController”的字眼,如图9-10所示。

图9-10 onLongTouch(1)

也可以看到“Favorite”的字眼,如图9-11所示。

图9-11 onLongTouch(2)

除非这些字眼都是微信有意拿来迷惑我们的,否则这个[WCContentItemViewTemplate-NewSight onLongTouch]十有八九就是要找的长按手势响应函数。先不着急下结论,把带有“LongPressed”关键字的函数也浏览一遍,如图9-12所示。

图9-12 onLongPressedWCSightFullScreenWindow:

看上去是记录了一些信息,然后调用了onShowSightAction。接下来看看onShowSight-Action都做了些什么,如图9-13所示。

从图9-13可以看到,这个函数的一开始就创建了一个WCActionSheet对象,既然名字是ActionSheet,它的表现形式可能与UIActionSheet差不多,而我们在长按小视频播放窗口根本没看到与UIActionSheet类似的效果出现,因此可以判断onLongPressedWCSightFull-ScreenWindow:可能不是我们要找的函数。

接着看最后一个函数,onLongPressedWCSight:,如图9-14所示。

从图9-14可以看到,它记录了一些信息,然后调用了onLongTouch,这从侧面印证了我们的猜测。接下来,请出LLDB,测测onLongPressedWCSightFullScreenWindow:和onLongTouch的调用情况。先用debugserver附加MicroMessenger,如下:

图9-13 onShowSightAction

图9-14 onLongPressedWCSight:


snakeninnysiMac:Documents snakeninny$ ssh root@localhost -p 2222
FunMaker-5:~ root# debugserver *:1234 -a MicroMessenger
debugserver-@(#)PROGRAM:debugserver  PROJECT:debugserver-320.2.89
 for armv7.
Attaching to process MicroMessenger...
Listening to port 1234 for a connection from *...
Waiting for debugger instructions for process 0.

然后看看微信的ASLR偏移,如下:


(lldb) image list -o -f
[  0] 0x00000000 /private/var/mobile/Containers/Bundle/Application/E4EBD049-1A75-4830-BC65-0132C0EBC1CA/MicroMessenger.app/MicroMessenger(0x0000000000004000)
[  1] 0x022dc000 /Library/MobileSubstrate/MobileSubstrate.dylib(0x00000000022dc000)
……

微信的ASLR偏移是0。接着看看onLongPressedWCSightFullScreenWindow:和onLong Touch的基地址,如图9-15和图9-16所示。

图9-15 onLongPressedWCSightFullScreenWindow:的基地址

图9-16 onLongTouch的基地址

它们的基地址分别是0x21e484和0x21e7ec。下面在两个函数的开头各下一个断点,看看长按小视频播放窗口后,断点会不会被触发,如下:


(lldb) br s -a 0x21e484
Breakpoint 3: where = MicroMessenger`___lldb_unnamed_function9789$$MicroMessenger, address = 0x0021e484
(lldb) br s -a 0x21e7ec
Breakpoint 4: where = MicroMessenger`___lldb_unnamed_function9791$$MicroMessenger, address = 0x0021e7ec
Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x0021e7ec MicroMessenger`___lldb_unnamed_function9791$$MicroMessenger, queue = 'com.apple.main-thread, stop reason = breakpoint 4.1
    frame #0: 0x0021e7ec MicroMessenger`___lldb_unnamed_function9791$$MicroMessenger
MicroMessenger`___lldb_unnamed_function9791$$MicroMessenger:
-> 0x21e7ec:  push   {r4, r5, r6, r7, lr}
   0x21e7ee:  add    r7, sp, #12
   0x21e7f0:  push.w {r8, r10, r11}
   0x21e7f4:  sub    sp, #32
(lldb) p (char *)$r1
(char *) $0 = 0x017fdc2b "onLongTouch"
(lldb) c
Process 184500 resuming
Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x0021e7ec MicroMessenger`___lldb_unnamed_function9791$$MicroMessenger, queue = 'com.apple.main-thread, stop reason = breakpoint 4.1
    frame #0: 0x0021e7ec MicroMessenger`___lldb_unnamed_function9791$$MicroMessenger
MicroMessenger`___lldb_unnamed_function9791$$MicroMessenger:
-> 0x21e7ec:  push   {r4, r5, r6, r7, lr}
   0x21e7ee:  add    r7, sp, #12
   0x21e7f0:  push.w {r8, r10, r11}
   0x21e7f4:  sub    sp, #32
(lldb) p (char *)$r1
(char *) $1 = 0x017fdc2b "onLongTouch"

可以看到,onLongTouch被调用了2次,而onLongPressedWCSightFullScreenWindow:没有被调用。再看看onLongPressedWCSight:的调用情况,它的基地址如图9-17所示。

图9-17 onLongPressedWCSight:的基地址

然后下个断点,看看它会不会被触发,如下:


(lldb) c
Process 184500 resuming
(lldb) br del
About to delete all breakpoints, do you want to do that?: [Y/n] y
All breakpoints removed. (2 breakpoints)
(lldb) br s -a 0x21e414
Breakpoint 5: where = MicroMessenger`___lldb_unnamed_function9788$$MicroMessenger, address = 0x0021e414
Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x0021e414 MicroMessenger`___lldb_unnamed_function9788$$MicroMessenger, queue = 'com.apple.main-thread, stop reason = breakpoint 5.1
    frame #0: 0x0021e414 MicroMessenger`___lldb_unnamed_function9788$$MicroMessenger
MicroMessenger`___lldb_unnamed_function9788$$MicroMessenger:
-> 0x21e414:  push   {r4, r5, r6, r7, lr}
   0x21e416:  add    r7, sp, #12
   0x21e418:  sub    sp, #16
   0x21e41a:  mov    r4, r0
(lldb) p (char *)$r1
(char *) $2 = 0x0182c799 "onLongPressedWCSight:"
(lldb) c
Process 184500 resuming
Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x0021e414 MicroMessenger`___lldb_unnamed_function9788$$MicroMessenger, queue = 'com.apple.main-thread, stop reason = breakpoint 5.1
    frame #0: 0x0021e414 MicroMessenger`___lldb_unnamed_function9788$$MicroMessenger
MicroMessenger`___lldb_unnamed_function9788$$MicroMessenger:
-> 0x21e414:  push   {r4, r5, r6, r7, lr}
   0x21e416:  add    r7, sp, #12
   0x21e418:  sub    sp, #16
   0x21e41a:  mov    r4, r0
(lldb) p (char *)$r1
(char *) $3 = 0x0182c799 "onLongPressedWCSight:"
(lldb) po $r2
<WCSightView: 0x2454dc0; baseClass = UIControl; frame = (0 3; 200 150); gestureRecognizers = <NSArray: 0x87e5110>; layer = <CALayer: 0xd3be460>>

这里onLongPressedWCSight:也被调用了2次,且其参数是一个WCSightView对象。到此,我们已经定位到了小视频播放窗口的长按响应函数,即onLongPressedWCSight:或onLongTouch,接下来就要开始寻找小视频的踪影了。

9.2.6 用Cycript定位小视频的controller

首先点击小视频窗口中的“Tap to download”,把视频下载到本机,如图9-18所示。

图9-18 下载小视频

下载成功后,“Tap to download”字样消失。通过V拿到C进而定位M的过程前面已经重复过很多次了,这里直接操作起来,如下:


FunMaker-5:~ root# cycript -p MicroMessenger
cy# ?expand
expand == true
cy# [[UIApp keyWindow] recursiveDescription]
@"<iConsoleWindow: 0x2392e50; baseClass = UIWindow; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x2391b00>; layer = <UIWindowLayer: 0x2391690>>
   | <UILayoutContainerView: 0x7e71870; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x7e71830>>
   |    | <UITransitionView: 0x7e720b0; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x7e722a0>>
……
   |    |    |    |    |    |    |    |    |    |    |    |    | <WCContentItemViewTemplateNewSight: 0xd3be3e0; frame = (61 64; 200 153); clipsToBounds = YES; layer = <CALayer: 0x7e922d0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    | <WCSightView: 0x2454dc0; baseClass = UIControl; frame = (0 3; 200 150); gestureRecognizers = <NSArray: 0x87e5110>; layer = <CALayer: 0xd3be460>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UIImageView: 0xd34e8d0; frame = (0 0; 200 150); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0xd34e950>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <SightPlayerView: 0x7e50ff0; frame = (0 0; 200 150); layer = <CALayer: 0xd302770>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UIView: 0xd37d9e0; frame = (0 0; 200 150); layer = <CALayer: 0xd37da50>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UIView: 0xd30d5f0; frame = (0 0; 200 150); tag = 10050; layer = <CALayer: 0x87e5650>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <SightIconView: 0xd3be2e0; frame = (0 0; 200 150); layer = <CALayer: 0xd3be380>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <MMUILabel: 0x7ee7530; baseClass = UILabel; frame = (0 103; 200 20); text = 'Tap to play'; hidden = YES; userInteractionEnabled = NO; tag = 10040; layer = <_UILabelLayer: 0x7e50dd0>>
……
cy# [#0xd3be3e0 nextResponder]
#"<WCTimeLineCellView: 0x872c530; frame = (0 0; 313 243); tag = 1048577; layer = <CALayer: 0x872ce80>>"
cy# [#0x872c530 nextResponder]
#"<UITableViewCellContentView: 0x8729d80; frame = (0 0; 320 251); gestureRecognizers = <NSArray: 0x8729f80>; layer = <CALayer: 0x8729df0>>"
cy# [#0x8729d80 nextResponder]
#"<MMTableViewCell: 0x8729be0; baseClass = UITableViewCell; frame = (0 1164.33; 320 251); autoresize = W; layer = <CALayer: 0x8729b50>>"
cy# [#0x8729be0 nextResponder]
#"<UITableViewWrapperView: 0xab09890; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0xab09b00>; layer = <CALayer: 0x7e6e4b0>; contentOffset: {0, 0}; contentSize: {320, 568}>"
cy# [#0xab09890 nextResponder]
#"<MMTableView: 0x30c3200; baseClass = UITableView; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0xab09600>; layer = <CALayer: 0xab09160>; contentOffset: {0, 1090}; contentSize: {320, 3186.3333}>"
cy# [#0x30c3200 nextResponder]
#"<UIView: 0x7e3b040; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x7e3afd0>>"
cy# [#0x7e3b040 nextResponder]
#"<WCTimeLineViewController: 0x28bd200>"

我们拿到了C,即WCTimeLineViewController;同时也能猜到,朋友圈的内部代号是“Time Line”。

9.2.7 从WCTimeLineViewController找到小视频对象

通览WCTimeLineViewController的头文件,你会发现其中的property很少,也没有很明显地访问M的方法,比较可疑的地方是其中的2个全局变量,如下:


WCDataItem *_inputDataItem;
WCDataItem *_cacheDateItem;

但它们全都是null,如下:


cy# #0x28bd200->_cacheDateItem
null
cy# #0x28bd200->_inputDataItem
null

线索貌似到此中断了,难道要就此放弃?当然不是!因为朋友圈是以TableView的形式展示的,而WCTimeLineViewController中存在一个名为tableView:cellForRowAtIndexPath:的方法,说明它实现了UITable-ViewDataSource协议,因此一定和M有千丝万缕的关系。那就去IDA里一探究竟,如图9-19所示。

图9-19 [WCTimeLineViewControllertableView:cellForRowAtIndexPath:]

通览这个函数,你会发现图9-19中的三个深色方块是整个函数的核心,其他的部分只是在给这个cell设置背景图、主题、颜色等周边元素。现在近距离看看这三个深色方块,如图9-20所示。

图9-20 三个深色方块

图比较小,从左至右的三个函数分别是genUploadFailCell:indexPath、genNormalCell:indexPath:和genRedHeartCell:indexPath:。小视频是哪种cell呢?我想你应该也会猜它是“NormalCell”,下面看看genNormalCell:indexPath:的实现,如图9-21所示。

图9-21 [WCTimeLineViewController genNormalCell:indexPath:]

它的逻辑并不复杂,从上到下浏览,很快就能发现一个可疑的函数,如图9-22所示。

图9-22的getTimelineDataItemOfIndex:很有可能就是当前cell的数据源。我们在最下方的“__text:002A091C BLX.W j__objc_msgSend”上下一个断点,然后想办法触发它——当UITableView需要显示新的cell时,tableView:cellForRowAtIndexPath:会得到调用。因此,为了让断点停在带有小视频播放窗口的cell上,要先把小视频滑出当前界面,然后再滑进来。因为在把小视频滑出去的时候,新的cell会触发断点,但这种断点不符合要求,所以这里先“dis断点号”,待小视频窗口完全滑出当前界面后,再“en断点号”,然后把小视频滑回来,这时断点就会停在小视频cell上,如下:

图9-22 [WCTimeLineViewController genNormalCell:indexPath:]


(lldb) br s -a 0x2A091C
Breakpoint 6: where = MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 208, address = 0x002a091c
Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x002a091c MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 208, queue = 'com.apple.main-thread, stop reason = breakpoint 6.1
    frame #0: 0x002a091c MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 208
MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 208:
-> 0x2a091c:  blx    0xe08e0c     ; ___lldb_unnamed_function70162$$MicroMessenger
   0x2a0920:  mov    r11, r0
   0x2a0922:  movw   r0, #32442
   0x2a0926:  movt   r0, #436
(lldb) ni
Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x002a0920 MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x002a0920 MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212
MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212:
-> 0x2a0920:  mov    r11, r0
   0x2a0922:  movw   r0, #32442
   0x2a0926:  movt   r0, #436
   0x2a092a:  add    r0, pc
(lldb) po $r0
Class name: WCDataItem, addr: 0x80f52b0
tid: 11896185303680028954
username: wxid_hqouu9kgsgw3e6
createtime: 1418135798
commentUsers: (
)
contentObj: <WCContentItem: 0x8724c20>

我们拿到了一个WCDataItem对象,它的内部还有一个WCContentItem对象。那这个WCDataItem对象到底是不是小视频的数据呢?用LLDB来测试一下,把这个返回值给置NULL,看看是什么效果。重复刚才的操作,在小视频滑回来时触发断点,如下:


Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x002a091c MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 208, queue = 'com.apple.main-thread, stop reason = breakpoint 6.1
    frame #0: 0x002a091c MicroMessenger`___lldb_unnamed_function11980$$ MicroMessenger + 208
MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 208:
-> 0x2a091c:  blx    0xe08e0c                  ; ___lldb_unnamed_function70162$$MicroMessenger
   0x2a0920:  mov    r11, r0
   0x2a0922:  movw   r0, #32442
   0x2a0926:  movt   r0, #436
(lldb) ni
Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x002a0920 MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x002a0920 MicroMessenger`___lldb_unnamed_function11980$$ MicroMessenger + 212
MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212:
-> 0x2a0920:  mov    r11, r0
   0x2a0922:  movw   r0, #32442
   0x2a0926:  movt   r0, #436
   0x2a092a:  add    r0, pc
(lldb) register write r0 0
(lldb) br del
About to delete all breakpoints, do you want to do that?: [Y/n] y
All breakpoints removed. (1 breakpoint)
(lldb) c

此时,第一条小视频完全消失了,效果如图9-23所示。说明它的数据源就是WCDataItem。在分析WCDataItem之前,我们面临的问题是,如何从被钩住(hook)的函数[WCContentItemViewTemplate-NewSight onLongTouch]中拿到它的WCDataItem对象?

图9-23 把返回值置NULL的效果

9.2.8 从WCContentItemViewTemplateNew-Sight中提取WCDataItem对象

还记得在刚才的分析中是怎么拿到WCDataItem对象的吗?答案是通过getTimeline-DataItemOfIndex:函数。回到图9-22中,看看这个函数的调用者和参数都是什么。

可以看到,它的调用者是getService:的返回值,参数是calcDataItemIndex:的返回值,如图9-24所示。

getService:和calcDataItemIndex:又要怎么调用呢?下面逐个来分析,先看getService:。它的调用者来自“MOV R0,R6”,即R6;R6来自[MMServiceCenter defaultCenter]的返回值。它的参数来自[WCFacade class]的返回值,如图9-25所示。

图9-24 解析getTimelineDataItemOfIndex:(1)

图9-25 解析getTimelineDataItemOfIndex:(2)

因此getTimelineDataItemOfIndex:的调用者可以通过[[MMServiceCenter defaultCenter]getService:[WCFacade class]]来获得。接着分析calcDataItemIndex:,它的调用者来自“MOV R0,R4”,即R4;而R4就是self。它的参数来自[indexPath section]的返回值,如图9-26和图9-27所示。

图9-26 解析getTimelineDataItemOfIndex:(3)

图9-27 解析getTimelineDataItemOfIndex:(4)

因此getTimelineDataItemOfIndex:的参数可以通过[WCTimeLineViewController calcDataItem-Index:[indexPath section]]获取。因为我们位于[WCContentItemViewTemplateNewSight onLongTouch]中,所以可以通过[self nextResponder]依次拿到MMTableViewCell、MMTableView和WCTimeLineViewController,再通过[MMTableView indexPathForCell:MMTableViewCell]拿到indexPath,这个过程在9.2.6节已经得到了验证。虽然看起来有些麻烦,但至少通过符合MVC标准的方式从WCContentItemViewTemplateNewSight中成功提取了WCDataItem对象。值得一提的是,WCTimeLineViewController和WCContentItemViewTemplateNewSight的前缀是WC,笔者猜它是“WeChat”的缩写;而MMTableViewCell和MMTableView的前缀是MM,故而猜它是“MicroMessenger”的缩写——这种命名上的不统一,可能就是因为不同模块不同分工而造成的。接下来,重点剖析WCDataItem,把小视频的本地路径和下载地址从中提取出来。

9.2.9 从WCDataItem中提取目标信息

打开WCDataItem.h,大致浏览一下,如下:


@interface WCDataItem : NSObject <NSCoding>
{
    int cid;
    NSString *tid;
    int type;
    int flag;
    NSString *username;
    NSString *nickname;
    int createtime;
    NSString *sourceUrl;
    NSString *sourceUrl2;
    WCLocationInfo *locationInfo;
    BOOL isPrivate;
    NSMutableArray *sharedGroupIDs;
    NSMutableArray *blackUsers;
    NSMutableArray *visibleUsers;
    unsigned long extFlag;
    BOOL likeFlag;
    int likeCount;
    NSMutableArray *likeUsers;
    int commentCount;
    NSMutableArray *commentUsers;
    int withCount;
    NSMutableArray *withUsers;
    WCContentItem *contentObj;
    WCAppInfo *appInfo;
    NSString *publicUserName;
    NSString *sourceUserName;
    NSString *sourceNickName;
    NSString *contentDesc;
    NSString *contentDescPattern;
    int contentDescShowType;
    int contentDescScene;
    WCActionInfo *actionInfo;
    unsigned int hash;
    SnsObject *snsObject;
    BOOL isBidirectionalFan;
    BOOL noChange;
    BOOL isRichText;
    NSMutableDictionary *extData;
    int uploadErrType;
    NSString *statisticsData;
}
+ (id)fromBuffer:(id)arg1;
+ (id)fromServerObject:(id)arg1;
+ (id)fromUploadTask:(id)arg1;
@property(retain, nonatomic) WCActionInfo *actionInfo; // 
@synthesize actionInfo;
@property(retain, nonatomic) WCAppInfo *appInfo; // 
@synthesize appInfo;
@property(retain, nonatomic) NSArray *blackUsers; // 
@synthesize blackUsers;
@property(nonatomic) int cid; // @synthesize cid;
@property(nonatomic) int commentCount; // @synthesize commentCount;
@property(retain, nonatomic) NSMutableArray *commentUsers; // 
@synthesize commentUsers;
- (int)compareDesc:(id)arg1;
- (int)compareTime:(id)arg1;
@property(retain, nonatomic) NSString *contentDesc; // 
@synthesize contentDesc;
@property(retain, nonatomic) NSString *contentDescPattern; // 
@synthesize contentDescPattern;
@property(nonatomic) int contentDescScene; // @synthesize contentDescScene;
@property(nonatomic) int contentDescShowType; // @synthesize 
contentDescShowType;
@property(retain, nonatomic) WCContentItem *contentObj; // 
@synthesize contentObj;
@property(nonatomic) int createtime; // @synthesize createtime;
- (void)dealloc;
- (id)description;
- (id)descriptionForKeyPaths;
- (void)encodeWithCoder:(id)arg1;
@property(retain, nonatomic) NSMutableDictionary *extData; // 
@synthesize extData;
@property(nonatomic) unsigned long extFlag; // @synthesize extFlag;
@property(nonatomic) int flag; // @synthesize flag;
- (id)getDisplayCity;
- (id)getMediaWraps;
- (BOOL)hasSharedGroup;
- (unsigned int)hash;
- (id)init;
- (id)initWithCoder:(id)arg1;
@property(nonatomic) BOOL isBidirectionalFan; // @synthesize isBidirectionalFan;
- (BOOL)isEqual:(id)arg1;
@property(nonatomic) BOOL isPrivate; // @synthesize isPrivate;
- (BOOL)isRead;
@property(nonatomic) BOOL isRichText; // @synthesize isRichText;
- (BOOL)isUploadFailed;
- (BOOL)isUploading;
- (BOOL)isValid;
- (id)itemID;
- (int)itemType;
- (id)keyPaths;
@property(nonatomic) int likeCount; // @synthesize likeCount;
@property(nonatomic) BOOL likeFlag; // @synthesize likeFlag;
@property(retain, nonatomic) NSMutableArray *likeUsers; // 
@synthesize likeUsers;
- (void)loadPattern;
@property(retain, nonatomic) WCLocationInfo *locationInfo; // 
@synthesize locationInfo;
- (void)mergeLikeUsers:(id)arg1;
- (void)mergeMessage:(id)arg1;
- (void)mergeMessage:(id)arg1 needParseContent:(BOOL)arg2;
@property(retain, nonatomic) NSString *nickname; // 
@synthesize nickname;
@property(nonatomic) BOOL noChange; // @synthesize noChange;
- (void)parseContentForNetWithDataItem:(id)arg1;
- (void)parseContentForUI;
- (void)parsePattern;
@property(retain, nonatomic) NSString *publicUserName; // 
@synthesize publicUserName;
- (id)sequence;
- (void)setCreateTime:(unsigned long)arg1;
- (void)setHash:(unsigned int)arg1;
- (void)setIsUploadFailed:(BOOL)arg1;
- (void)setSequence:(id)arg1;
@property(retain, nonatomic) NSMutableArray *sharedGroupIDs; // @synthesize sharedGroupIDs;
@property(retain, nonatomic) SnsObject *snsObject; // 
@synthesize snsObject;
@property(retain, nonatomic) NSString *sourceNickName; // 
@synthesize sourceNickName;
@property(retain, nonatomic) NSString *sourceUrl2; // 
@synthesize sourceUrl2;
@property(retain, nonatomic) NSString *sourceUrl; // 
@synthesize sourceUrl;
@property(retain, nonatomic) NSString *sourceUserName; // 
@synthesize sourceUserName;
@property(retain, nonatomic) NSString *statisticsData; // 
@synthesize statisticsData;
@property(retain, nonatomic) NSString *tid; // @synthesize tid;
@property(nonatomic) int type; // @synthesize type;
@property(nonatomic) int uploadErrType; // @synthesize uploadErrType;
@property(retain, nonatomic) NSString *username; // 
@synthesize username;
@property(retain, nonatomic) NSArray *visibleUsers; // 
@synthesize visibleUsers;
@property(nonatomic) int withCount; // @synthesize withCount;
@property(retain, nonatomic) NSMutableArray *withUsers; // 
@synthesize withUsers;
- (id)toBuffer;
@end

可以看到,文件中一共有4处出现了“path”和“url”关键词,如下:


- (id)descriptionForKeyPaths;
- (id)keyPaths;
@property(retain, nonatomic) NSString *sourceUrl2;
@property(retain, nonatomic) NSString *sourceUrl;

下面用LLDB来看看它们都会返回什么。重复刚才的操作,在小视频滑回来时触发断点,如下:


Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x002a091c 
MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 208, queue = 'com.apple.main-thread, stop reason = breakpoint 7.1
    frame #0: 0x002a091c 
MicroMessenger`___lldb_unnamed_function11980$$Micro Messenger + 208MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 208:
-> 0x2a091c:  blx    0xe08e0c                  ; 
___lldb_unnamed_function70162$$MicroMessenger
   0x2a0920:  mov    r11, r0
   0x2a0922:  movw   r0, #32442
   0x2a0926:  movt   r0, #436
(lldb) ni
Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x002a0920 
MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212, queue = 'com.apple.main-thread, stop reason = 
instruction step over
    frame #0: 0x002a0920 
MicroMessenger`___lldb_unnamed_function11980$$Micro Messenger + 212
MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212:
-> 0x2a0920:  mov    r11, r0
   0x2a0922:  movw   r0, #32442
   0x2a0926:  movt   r0, #436
   0x2a092a:  add    r0, pc
(lldb) po [$r0 descriptionForKeyPaths]
Class name: WCDataItem, addr: 0x80f52b0
tid: 11896185303680028954
username: wxid_hqouu9kgsgw3e6
createtime: 1418135798
commentUsers: (
)
contentObj: <WCContentItem: 0x8724c20>
(lldb) po [$r0 keyPaths]
<__NSArrayI 0x87b5260>(
tid,
username,
createtime,
commentUsers,
contentObj
)
(lldb) po [$r0 sourceUrl2]
 nil
(lldb) po [$r0 sourceUrl]
 nil

这几个函数的返回值并没有让人眼前一亮的信息,但多次出现的WCContentItem却引起了笔者的注意。显然,“content”比“data”的含义更明确,小视频对象的信息有可能就是它提供的,下面来看看WCContentItem.h,如下:


@interface WCContentItem : NSObject <NSCoding>
{
    NSString *title;
    NSString *desc;
    NSString *titlePattern;
    NSString *descPattern;
    NSString *linkUrl;
    NSString *linkUrl2;
    int type;
    int flag;
    NSString *username;
    NSString *nickname;
    int createtime;
    NSMutableArray *mediaList;
}
@property(nonatomic) int createtime; // @synthesize createtime;
- (void)dealloc;
@property(retain, nonatomic) NSString *desc; // @synthesize desc;
@property(retain, nonatomic) NSString *descPattern; // 
@synthesize descPattern;
- (void)encodeWithCoder:(id)arg1;
@property(nonatomic) int flag; // @synthesize flag;
- (id)init;
- (id)initWithCoder:(id)arg1;
- (BOOL)isValid;
@property(retain, nonatomic) NSString *linkUrl; // 
@synthesize linkUrl;
@property(retain, nonatomic) NSString *linkUrl2; // 
@synthesize linkUrl2;
@property(retain, nonatomic) NSMutableArray *mediaList; // 
@synthesize mediaList;
@property(retain, nonatomic) NSString *nickname; // 
@synthesize nickname;
@property(retain, nonatomic) NSString *title; // @synthesize title;
@property(retain, nonatomic) NSString *titlePattern; // 
@synthesize titlePattern;
@property(nonatomic) int type; // @synthesize type;
@property(retain, nonatomic) NSString *username; // 
@synthesize username;
@end

可以看到,文件中一共有2处出现了“url”关键词,如下:


@property(retain, nonatomic) NSString *linkUrl;
@property(retain, nonatomic) NSString *linkUrl2;

通过[WCDataItem contentObj]函数可以获取其对应的WCContentItem对象,我们用LLDB看看上面2个property的值。重复刚才的操作,在小视频滑回来时触发断点,如下:


Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x002a091c MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 208, queue = 'com.apple.main-thread, stop reason = breakpoint 7.1
    frame #0: 0x002a091c MicroMessenger`___lldb_unnamed_function11980$$Micro Messenger + 208
MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 208:
-> 0x2a091c:  blx    0xe08e0c                  ; ___lldb_unnamed_function70162$$MicroMessenger
   0x2a0920:  mov    r11, r0
   0x2a0922:  movw   r0, #32442
   0x2a0926:  movt   r0, #436
(lldb) ni
Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x002a0920 MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x002a0920 MicroMessenger`___lldb_unnamed_function11980$$Micro Messenger + 212
MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212:
-> 0x2a0920:  mov    r11, r0
   0x2a0922:  movw   r0, #32442
   0x2a0926:  movt   r0, #436
   0x2a092a:  add    r0, pc
(lldb) po [$r0 descriptionForKeyPaths]
Class name: WCDataItem, addr: 0x80f52b0
tid: 11896185303680028954
username: wxid_hqouu9kgsgw3e6
createtime: 1418135798
commentUsers: (
)
contentObj: <WCContentItem: 0x8724c20>
(lldb) po [$r0 keyPaths]
<__NSArrayI 0x87b5260>(
tid,
username,
createtime,
commentUsers,
contentObj
)
(lldb) po [$r0 sourceUrl2]
 nil
(lldb) po [$r0 sourceUrl]
 nil

接下来在浏览器里输入这个url,看看对应的是个什么东西,如图9-28所示。

图9-28 [[$r0 contentObj]linkUrl]

跟我们想要的结果驴唇不对马嘴。WCContentItem.h里的内容本来就不多,小视频会藏在哪里呢?回看这个文件,一个名为mediaList的property引起了笔者的注意。相对于“content”、“media”的定位更精确了,小视频会不会藏在它里面呢?还是用LLDB测一测。重复刚才的操作,在小视频滑回来时触发断点,如下:


Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x002a091c MicroMessenger`__lldb_unnamed_function11980$$MicroMessenger + 208, queue ='com.apple.main-thread, stop reason = breakpoint 8.1
    frame #0: 0x002a091c MicroMessenger`___lldb_unnamed_function11980$$Micro Messenger + 208
MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 208:
-> 0x2a091c:  blx    0xe08e0c                  ; ___lldb_unnamed_function70162$$MicroMessenger
   0x2a0920:  mov    r11, r0
   0x2a0922:  movw   r0, #32442
   0x2a0926:  movt   r0, #436
(lldb) ni
Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x002a0920 MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x002a0920 MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212
MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212:
-> 0x2a0920:  mov    r11, r0
   0x2a0922:  movw   r0, #32442
   0x2a0926:  movt   r0, #436
   0x2a092a:  add    r0, pc
(lldb) po [[[[$r0 contentObj] mediaList] objectAtIndex:0] pathForData]
/var/mobile/Containers/Data/Application/E9BE84D8-9982-4814-9289-823D5FD91144/Library/WechatPrivate/c5f5eb23e53bb2ee021b0e89b5c4bc9a/wc/media/5/60/2a16b0b62baf39924448a74fa03ff2
(lldb) po [[[[$r0 contentObj] mediaList] objectAtIndex:0] pathForPreview]
/var/mobile/Containers/Data/Application/E9BE84D8-9982-4814-9289-823D5FD91144/Library/WechatPrivate/c5f5eb23e53bb2ee021b0e89b5c4bc9a/wc/media/5/7f/cdc7939813d1a 95feda4bed05f9b82
(lldb) po [[[[$r0 contentObj] mediaList] objectAtIndex:0] pathForSightData]
/var/mobile/Containers/Data/Application/E9BE84D8-9982-4814-9289-823D5FD91144/Library/WechatPrivate/c5f5eb23e53bb2ee021b0e89b5c4bc9a/wc/media/5/60/2a16b0b62baf39924448a74fa03ff2.mp4
(lldb) po [[[[$r0 contentObj] mediaList] objectAtIndex:0] dataUrl]
type[1], url[http://vcloud1023.tc.qq.com/1023_0114929ce86949a8bfb6f7b46b6b39b8.f0.mp4]
(lldb) po [[[[$r0 contentObj] mediaList] objectAtIndex:0] lowBandUrl]
 nil
(lldb) po [[[[$r0 contentObj] mediaList] objectAtIndex:0] previewUrls]
<__NSArrayM 0x8725950>(
type[1], url[http://mmsns.qpic.cn/mmsns/WiaWbRORjpHsUXcNL3dNsVLDibRZ9oufPnXeJqZdlG4xhND43M87sh7DRcxttVPxAO/0]
)

此时,一个新的类WCMediaItem出现了。下面看看它的头文件WCMediaItem.h,如下:


@interface WCMediaItem : NSObject <NSCoding>
{
    NSString *mid;
    int type;
    int subType;
    NSString *title;
    NSString *desc;
    NSString *titlePattern;
    NSString *descPattern;
    NSString *userData;
    NSString *source;
    NSMutableArray *previewUrls;
    WCUrl *dataUrl;
    WCUrl *lowBandUrl;
    struct CGSize imgSize;
    BOOL likeFlag;
    int likeCount;
    NSMutableArray *likeUsers;
    int commentCount;
    NSMutableArray *commentUsers;
    int withCount;
    NSMutableArray *withUsers;
    int createTime;
}
- (id).cxx_construct;
- (id)cityForData;
@property(nonatomic) int commentCount; // @synthesize comentCount;
@property(retain, nonatomic) NSMutableArray *commentUsers; // 
@synthesize commentUsers;
- (id)comparativePathForPreview;
@property(nonatomic) int createTime; // @synthesize 
createTime;
@property(retain, nonatomic) WCUrl *dataUrl; // @synthesize dataUrl;
- (void)dealloc;
@property(retain, nonatomic) NSString *desc; // @synthesize desc;
@property(retain, nonatomic) NSString *descPattern; // 
@synthesize descPattern;
- (void)encodeWithCoder:(id)arg1;
- (BOOL)hasData;
- (BOOL)hasPreview;
- (BOOL)hasSight;
- (id)hashPathForString:(id)arg1;
- (id)imageOfSize:(int)arg1;
@property(nonatomic) struct CGSize imgSize; // @synthesize imgSize;
- (id)init;
- (id)initWithCoder:(id)arg1;
- (BOOL)isValid;
@property(nonatomic) int likeCount; // @synthesize likeCount;
@property(nonatomic) BOOL likeFlag; // @synthesize likeFlag;
@property(retain, nonatomic) NSMutableArray *likeUsers; // 
@synthesize likeUsers;
- (CDStruct_c3b9c2ee)locationForData;
@property(retain, nonatomic) WCUrl *lowBandUrl; // 
@synthesize lowBandUrl;
- (id)mediaID;
- (int)mediaType;
@property(retain, nonatomic) NSString *mid; // @synthesize mid;
- (id)pathForData;
- (id)pathForPreview;
- (id)pathForSightData;
@property(retain, nonatomic) NSMutableArray *previewUrls; // 
@synthesize previewUrls;
- (BOOL)saveDataFromData:(id)arg1;
- (BOOL)saveDataFromMedia:(id)arg1;
- (BOOL)saveDataFromPath:(id)arg1;
- (BOOL)savePreviewFromData:(id)arg1;
- (BOOL)savePreviewFromMedia:(id)arg1;
- (BOOL)savePreviewFromPath:(id)arg1;
- (BOOL)saveSightDataFromData:(id)arg1;
- (BOOL)saveSightDataFromMedia:(id)arg1;
- (BOOL)saveSightDataFromPath:(id)arg1;
- (BOOL)saveSightPreviewFromMedia:(id)arg1;
@property(retain, nonatomic) NSString *source; // @synthesize source;
@property(nonatomic) int subType; // @synthesize subType;
@property(retain, nonatomic) NSString *title; // @synthesize title;
@property(retain, nonatomic) NSString *titlePattern; // 
@synthesize titlePattern;
@property(nonatomic) int type; // @synthesize type;
@property(retain, nonatomic) NSString *userData; // 
@synthesize userData;
@property(nonatomic) int withCount; // @synthesize withCount;
@property(retain, nonatomic) NSMutableArray *withUsers; // 
@synthesize withUsers;
- (id)videoStreamForData;
- (id)voiceStreamForData;
@end

可以看到,在头文件中出现了8次“path”关键词,如下:


- (id)comparativePathForPreview;
- (id)hashPathForString:(id)arg1;
- (id)pathForData;
- (id)pathForPreview;
- (id)pathForSightData;
- (BOOL)saveDataFromPath:(id)arg1;
- (BOOL)savePreviewFromPath:(id)arg1;
- (BOOL)saveSightDataFromPath:(id)arg1;

还有3次“url”关键词,如下:


@property(retain, nonatomic) WCUrl *dataUrl;
@property(retain, nonatomic) WCUrl *lowBandUrl;
@property(retain, nonatomic) NSMutableArray *previewUrls;

其中,pathForData、pathForPreview和pathForSightData极有可能返回一个path;dataUrl、lowBandUrl和previewUrls极有可能返回url,马上用LLDB看看这些返回值是什么。重复刚才的操作,在小视频滑回来时触发断点,如下:


Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x002a091c MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 208, queue = 'com.apple.main-thread, stop reason = breakpoint 8.1
    frame #0: 0x002a091c MicroMessenger`___lldb_unnamed_function11980$$Micro Messenger + 208
MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 208:
-> 0x2a091c:  blx    0xe08e0c                  ; ___lldb_unnamed_function70162$$MicroMessenger
   0x2a0920:  mov    r11, r0
   0x2a0922:  movw   r0, #32442
   0x2a0926:  movt   r0, #436
(lldb) ni
Process 184500 stopped
* thread #1: tid = 0x2d0b4, 0x002a0920 MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x002a0920 MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212
MicroMessenger`___lldb_unnamed_function11980$$MicroMessenger + 212:
-> 0x2a0920:  mov    r11, r0
   0x2a0922:  movw   r0, #32442
   0x2a0926:  movt   r0, #436
   0x2a092a:  add    r0, pc
(lldb) po [[[[$r0 contentObj] mediaList] objectAtIndex:0] pathForData]
/var/mobile/Containers/Data/Application/E9BE84D8-9982-4814-9289-823D5FD91144/Library/WechatPrivate/c5f5eb23e53bb2ee021b0e89b5c4bc9a/wc/media/5/60/2a16b0b62baf39924448a74fa03ff2
(lldb) po [[[[$r0 contentObj] mediaList] objectAtIndex:0] pathForPreview]
/var/mobile/Containers/Data/Application/E9BE84D8-9982-4814-9289-823D5FD91144/Library/WechatPrivate/c5f5eb23e53bb2ee021b0e89b5c4bc9a/wc/media/5/7f/cdc7939813d1a 95feda4bed05f9b82
(lldb) po [[[[$r0 contentObj] mediaList] objectAtIndex:0] pathForSightData]
/var/mobile/Containers/Data/Application/E9BE84D8-9982-4814-9289-823D5FD91144/Library/WechatPrivate/c5f5eb23e53bb2ee021b0e89b5c4bc9a/wc/media/5/60/2a16b0b62baf39924448a74fa03ff2.mp4
(lldb) po [[[[$r0 contentObj] mediaList] objectAtIndex:0] dataUrl]
type[1], url[http://vcloud1023.tc.qq.com/1023_0114929ce86949a8bfb6f7b46b6b39b8.f0.mp4]
(lldb) po [[[[$r0 contentObj] mediaList] objectAtIndex:0] lowBandUrl]
 nil
(lldb) po [[[[$r0 contentObj] mediaList] objectAtIndex:0] previewUrls]
<__NSArrayM 0x8725950>(
type[1], url[http://mmsns.qpic.cn/mmsns/WiaWbRORjpHsUXcNL3dNsVLDibRZ9oufPnXeJqZdlG4xhND43M87sh7DRcxttVPxAO/0]
)

从文件名就可以看出,这些应该就是我们要找的小视频信息了。不管你是直接在ssh中操作,还是用iFunBox浏览本地文件;不管你是用MobileSafari,还是用Chrome打开URL,都可以得出以下结论:

·pathForData返回小视频的本地路径,不带后缀名;

·pathForPreview返回小视频的预览图片路径,没有后缀名;

·pathForSightData返回小视频的本地路径,带后缀名;

·dataUrl返回小视频的网络URL;

·lowBandUrl返回nil,笔者猜测,当网络状况不好时,它的值不为nil;为了节省带宽,这个URL对应的mp4文件很可能比dataUrl对应的文件要小;

·previewUrls返回小视频的预览图URL。

tweak原型搭建到此结束,下面先整理一下思路,再开始写代码。