前言

此文为逆向微信二进制文件,实现朋友圈小视频转发的教程,从最开始的汇编代码入手到最后重签名安装等操作,手把手教你玩转微信!学会之后再去逆向微信其他功能易如反掌。
本篇文章由于篇幅太长分成了两篇,上篇讲解的是逆向工作,也就是怎么找到相关的函数和方法实现,下篇讲解的是怎么在非越狱机重签名安装和越狱机tweak安装的详细过程。
正文的第二部分还提供了微信自动抢红包、修改微信步数的代码,这些都可以照葫芦画瓢按照本文的套路一步步逆向找到,这里就不再赘述。
在实践之前,需要准备好一部越狱的手机,然后将下文列出的所有工具安装好。IDA跟Reveal都是破解版,IDA的正版要2000多刀,对于这么牛逼的逆向工具确实物有所值,不过不是专门研究逆向的公司也没必要用正版的,下个Windows的破解版就好,Mac上暂时没找到。Mac上可以用hopper代替IDA,也是一款很牛逼的逆向工具。废话不多说,正式开始吧!

转载请注明出处:来自LeonLei的博客http://www.gaoshilei.com

逆向微信朋友圈(上篇)

一、获取朋友圈的小视频

注意:本文逆向的微信的二进制文件为6.3.28版本,如果是不同的微信版本,二进制文件中的基地址也不相同

本文涉及到的工具

  1. cycript
  2. LLDB与debugserver(Xcode自带)
  3. OpenSSH
  4. IDA
  5. Reveal
  6. theos
  7. CydiaSubstrate
  8. iOSOpenDev
  9. ideviceinstaller
  10. tcprelay(本地端口映射,USB连接SSH,不映射可通过WiFi连接)
  11. dumpdecrypted
  12. class-dump
  13. iOS App Signer
  14. 编译好的yololib

逆向环境为MacOS + iPhone5S 9.1越狱机
先用dumpdecrypted给微信砸壳(不会的请我写的看这篇教程),获得一个WeChat.decrypted文件,先把这个文件扔到IDA中分析(60MB左右的二进制文件,IDA差不多40分钟才能分析完),用class-dump导出所有头文件

1
LeonLei-MBP:~ gaoshilei$ class-dump -S -s -H /Users/gaoshilei/Desktop/reverse/binary_for_class-dump/WeChat.decrypted -o /Users/gaoshilei/Desktop/reverse/binary_for_class-dump/class-Header/WeChat

我滴个亲娘!一共有8000个头文件,微信果然工程量浩大!稳定一下情绪,理一理思路继续搞。要取得小视频的下载链接,找到播放视频的View,顺藤摸瓜就能找到小视频的URL。用Reveal查看小视频的播放窗口
Reveal
可以看出来WCContentItemViewTemplateNewSigh这个对象是小视频的播放窗口,它的subView有WCSightView,SightView、SightPlayerView,这几个类就是我们的切入点。
保存视频到favorite的时候是长按视频弹出选项的,那么在WCContentItemViewTemplateNewSight这个类里面可能有手势相关的方法,去刚才导出的头文件中找线索。

1
2
3
- (void)onLongTouch;
- (void)onLongPressedWCSight:(id)arg1;
- (void)onLongPressedWCSightFullScreenWindow:(id)arg1;

这几个方法跟长按手势相关,再去IDA中找到这些函数,逐个查看。onLongPressedWCSight和onLongPressedWCSightFullScreenWindow都比较简单,onLongTouch比较长,而且发现了内部调用了方法Favorites_Add,因为长按视频的时候出来一个选项就是Favorites,并且我看到这个函数调用

1
2
ADRP            X8, #selRef_sightVideoPath@PAGE
LDR X1, [X8,#selRef_sightVideoPath@PAGEOFF]

这里拿到了小视频的地址,可以推测这个函数跟收藏有关,下面打断点测试。

1
2
(lldb) im li -o -f
[ 0] 0x000000000003c000 /var/mobile/Containers/Bundle/Application/2F1D52EC-C57E-4F95-B715-EF04351232E8/WeChat.app/WeChat(0x000000010003c000)

可以看到WeChat的ASLR为0x3c000,在IDA查找到这三个函数的基地址,分别下断点

1
2
3
4
5
6
(lldb) br s -a 0x1020D3A10+0x3c000
Breakpoint 1: where = WeChat`___lldb_unnamed_symbol110094$$WeChat + 28, address = 0x000000010210fa10
(lldb) br s -a 0x1020D3370+0x3c000
Breakpoint 2: where = WeChat`___lldb_unnamed_symbol110091$$WeChat + 8, address = 0x000000010210f370
(lldb) br s -a 0x1020D33E4+0x3c000
Breakpoint 3: where = WeChat`___lldb_unnamed_symbol110092$$WeChat + 12, address = 0x000000010210f3e4

回到微信里面长按小视频,看断点触发情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Process 3721 stopped
* thread #1: tid = 0x658fc, 0x000000010210f370 WeChat`___lldb_unnamed_symbol110091$$WeChat + 8, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
frame #0: 0x000000010210f370 WeChat`___lldb_unnamed_symbol110091$$WeChat + 8
WeChat`___lldb_unnamed_symbol110091$$WeChat:
-> 0x10210f370 <+8>: add x29, sp, #16 ; =16
0x10210f374 <+12>: mov x19, x0
0x10210f378 <+16>: adrp x8, 4968
0x10210f37c <+20>: ldr x0, [x8, #744]
(lldb) c
Process 3721 resuming
Process 3721 stopped
* thread #1: tid = 0x658fc, 0x000000010210fa10 WeChat`___lldb_unnamed_symbol110094$$WeChat + 28, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x000000010210fa10 WeChat`___lldb_unnamed_symbol110094$$WeChat + 28
WeChat`___lldb_unnamed_symbol110094$$WeChat:
-> 0x10210fa10 <+28>: add x29, sp, #96 ; =96
0x10210fa14 <+32>: sub sp, sp, #96 ; =96
0x10210fa18 <+36>: mov x19, x0
0x10210fa1c <+40>: adrp x8, 4863
……

发现断点2先被触发,接着触发断点1,后面断点2和1又各触发了1次,断点3一直很安静。可以排除onLongPressedWCSightFullScreenWindow与收藏小视频的联系。小视频的踪影就要在剩下的两个方法中寻找了。通过V找到C,顺藤摸瓜找到M屡试不爽!用cycript注入WeChat,拿到播放小视频的view所在的Controller。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cy# [#0x138c18030 nextResponder]
#"<WCTimeLineCellView: 0x138c34620; frame = (0 0; 319 249); tag = 1048577; layer = <CALayer: 0x138362ba0>>"
cy# [#0x138c34620 nextResponder]
#"<UITableViewCellContentView: 0x138223c70; frame = (0 0; 320 256); gestureRecognizers = <NSArray: 0x1384ec480>; layer = <CALayer: 0x138081dc0>>"
cy# [#0x138223c70 nextResponder]
#"<MMTableViewCell: 0x138c9f930; baseClass = UITableViewCell; frame = (0 307; 320 256); autoresize = W; layer = <CALayer: 0x1382dcd10>>"
cy# [#0x138c9f930 nextResponder]
#"<UITableViewWrapperView: 0x137b57800; frame = (0 0; 320 504); gestureRecognizers = <NSArray: 0x1383db660>; layer = <CALayer: 0x138af20c0>; contentOffset: {0, 0}; contentSize: {320, 504}>"
cy# [#0x137b57800 nextResponder]
#"<MMTableView: 0x137b8ae00; baseClass = UITableView; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x138adb590>; layer = <CALayer: 0x138956890>; contentOffset: {0, 99.5}; contentSize: {320, 3193}>"
cy# [#0x137b8ae00 nextResponder]
#"<UIView: 0x138ade5c0; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x138ac9990>>"
cy# [#0x138ade5c0 nextResponder]
#"<WCTimeLineViewController: 0x1379eb000>"

通过响应者链条找到
WCContentItemViewTemplateNewSight所属的Controller为WCTimeLineViewController。在这个类的头文件中并没有发现有价值的线索,不过我们注意到小视频所在的view是属于MMTableVIewCell的(见上图Reveal分析图),这是每一个iOS最熟悉的TableView,cell的数据是通过UITableViewDataSource的代理方法- tableView:cellForRowAtIndexPath:赋值的,通过这个方法肯定能知道到M的影子。在IDA中找到[WCTimeLineViewController tableView:cellForRowAtIndexPath:],定位到基地址0x10128B6B0位置:

1
__text:000000010128B6B0     ADRP     X8, #selRef_genNormalCell_indexPath_@PAGE

这里的函数是WCTimeLineViewController中生成cell的方法,除了这个方法在这个类中还有另外三个生成cell的方法:

1
2
3
- (void)genABTestTipCell:(id)arg1 indexPath:(id)arg2;
- (void)genRedHeartCell:(id)arg1 indexPath:(id)arg2;
- (void)genUploadFailCell:(id)arg1 indexPath:(id)arg2;

通过字面意思可以猜测出normal这个应该是生成小视频cell的方法。继续在IDA中寻找线索

1
__text:0000000101287CC8     ADRP     X8, #selRef_getTimelineDataItemOfIndex_@PAGE

genNormalCell:IndexPath:方法中发现上面这个方法,可以大胆猜想这个方法是获取TimeLine(朋友圈)数据的方法,那小视频的数据肯定也是通过这个方法获取的,并且IDA可以看到这个方法中调用一个叫做selRef_getTimelineDataItemOfIndex_的方法,获取DataItem貌似就是cell的数据源啊!接下来用LLDB下断点验证猜想。
通过IDA可以找到这个方法对应的基地址为:0x101287CE4,先打印正在运行WeChat的ASLR偏移

1
2
3
4
LeonLei-MBP:~ gaoshilei$ lldb
(lldb) process connect connect://localhost:1234
(lldb) im li -o -f
[0] 0x0000000000050000 /var/mobile/Containers/Bundle/Application/2DCE8F30-9B6B-4652-901C-37EB1FF2A40D/WeChat.app/WeChat(0x0000000100050000)

所以我们下断点的位置是0x50000+0x101287CE4

1
2
(lldb) br s -a 0x50000+0x101287CE4
Breakpoint 1: where = WeChat`___lldb_unnamed_symbol63721$$WeChat + 252, address = 0x00000001012d7ce4

打印x0的值

1
2
3
4
5
6
7
8
9
(lldb) po $x0
Class name: WCDataItem, addr: 0x15f5f03b0
tid: 12393001887435993280
username: wxid_z8twcz4o18fg12
createtime: 1477360950
commentUsers: (
)
contentObj: <WCContentItem: 0x15f57d000>

得到一个WCDataItem的对象,这里x0的值就是selRef_getTimelineDataItemOfIndex_执行完的返回值,然后把x0的值改掉

1
2
(lldb) register write $x0 0
(lldb) c

此时会发现我们要刷新的那条小视频内容全部为空
小视频内容为空
到这里已经找到了小视频的源数据获取方法,问题是我们怎么拿到这个WCDataItem呢?继续看IDA分析函数的调用情况:

WCTimeLineViewController - (void)genNormalCell:(id) indexPath:(id)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
__text:0000000101287BCC                 STP             X28, X27, [SP,#var_60]!
__text:0000000101287BD0 STP X26, X25, [SP,#0x60+var_50]
__text:0000000101287BD4 STP X24, X23, [SP,#0x60+var_40]
__text:0000000101287BD8 STP X22, X21, [SP,#0x60+var_30]
__text:0000000101287BDC STP X20, X19, [SP,#0x60+var_20]
__text:0000000101287BE0 STP X29, X30, [SP,#0x60+var_10]
__text:0000000101287BE4 ADD X29, SP, #0x60+var_10
__text:0000000101287BE8 SUB SP, SP, #0x80
__text:0000000101287BEC MOV X19, X3
__text:0000000101287BF0 MOV X22, X0
__text:0000000101287BF4 MOV W25, #0x100000
__text:0000000101287BF8 MOVK W25, #1
__text:0000000101287BFC MOV X0, X2
__text:0000000101287C00 BL _objc_retain
__text:0000000101287C04 MOV X28, X0
__text:0000000101287C08 MOV X0, X19
__text:0000000101287C0C BL _objc_retain
__text:0000000101287C10 MOV X20, X0
__text:0000000101287C14 STR X20, [SP,#0xE0+var_98]
__text:0000000101287C18 ADRP X8, #selRef_row@PAGE
__text:0000000101287C1C LDR X1, [X8,#selRef_row@PAGEOFF]
__text:0000000101287C20 BL _objc_msgSend
__text:0000000101287C24 MOV X26, X0
__text:0000000101287C28 ADRP X8, #selRef_section@PAGE
__text:0000000101287C2C LDR X19, [X8,#selRef_section@PAGEOFF]
__text:0000000101287C30 MOV X0, X20
__text:0000000101287C34 MOV X1, X19
__text:0000000101287C38 BL _objc_msgSend
__text:0000000101287C3C STR X0, [SP,#0xE0+var_A8]
__text:0000000101287C40 MOV X0, X20
__text:0000000101287C44 MOV X1, X19
__text:0000000101287C48 BL _objc_msgSend
__text:0000000101287C4C MOV X2, X0
__text:0000000101287C50 ADRP X8, #selRef_calcDataItemIndex_@PAGE
__text:0000000101287C54 LDR X1, [X8,#selRef_calcDataItemIndex_@PAGEOFF]
__text:0000000101287C58 MOV X0, X22
__text:0000000101287C5C BL _objc_msgSend
__text:0000000101287C60 MOV X21, X0
__text:0000000101287C64 STR X21, [SP,#0xE0+var_C0]
__text:0000000101287C68 ADRP X8, #classRef_MMServiceCenter@PAGE
__text:0000000101287C6C LDR X0, [X8,#classRef_MMServiceCenter@PAGEOFF]
__text:0000000101287C70 ADRP X8, #selRef_defaultCenter@PAGE
__text:0000000101287C74 LDR X1, [X8,#selRef_defaultCenter@PAGEOFF]
__text:0000000101287C78 STR X1, [SP,#0xE0+var_B8]
__text:0000000101287C7C BL _objc_msgSend
__text:0000000101287C80 MOV X29, X29
__text:0000000101287C84 BL _objc_retainAutoreleasedReturnValue
__text:0000000101287C88 MOV X19, X0
__text:0000000101287C8C ADRP X8, #classRef_WCFacade@PAGE
__text:0000000101287C90 LDR X0, [X8,#classRef_WCFacade@PAGEOFF]
__text:0000000101287C94 ADRP X8, #selRef_class@PAGE
__text:0000000101287C98 LDR X1, [X8,#selRef_class@PAGEOFF]
__text:0000000101287C9C STR X1, [SP,#0xE0+var_B0]
__text:0000000101287CA0 BL _objc_msgSend
__text:0000000101287CA4 MOV X2, X0
__text:0000000101287CA8 ADRP X8, #selRef_getService_@PAGE
__text:0000000101287CAC LDR X1, [X8,#selRef_getService_@PAGEOFF]
__text:0000000101287CB0 STR X1, [SP,#0xE0+var_A0]
__text:0000000101287CB4 MOV X0, X19
__text:0000000101287CB8 BL _objc_msgSend
__text:0000000101287CBC MOV X29, X29
__text:0000000101287CC0 BL _objc_retainAutoreleasedReturnValue
__text:0000000101287CC4 MOV X20, X0
__text:0000000101287CC8 ADRP X8, #selRef_getTimelineDataItemOfIndex_@PAGE
__text:0000000101287CCC LDR X1, [X8,#selRef_getTimelineDataItemOfIndex_@PAGEOFF]
__text:0000000101287CD0 STR X1, [SP,#0xE0+var_C8]
__text:0000000101287CD4 MOV X2, X21
__text:0000000101287CD8 BL _objc_msgSend
__text:0000000101287CDC MOV X29, X29
__text:0000000101287CE0 BL _objc_retainAutoreleasedReturnValue
__text:0000000101287CE4 MOV X21, X0
__text:0000000101287CE8 MOV X0, X20
......

selRef_getTimelineDataItemOfIndex_ 传入的参数是x2,可以看到传值给x2的x21是函数selRef_calcDataItemIndex_ 的返回值,是一个unsigned long数据类型。继续分析,selRef_getTimelineDataItemOfIndex_ 函数的调用者是上一步selRef_getService_ 的返回值,经过断点分析发现是一个WCFacade对象。整理一下selRef_getTimelineDataItemOfIndex_ 的调用:
调用者是selRef_getService_ 的返回值;参数是selRef_calcDataItemIndex_ 的返回值
下面把目光转向那两个函数,用相同的原理分析它们各自怎么实现调用
1. 先看selRef_getService_
在0x101287CB4这个位置可以发现,这个函数的调用者是从通过x19 MOV的,打印x19发现是一个MMServiceCenter对象,往上找x19是在0x101287C88这个位置赋值的,结果很清晰x19是[MMServiceCenter defaultCenter]的返回值。
在0x101287CA4位置可以找到传入的参数x2,往上分析可以看出来它的参数是[WCFacade class]的返回值。
2. 接着找selRef_calcDataItemIndex_
在0x101287C58的位置找到它的调用者x0,x0通过x22赋值,继续向上寻找,发现在最上面0x101287BF0的位置,x22是x0赋值的,一开始的x0就是WCTimeLineViewController自身。
在0x101287C4C位置发现传入的参数来自x2,x2是通过上一步selRef_section函数的返回值x0赋值的,在0x101287C30位置可以发现selRef_section函数的调用者是x20赋值的,如下图所示,最终找到selRef_section的调用者是x3
selRef_section函数的调用者
x3就是函数 WCTimeLineViewController - (void)genNormalCell:(id) indexPath:(id)的第二个参数indexPath,,所以selRef_calcDataItemIndex_ 的参数是[IndexPath section]
对上面的分析结果做个梳理:
因此getTimelineDataItemOfIndex:的调用者可以通过

1
[[MMServiceCenter defaultCenter] getService:[WCFacade class]]

来获得,它的参数可以通过下面的函数获取

1
[WCTimeLineViewController calcDataItemIndex:[indexPath section]]

总感觉还少点什么?indexPath我们还没拿到呢!下一步就是拿到indexPath,这个就比较简单了,因为我们位于[WCContentItemViewTemplateNewSight onLongTouch]中,所以可以通过[self nextResponder]依次拿到MMTableViewCell、MMTableView和WCTimeLineViewController,再通过[MMTableView indexPathForCell:MMTableViewCell]拿到indexPath。
做完这些,已经拿到WCDataItem对象,接下来的重点要放在WCDataItem上,最终要获取我们要的小视频。到这个类的头文件中找线索,因为视频是下载完成后才能播放的,所以这里应该拿到了视频的路径,所以要注意url和path相关的属性或方法,然后找到下面这几个嫌疑对象

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

回到LLDB中,用断点打印这些值,看看有什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(lldb) po [$x0 keyPaths]
<__NSArrayI 0x15f74e9d0>(
tid,
username,
createtime,
commentUsers,
contentObj
)
(lldb) po [$x0 descriptionForKeyPaths]
Class name: WCDataItem, addr: 0x15f5f03b0
tid: 12393001887435993280
username: wxid_z8twcz4o18fg12
createtime: 1477360950
commentUsers: (
)
contentObj: <WCContentItem: 0x15f57d000>
(lldb) po [$x0 sourceUrl]
nil
(lldb) po [$x0 sourceUrl2]
nil

并没有什么有价值的线索,不过注意到WCDataItem里面有一个WCContentItem,看来只能从这儿入手了,去看一下头文件吧!

1
2
3
@property(retain, nonatomic) NSString *linkUrl; 
@property(retain, nonatomic) NSString *linkUrl2;
@property(retain, nonatomic) NSMutableArray *mediaList;

在LLDB打印出来

1
2
3
4
5
6
7
8
(lldb) po [[$x0 valueForKey:@"contentObj"] linkUrl]
https://support.weixin.qq.com/cgi-bin/mmsupport-bin/readtemplate?t=page/common_page__upgrade&v=1
(lldb) po [[$x0 valueForKey:@"contentObj"] linkUrl2]
nil
(lldb) po [[$x0 valueForKey:@"contentObj"] mediaList]
<__NSArrayM 0x15f985e10>(
<WCMediaItem: 0x15dfebdf0>
)

mediaList数组里面有一个WCMediaItem对象,Media一般用来表示视频和音频,大胆猜测就是它了!赶紧找到头文件搜索一遍。

1
2
3
4
5
@property(retain, nonatomic) WCUrl *dataUrl;
- (id)pathForData;
- (id)pathForSightData;
- (id)pathForTempAttachVideoData;
- (id)videoStreamForData;

上面这些属性和方法中pathForSightData是最有可能拿到小视频路径的,继续验证

1
2
3
4
5
6
7
8
9
10
(lldb) po [[[[$x0 valueForKey:@"contentObj"] mediaList] lastObject] dataUrl]
type[1], url[http://vweixinf.tc.qq.com/102/20202/snsvideodownload?filekey=30270201010420301e020166040253480410d14adcddf086f4e131d11a5b1cca1bdf0203039fa00400&bizid=1023&hy=SH&fileparam=302c0201010425302302040fde55e20204580ebd3602024eea02031e8d7d02030f42400204d970370a0201000400], enckey[0], encIdx[-1], token[]
(lldb) po [[[[$x0 valueForKey:@"contentObj"] mediaList] lastObject] pathForData]
/var/mobile/Containers/Data/Application/7C3A6322-1F57-49A0-ACDE-6EF0ED74D137/Library/WechatPrivate/6f696a1b596ce2499419d844f90418aa/wc/media/5/53/8fb0cdd77208de5b56169fb3458b45
(lldb) po [[[[$x0 valueForKey:@"contentObj"] mediaList] lastObject] pathForSightData]
/var/mobile/Containers/Data/Application/7C3A6322-1F57-49A0-ACDE-6EF0ED74D137/Library/WechatPrivate/6f696a1b596ce2499419d844f90418aa/wc/media/5/53/8fb0cdd77208de5b56169fb3458b45.mp4
(lldb) po [[[[$x0 valueForKey:@"contentObj"] mediaList] lastObject] pathForAttachVideoData]
nil
(lldb) po [[[[$x0 valueForKey:@"contentObj"] mediaList] lastObject] videoStreamForData]
nil

拿到小视频的网络url和本地路径了!这里可以用iFunBox或者scp从沙盒拷贝这个文件看看是不是这个cell应该播放的小视频。

1
2
LeonLei-MBP:~ gaoshilei$ scp root@192.168.0.115:/var/mobile/Containers/Data/Application/7C3A6322-1F57-49A0-ACDE-6EF0ED74D137/Library/WechatPrivate/6f696a1b596ce2499419d844f90418aa/wc/media/5/53/8fb0cdd77208de5b56169fb3458b45.mp4 Desktop/
8fb0cdd77208de5b56169fb3458b45.mp4 100% 232KB 231.9KB/s 00:00

用QuickTime打开发现果然是我们要寻找的小视频。再验证一下url是否正确,把上面打印的dataUrl在浏览器中打开,发现也是这个小视频。分析这个类可以得出下面的结论:

  • dataUrl:小视频的网络url
  • pathForData:小视频的本地路径
  • pathForSightData:小视频的本地路径(不带后缀)

至此小视频的路径和取得方式分析已经完成,要实现转发还要继续分析微信的朋友圈发布。

二、实现转发功能

1.“走进死胡同”

这节是我在找小视频转发功能时走的弯路,扒到最后并没有找到实现方法,不过也提供了一些逆向中常用的思路和方法,不想看的可以跳到第二节。

(1)找到小视频拍摄完成调用的方法名称

打开小视频的拍摄界面,用cycript注入,我们要找到发布小视频的方法是哪个,然后查看当前的窗口有哪些window(因为小视频的拍摄并不是在UIApplication的keyWindow中进行的)

1
2
3
4
5
6
7
8
cy# [UIApp windows].toString()
(
"<iConsoleWindow: 0x125f75e20; baseClass = UIWindow; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x125f77b70>; layer = <UIWindowLayer: 0x125df4810>>",
"<SvrErrorTipWindow: 0x127414d40; baseClass = UIWindow; frame = (0 64; 320 45); hidden = YES; gestureRecognizers = <NSArray: 0x12740d930>; layer = <UIWindowLayer: 0x1274030b0>>",
"<MMUIWindow: 0x127796440; baseClass = UIWindow; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x1278083c0>; layer = <UIWindowLayer: 0x127796750>>",
"<UITextEffectsWindow: 0x1270e0d40; frame = (0 0; 320 568); opaque = NO; autoresize = W+H; layer = <UIWindowLayer: 0x1270b4ba0>>",
"<NewYearActionSheet: 0x127797e10; baseClass = UIWindow; frame = (0 0; 320 568); hidden = YES; userInteractionEnabled = NO; layer = <UIWindowLayer: 0x1277d5490>>"
)

发现当前页面一共有5个window,其中MMUIWindow是小视频拍摄所在的window,打印它的UI树状结构

1
cy# [#0x127796440 recursiveDescription]

打印结果比较长,不贴了。找到这个按钮是拍摄小视频的按钮

1
2
3
|    |    |    |    |    | <UIButton: 0x1277a9d70; frame = (89.5 368.827; 141 141); opaque = NO; gestureRecognizers = <NSArray: 0x1277aaeb0>; layer = <CALayer: 0x1277a9600>>
| | | | | | | <UIView: 0x1277aa0a0; frame = (0 0; 141 141); userInteractionEnabled = NO; tag = 252707333; layer = <CALayer: 0x1277aa210>>
| | | | | | | | <UIImageView: 0x1277aa2e0; frame = (0 0; 141 141); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x1277aa490>>

然后执行

1
cy# [#0x1277a9d70 setHidden:YES]

发现拍摄的按钮消失了,验证了我的猜想。寻找按钮的响应事件,可以通过target来寻找

1
2
3
4
5
6
cy# [#0x1277a9d70 allTargets]
[NSSet setWithArray:@[#"<MainFrameSightViewController: 0x1269a4600>"]]]
cy# [#0x1277a9d70 allControlEvents]
193
cy# [#0x1277a9d70 actionsForTarget:#0x1269a4600 forControlEvent:193]
null

发现按钮并没有对应的action,这就奇怪了!UIButton必须要有target和action,不然这个Button不能响应事件。我们试试其他的ControlEvent

1
2
3
4
5
6
cy# [#0x1277a9d70 actionsForTarget:#0x1269a4600 forControlEvent:UIControlEventTouchDown]
@["btnPress"]
cy# [#0x1277a9d70 actionsForTarget:#0x1269a4600 forControlEvent:UIControlEventTouchUpOutside]
@["btnRelease"]
cy# [#0x1277a9d70 actionsForTarget:#0x1269a4600 forControlEvent:UIControlEventTouchUpInside]
@["btnRelease"]

结果发现这三个ContrlEvent有对应的action,我们再看看这三个枚举的值

1
2
3
4
5
6
7
8
9
10
11
12
typedef enum UIControlEvents : NSUInteger {
UIControlEventTouchDown = 1 << 0,
UIControlEventTouchDownRepeat = 1 << 1,
UIControlEventTouchDragInside = 1 << 2,
UIControlEventTouchDragOutside = 1 << 3,
UIControlEventTouchDragEnter = 1 << 4,
UIControlEventTouchDragExit = 1 << 5,
UIControlEventTouchUpInside = 1 << 6,
UIControlEventTouchUpOutside = 1 << 7,
UIControlEventTouchCancel = 1 << 8,
......
} UIControlEvents;

可以看出来UIControlEventTouchDown对应1,UIControlEventTouchUpInside对应128,UIControlEventTouchUpOutside对应64,三者相加正好193!原来调用[#0x1277a9d70 allControlEvents]的时候返回的应该是枚举,有多个枚举则把它们的值相加,是不是略坑?我也是这样觉得的!刚才我们把三种ControlEvent对应的action都打印出来了,继续LLDB+IDA进行动态分析。
#### (2)找到小视频拍摄完成跳转到发布界面的方法
因为要找到小视频发布的方法,所以对应的btnPress函数我们并不关心,把重点放在btnRelease上面,拍摄按钮松开后就会调用的方法。在IDA中找到这个方法
MainFrameSightViewController - (void)btnRelease
找到之后下个断点

1
2
3
4
5
6
7
8
9
10
(lldb) br s -a 0xac000+0x10209369C
Breakpoint 4: where = WeChat`___lldb_unnamed_symbol108894$$WeChat + 32, address = 0x000000010213f69c
Process 3813 stopped
* thread #1: tid = 0xf1ef0, 0x000000010213f69c WeChat`___lldb_unnamed_symbol108894$$WeChat + 32, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
frame #0: 0x000000010213f69c WeChat`___lldb_unnamed_symbol108894$$WeChat + 32
WeChat`___lldb_unnamed_symbol108894$$WeChat:
-> 0x10213f69c <+32>: bl 0x1028d0b60 ; symbol stub for: objc_msgSend
0x10213f6a0 <+36>: cmp w0, #2 ; =2
0x10213f6a4 <+40>: b.ne 0x10213f6dc ; <+96>
0x10213f6a8 <+44>: adrp x8, 5489

用手机拍摄小视频然后松开,触发了断点,说明我们的猜想是正确的。继续分析发现代码是从上图的右边走的,看了一下没有什么方法是跳转到发布视频的,不过仔细看一下有一个block,是系统的延时block,位置在0x102093760。然后我们跟着断点进去,在0x1028255A0跳转到x16所存的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) si
Process 3873 stopped
* thread #1: tid = 0xf62c4, 0x00000001028d9598 WeChat`dispatch_after, queue = 'com.apple.main-thread', stop reason = instruction step into
frame #0: 0x00000001028d9598 WeChat`dispatch_after
WeChat`dispatch_after:
-> 0x1028d9598 <+0>: adrp x16, 1655
0x1028d959c <+4>: ldr x16, [x16, #1056]
0x1028d95a0 <+8>: br x16

WeChat`dispatch_apply:
0x1028d95a4 <+0>: adrp x16, 1655
(lldb) po $x2
<__NSStackBlock__: 0x16fd49f88>

发现传入的参数x2是一个block,我们再回顾一下dispatch_after函数

1
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);

这个函数有三个参数,分别是dispatch_time_t、dispatch_queue_t、dispatch_block_t,那这里打印的x2就是要传入的block,所以我们猜测拍摄完小视频会有一个延时,然后执行刚才传入的block,所以x2中肯定有其他方法调用,下一步就是要知道这个block的位置。

1
2
3
4
5
(lldb) memory read --size 8 --format x 0x16fd49f88
0x16fd49f88: 0x000000019f8fd218 0x00000000c2000000
0x16fd49f98: 0x000000010214777c 0x0000000102fb0e60
0x16fd49fa8: 0x000000015da32600 0x000000015ea1a430
0x16fd49fb8: 0x000000015cf5fee0 0x000000016fd49ff0

0x000000010214777c就是block所在的位置,当然要减掉当前WeChat的ASLR偏移,最终在IDA中的地址为0x10209377C,突然发现这就是btnRelease的子程序sub_10209377C。这个子程序非常简单,只有一个方法selRef_logicCheckState_有可能是我们的目标。先看看这个方法是谁调用的

1
2
3
4
5
6
7
8
9
10
11
12
(lldb) br s -a 0xb4000+0x1020937BC
......
Process 3873 stopped
* thread #1: tid = 0xf62c4, 0x00000001021477bc WeChat`___lldb_unnamed_symbol108895$$WeChat + 64, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
frame #0: 0x00000001021477bc WeChat`___lldb_unnamed_symbol108895$$WeChat + 64
WeChat`___lldb_unnamed_symbol108895$$WeChat:
-> 0x1021477bc <+64>: adrp x8, 5489
0x1021477c0 <+68>: ldr x1, [x8, #1552]
0x1021477c4 <+72>: orr w2, wzr, #0x1
0x1021477c8 <+76>: ldp x29, x30, [sp, #16]
(lldb) po $x0
<MainFrameSightViewController: 0x15d1f0c00>

发现还是MainFrameSightViewController这个对象调用的,那selRef_logicCheckState_ 肯定也在这个类的头文件中,寻找一下果然发现了

1
- (void)logicCheckState:(int)arg1;

在IDA左侧窗口中寻找[MainFrameSightViewController logicCheckState:],发现这个方法超级复杂,逻辑太多了,不着急慢慢捋。
在0x102094D6C位置我们发现一个switch jump,思路就很清晰了,我们只要找到小视频拍摄完成的这条线往下看就行了,LLDB来帮忙看看走的那条线。在0x102094D6C位置下个断点,这个断点在拍摄小视频的时候会多次触发,可以在拍摄之前把断点dis掉,拍摄松手之前再启用断点,打印此时的x8值

1
2
(lldb) p/x $x8
(unsigned long) $38 = 0x0000000102174e10

x8是一个指针,它指向的地址是0x102174e10,用这个地址减去当前ASLR的偏移就可以找到在IDA中的基地址,发现是0x102094E10,拍摄完成的逻辑处理这条线找到了,一直走到0x102094E24位置之后跳转0x1020951C4,这个分支的内容较少,里面有三个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
loc_1020951C4
ADRP X8, #selRef_hideTips@PAGE
LDR X1, [X8,#selRef_hideTips@PAGEOFF]
MOV X0, X19
BL _objc_msgSend
ADRP X8, #selRef_finishWriter@PAGE
LDR X1, [X8,#selRef_finishWriter@PAGEOFF]
MOV X0, X19
BL _objc_msgSend
ADRP X8, #selRef_turnCancelBtnForFinishRecording@PAGE
LDR X1, [X8,#selRef_turnCancelBtnForFinishRecording@PAGEOFF]
MOV X0, X19
BL _objc_msgSend
B loc_102095288

其中selRef_finishWriterselRef_turnCancelBtnForFinishRecording需要重点关注,这两个方法看上去都是小视频录制结束的意思,线索极有可能就在这两个函数中。通过查看调用者发现这两个方法都属于MainFrameSightViewController,继续在IDA中查看这两个方法。在selRef_finishWriter 中靠近末尾0x102094248的位置发现一个方法名叫做f_switchToSendingPanel,下个断点,然后拍摄视频,发现这个方法并没有被触发。应该不是通过这个方法调用发布界面的,继续回到selRef_finishWriter 方法中;在0x1020941DC的位置调用方法selRef_stopRecording,打印它的调用者发现这个方法属于SightFacade,继续在IDA中寻找这个方法的实现。在这个方法的0x101F9BED4位置又调用了selRef_stopRecord,同样打印调用者发现这个方法属于SightCaptureLogicF4,有点像剥洋葱,继续在寻找这个方法的实现。在这个方法内部0x101A98778位置又调用了selRef_finishWriting,同样的原理找到这个方法是属于SightMovieWriter。已经剥了3层了,继续往下:
SightMovieWriter - (void)finishWriting中的0x10261D004位置分了两条线,这个位置下个断点,然后拍摄完小视频触发断点,打印x19的值

1
2
(lldb) po $x19
<OS_dispatch_queue: CAPTURE.CALLBACK[0x13610bcd0] = { xrefcnt = 0x4, refcnt = 0x4, suspend_cnt = 0x0, locked = 1, target = com.apple.root.default-qos.overcommit[0x1a0aa3700], width = 0x0, running = 0x0, barrier = 1 }>

所以代码不会跳转到loc_10261D054而是走的左侧,在左侧的代码中发现启用了一个block,这个block是子程序sub_10261D0AC,地址为0x10261D0AC,找到这个地址,结构如下图所示:
sub_10261D0AC
可以看出来主要分两条线,我们在第一个方框的末尾也就是0x10261D108位置下个断点,等拍摄完毕触发断点之后打印x0的值为1,这里的汇编代码为

1
2
__text:000000010261D104                 CMP             X0, #2
__text:000000010261D108 B.EQ loc_10261D234

B.EQ是在上一步的结果为0才会跳转到loc_10261D234,但是这里的结果是不为0的,将x0的值改为2让上一步的结果为0

1
2
3
4
5
(lldb) po $x0
1
(lldb) register write $x0 2
(lldb) po $x0
2

此时放开断点,等待跳转到小视频发布界面,结果是一直卡在这个界面没有任何反应,所以猜测实现跳转的逻辑应该在右边的那条线,继续顺着右边的线寻找:
在右侧0x10261D3AC位置发现调用了下面的这个方法

1
- (void)finishWritingWithCompletionHandler:(void (^)(void))handler;

这个方法是系统提供的AVAssetWriter里面的方法,在视频写入完成之后要做的操作,这个里是要传入一个block的,因为只有一个参数所以对应的变量是x2,打印x2的值

1
2
3
4
5
6
7
(lldb) po $x2
<__NSStackBlock__: 0x16e086c78>
(lldb) memory read --size 8 --format x 0x16e086c78
0x16e086c78: 0x00000001a0aa5218 0x00000000c2000000
0x16e086c88: 0x00000001026d94b0 0x0000000102fc98c0
0x16e086c98: 0x0000000136229fd0 0x000000016e086d00
0x16e086ca8: 0x00000001997f5318 0xfffffffec9e882ff

并且通过栈内存找到block位置为0x10261D4B0(需要减去ASLR的偏移)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sub_10261D4B0
var_20= -0x20
var_10= -0x10
STP X20, X19, [SP,#var_20]!
STP X29, X30, [SP,#0x20+var_10]
ADD X29, SP, #0x20+var_10
MOV X19, X0
LDR X0, [X19,#0x20]
ADRP X8, #selRef_stopAmr@PAGE
LDR X1, [X8,#selRef_stopAmr@PAGEOFF]
BL _objc_msgSend
LDR X0, [X19,#0x20]
ADRP X8, #selRef_compressAudio@PAGE
LDR X1, [X8,#selRef_compressAudio@PAGEOFF]
LDP X29, X30, [SP,#0x20+var_10]
LDP X20, X19, [SP+0x20+var_20],#0x20
B _objc_msgSend
; End of function sub_10261D4B0

只调用了两个方法,一个是selRef_stopAmr停止amr(一种音频格式),另一个是selRef_compressAudio压缩音频,拍摄完成的下一步操作应该不会放在这两个方法里面,找了这么久也没有头绪,这个路看来走不通了,不要钻牛角尖,战略性撤退寻找其他入口。
逆向的乐趣就是一直寻找真相的路上,能体会到成功的乐趣,也有可能方向错了离真相反而越来越远,不要气馁调整方向继续前进!

2.“另辟蹊径”

(由于微信在后台偷偷升级了,下面的内容都是微信6.3.30版本的ASLR,上面的分析基于6.3.28版本)

注意到在点击朋友圈右上角的相机按钮底部会弹出一个Sheet,第一个就是Sight小视频,从这里入手,用cycript查看Sight按钮对应的事件是哪个

1
2
3
4
5
6
7
8
9
10
11
iPhone-5S:~ root# cycript -p "WeChat"
cy# [UIApp windows].toString()
`(
"<iConsoleWindow: 0x14d6ccc00; baseClass = UIWindow; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x14d7df110>; layer = <UIWindowLayer: 0x14d7d6f60>>",
"<SvrErrorTipWindow: 0x14eaa5800; baseClass = UIWindow; frame = (0 0; 320 45); hidden = YES; gestureRecognizers = <NSArray: 0x14e9e8950>; layer = <UIWindowLayer: 0x14e9e6510>>",
"<UITextEffectsWindow: 0x14ec38ba0; frame = (0 0; 320 568); opaque = NO; autoresize = W+H; layer = <UIWindowLayer: 0x14ec39360>>",
"<UITextEffectsWindow: 0x14e9c67a0; frame = (0 0; 320 568); layer = <UIWindowLayer: 0x14d683ff0>>",
"<UIRemoteKeyboardWindow: 0x14f226e40; frame = (0 0; 320 568); opaque = NO; autoresize = W+H; layer = <UIWindowLayer: 0x14d6f4de0>>",
"<NewYearActionSheet: 0x14f1704a0; baseClass = UIWindow; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x14ef9bf90>; layer = <UIWindowLayer: 0x14ef61a20>>"
)`
cy# [#0x14f1704a0 recursiveDescription].toString()

底部的Sheet是NewYearActionSheet,然后打印NewYearActionSheet的UI树状结构图(比较长不贴了)。然后找到Sight对应的UIButton是0x14f36d470

1
2
3
4
5
6
cy# [#0x14f36d470 allTargets]
[NSSet setWithArray:@[#"<NewYearActionSheet: 0x14f1704a0; baseClass = UIWindow; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x14ef9bf90>; layer = <UIWindowLayer: 0x14ef61a20>>"]]]
cy# [#0x14f36d470 allControlEvents]
64
cy# [#0x14f36d470 actionsForTarget:#0x14f1704a0 forControlEvent:64]
@["OnDefaultButtonTapped:"]

通过UIControl的actionsForTarget:forControlEvent:方法可以找到按钮绑定的事件,Sight按钮的触发方法为OnDefaultButtonTapped:,回到IDA中在NewYearActionSheet中找到这个方法们继续往下分析只有这个方法selRef_dismissWithClickedButtonIndex_animated,通过打印它的调用者发现还是NewYearActionSheet,继续点进去找到newYearActionSheet_clickedButtonAtIndex方法,看样子不是NewYearActionSheet自己的,打印调用者x0发现它属于类WCTimeLineViewController。跟着断点走下去在0x1012B78CC位置调用了方法#selRef_showSightWindowForMomentWithMask_byViewController_scene
通过观察发现这个方法的调用者是0x1012B78AC这个位置的返回值x0,这是一个类SightFacade,猜测这个方法在SightFacade里面,去头文件里找一下果然发现这个方法

1
- (void)showSightWindowForMomentWithMask:(id)arg1 byViewController:(id)arg2 scene:(int)arg3;

这个方法应该就是跳转到小视频界面的方法了。下面分别打印它的参数

1
2
3
4
5
6
7
8
(lldb) po $x2
<UIImage: 0x14f046660>, {320, 568}
(lldb) po $x3
<WCTimeLineViewController: 0x14e214800>
(lldb) po $x4
2
(lldb) po $x0
<SightFacade: 0x14f124b40>

其中x2、x3、x4分别对应三个参数,x0是调用者,跳到这个方法内部查看怎么实现的。发现在这个方法中进行了小视频拍摄界面的初始化工作,首先初始化一个MainFrameSightViewController,再创建一个UINavigationController将MainFrameSightViewController放进去,接下来初始化一个MMWindowController调用

1
- (id)initWithViewController:(id)arg1 windowLevel:(int)arg2;

方法将UINavigationController放了进去,完成小视频拍摄界面的所有UI创建工作。
拍摄完成之后进入发布界面,此时用cycript找到当前的Controller是SightMomentEditViewController,由此萌生一个想法,跳过前面的拍摄界面直接进入发布界面不就可以了吗?我们自己创建一个SightMomentEditViewController然后放到UINavigationController里面,然后再将这个导航控制器放到MMWindowController里面。(这里我已经写好tweak进行了验证,具体的tweak思路编写在后文有)结果是的确可以弹出发布的界面,但是导航栏的NavgationBar遮住了原来的,整个界面是透明的,很难看,而且发布完成之后无法销毁整个MMWindowController,还是停留在发布界面。我们要的结果不是这个,不过确实有很大的收获,最起码可以直接调用发布界面了,小视频也能正常转发。我个人猜测,当前界面不能被销毁的原因是因为MMWindowController新建了一个window,它跟TimeLine所在的keyWindow不是同一个,SightMomentEditViewController的按钮触发的方法是没有办法销毁这个window的,所以有一个大胆的猜想,我直接在当前的WCTimeLineViewController上把SightMomentEditViewController展示出来不就可以了吗?

1
2
[WCTimelineVC presentViewController:editSightVC animated:YES completion:^{
}];

像这样展示岂不妙哉?不过通过观察SightMomentEditViewController的头文件,结合小视频发布时界面上的元素,推测创建这个控制器至少需要两个属性,一个是小视频的路径,另一个是小视频的缩略图,将这两个关键属性给了SightMomentEditViewController那么应该就可以正常展示了

1
2
3
4
5
6
7
8
9
SightMomentEditViewController *editSightVC = [[%c(SightMomentEditViewController) alloc] init];
NSString *localPath = [[self iOSREMediaItemFromSight] pathForSightData];
UIImage *image = [[self valueForKey:@"_sightView"] getImage];
[editSightVC setRealMoviePath:localPath];
[editSightVC setMoviePath:localPath];
[editSightVC setRealThumbImage:image];
[editSightVC setThumbImage:image];
[WCTimelineVC presentViewController:editSightVC animated:YES completion:^{
}];

小视频的发布界面可以正常显示并且所有功能都可以正常使用,唯一的问题是返回按钮没有效果,并不能销毁SightMomentEditViewController。用cycript查看左侧按钮的actionEvent找到它的响应函数是- (void)popSelf;,点击左侧返回触发的是pop方法,但是这个控制器并不在navgationController里面,所以无效,我们要对这个方法进行重写

1
2
3
4
5
6
- (void)popSelf
{
[self dismissViewControllerAnimated:YES completion:^{

}];
}

此时再点击返回按钮就可以正常退出了,此外,在WCContentItemViewTemplateNewSight中发现了一个方法叫做- (void)sendSightToFriend;,可以直接将小视频转发给好友,至此小视频转发的功能已经找到了。