前言
此文为逆向微信二进制文件,实现朋友圈小视频转发的教程,从最开始的汇编代码入手到最后重签名安装等操作,手把手教你玩转微信!学会之后再去逆向微信其他功能易如反掌。
本篇文章由于篇幅太长分成了两篇,上篇讲解的是逆向工作,也就是怎么找到相关的函数和方法实现,下篇讲解的是怎么在非越狱机重签名安装和越狱机tweak安装的详细过程。
正文的第二部分还提供了微信自动抢红包、修改微信步数的代码,这些都可以照葫芦画瓢按照本文的套路一步步逆向找到,这里就不再赘述。
在实践之前,需要准备好一部越狱的手机,然后将下文列出的所有工具安装好。IDA跟Reveal都是破解版,IDA的正版要2000多刀,对于这么牛逼的逆向工具确实物有所值,不过不是专门研究逆向的公司也没必要用正版的,下个Windows的破解版就好,Mac上暂时没找到。Mac上可以用hopper代替IDA,也是一款很牛逼的逆向工具。废话不多说,正式开始吧!
转载请注明出处:来自LeonLei的博客http://www.gaoshilei.com
逆向微信朋友圈(上篇)
一、获取朋友圈的小视频
注意:本文逆向的微信的二进制文件为6.3.28版本,如果是不同的微信版本,二进制文件中的基地址也不相同
本文涉及到的工具
- cycript
- LLDB与debugserver(Xcode自带)
- OpenSSH
- IDA
- Reveal
- theos
- CydiaSubstrate
- iOSOpenDev
- ideviceinstaller
- tcprelay(本地端口映射,USB连接SSH,不映射可通过WiFi连接)
- dumpdecrypted
- class-dump
- iOS App Signer
- 编译好的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查看小视频的播放窗口
可以看出来WCContentItemViewTemplateNewSigh这个对象是小视频的播放窗口,它的subView有WCSightView,SightView、SightPlayerView,这几个类就是我们的切入点。
保存视频到favorite的时候是长按视频弹出选项的,那么在WCContentItemViewTemplateNewSight这个类里面可能有手势相关的方法,去刚才导出的头文件中找线索。
1 | - (void)onLongTouch; |
这几个方法跟长按手势相关,再去IDA中找到这些函数,逐个查看。onLongPressedWCSight和onLongPressedWCSightFullScreenWindow都比较简单,onLongTouch比较长,而且发现了内部调用了方法Favorites_Add,因为长按视频的时候出来一个选项就是Favorites,并且我看到这个函数调用
1 | ADRP X8, #selRef_sightVideoPath@PAGE |
这里拿到了小视频的地址,可以推测这个函数跟收藏有关,下面打断点测试。
1 | (lldb) im li -o -f |
可以看到WeChat的ASLR为0x3c000,在IDA查找到这三个函数的基地址,分别下断点
1 | (lldb) br s -a 0x1020D3A10+0x3c000 |
回到微信里面长按小视频,看断点触发情况
1 | Process 3721 stopped |
发现断点2先被触发,接着触发断点1,后面断点2和1又各触发了1次,断点3一直很安静。可以排除onLongPressedWCSightFullScreenWindow与收藏小视频的联系。小视频的踪影就要在剩下的两个方法中寻找了。通过V找到C,顺藤摸瓜找到M屡试不爽!用cycript注入WeChat,拿到播放小视频的view所在的Controller。
1 | cy# [#0x138c18030 nextResponder] |
通过响应者链条找到
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 | - (void)genABTestTipCell:(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 | LeonLei-MBP:~ gaoshilei$ lldb |
所以我们下断点的位置是0x50000+0x101287CE4
1 | (lldb) br s -a 0x50000+0x101287CE4 |
打印x0的值
1 | (lldb) po $x0 |
得到一个WCDataItem的对象,这里x0的值就是selRef_getTimelineDataItemOfIndex_
执行完的返回值,然后把x0的值改掉
1 | (lldb) register write $x0 0 |
此时会发现我们要刷新的那条小视频内容全部为空
到这里已经找到了小视频的源数据获取方法,问题是我们怎么拿到这个WCDataItem呢?继续看IDA分析函数的调用情况:
WCTimeLineViewController - (void)genNormalCell:(id) indexPath:(id)
1 | __text:0000000101287BCC STP X28, X27, [SP,#var_60]! |
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
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 | @property(retain, nonatomic) NSString *sourceUrl2; |
回到LLDB中,用断点打印这些值,看看有什么。
1 | (lldb) po [$x0 keyPaths] |
并没有什么有价值的线索,不过注意到WCDataItem里面有一个WCContentItem,看来只能从这儿入手了,去看一下头文件吧!
1 | @property(retain, nonatomic) NSString *linkUrl; |
在LLDB打印出来
1 | (lldb) po [[$x0 valueForKey:@"contentObj"] linkUrl] |
mediaList数组里面有一个WCMediaItem对象,Media一般用来表示视频和音频,大胆猜测就是它了!赶紧找到头文件搜索一遍。
1 | @property(retain, nonatomic) WCUrl *dataUrl; |
上面这些属性和方法中pathForSightData
是最有可能拿到小视频路径的,继续验证
1 | (lldb) po [[[[$x0 valueForKey:@"contentObj"] mediaList] lastObject] dataUrl] |
拿到小视频的网络url和本地路径了!这里可以用iFunBox或者scp从沙盒拷贝这个文件看看是不是这个cell应该播放的小视频。
1 | 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/ |
用QuickTime打开发现果然是我们要寻找的小视频。再验证一下url是否正确,把上面打印的dataUrl在浏览器中打开,发现也是这个小视频。分析这个类可以得出下面的结论:
- dataUrl:小视频的网络url
- pathForData:小视频的本地路径
- pathForSightData:小视频的本地路径(不带后缀)
至此小视频的路径和取得方式分析已经完成,要实现转发还要继续分析微信的朋友圈发布。
二、实现转发功能
1.“走进死胡同”
这节是我在找小视频转发功能时走的弯路,扒到最后并没有找到实现方法,不过也提供了一些逆向中常用的思路和方法,不想看的可以跳到第二节。
(1)找到小视频拍摄完成调用的方法名称
打开小视频的拍摄界面,用cycript注入,我们要找到发布小视频的方法是哪个,然后查看当前的窗口有哪些window(因为小视频的拍摄并不是在UIApplication的keyWindow中进行的)
1 | cy# [UIApp windows].toString() |
发现当前页面一共有5个window,其中MMUIWindow是小视频拍摄所在的window,打印它的UI树状结构
1 | cy# [#0x127796440 recursiveDescription] |
打印结果比较长,不贴了。找到这个按钮是拍摄小视频的按钮
1 | | | | | | | <UIButton: 0x1277a9d70; frame = (89.5 368.827; 141 141); opaque = NO; gestureRecognizers = <NSArray: 0x1277aaeb0>; layer = <CALayer: 0x1277a9600>> |
然后执行
1 | cy# [#0x1277a9d70 setHidden:YES] |
发现拍摄的按钮消失了,验证了我的猜想。寻找按钮的响应事件,可以通过target来寻找
1 | cy# [#0x1277a9d70 allTargets] |
发现按钮并没有对应的action,这就奇怪了!UIButton必须要有target和action,不然这个Button不能响应事件。我们试试其他的ControlEvent
1 | cy# [#0x1277a9d70 actionsForTarget:#0x1269a4600 forControlEvent:UIControlEventTouchDown] |
结果发现这三个ContrlEvent有对应的action,我们再看看这三个枚举的值
1 | typedef enum UIControlEvents : NSUInteger { |
可以看出来UIControlEventTouchDown对应1,UIControlEventTouchUpInside对应128,UIControlEventTouchUpOutside对应64,三者相加正好193!原来调用[#0x1277a9d70 allControlEvents]
的时候返回的应该是枚举,有多个枚举则把它们的值相加,是不是略坑?我也是这样觉得的!刚才我们把三种ControlEvent对应的action都打印出来了,继续LLDB+IDA进行动态分析。
#### (2)找到小视频拍摄完成跳转到发布界面的方法
因为要找到小视频发布的方法,所以对应的btnPress
函数我们并不关心,把重点放在btnRelease
上面,拍摄按钮松开后就会调用的方法。在IDA中找到这个方法
找到之后下个断点
1 | (lldb) br s -a 0xac000+0x10209369C |
用手机拍摄小视频然后松开,触发了断点,说明我们的猜想是正确的。继续分析发现代码是从上图的右边走的,看了一下没有什么方法是跳转到发布视频的,不过仔细看一下有一个block,是系统的延时block,位置在0x102093760。然后我们跟着断点进去,在0x1028255A0跳转到x16所存的地址
1 | (lldb) si |
发现传入的参数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 | (lldb) memory read --size 8 --format x 0x16fd49f88 |
0x000000010214777c就是block所在的位置,当然要减掉当前WeChat的ASLR偏移,最终在IDA中的地址为0x10209377C,突然发现这就是btnRelease
的子程序sub_10209377C。这个子程序非常简单,只有一个方法selRef_logicCheckState_
有可能是我们的目标。先看看这个方法是谁调用的
1 | (lldb) br s -a 0xb4000+0x1020937BC |
发现还是MainFrameSightViewController这个对象调用的,那selRef_logicCheckState_
肯定也在这个类的头文件中,寻找一下果然发现了
1 | - (void)logicCheckState:(int)arg1; |
在IDA左侧窗口中寻找[MainFrameSightViewController logicCheckState:],发现这个方法超级复杂,逻辑太多了,不着急慢慢捋。
在0x102094D6C位置我们发现一个switch jump,思路就很清晰了,我们只要找到小视频拍摄完成的这条线往下看就行了,LLDB来帮忙看看走的那条线。在0x102094D6C位置下个断点,这个断点在拍摄小视频的时候会多次触发,可以在拍摄之前把断点dis掉,拍摄松手之前再启用断点,打印此时的x8值
1 | (lldb) p/x $x8 |
x8是一个指针,它指向的地址是0x102174e10,用这个地址减去当前ASLR的偏移就可以找到在IDA中的基地址,发现是0x102094E10,拍摄完成的逻辑处理这条线找到了,一直走到0x102094E24位置之后跳转0x1020951C4,这个分支的内容较少,里面有三个函数
1 | loc_1020951C4 |
其中selRef_finishWriter
和selRef_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 | (lldb) po $x19 |
所以代码不会跳转到loc_10261D054而是走的左侧,在左侧的代码中发现启用了一个block,这个block是子程序sub_10261D0AC,地址为0x10261D0AC,找到这个地址,结构如下图所示:
可以看出来主要分两条线,我们在第一个方框的末尾也就是0x10261D108位置下个断点,等拍摄完毕触发断点之后打印x0的值为1,这里的汇编代码为
1 | __text:000000010261D104 CMP X0, #2 |
B.EQ是在上一步的结果为0才会跳转到loc_10261D234,但是这里的结果是不为0的,将x0的值改为2让上一步的结果为0
1 | (lldb) po $x0 |
此时放开断点,等待跳转到小视频发布界面,结果是一直卡在这个界面没有任何反应,所以猜测实现跳转的逻辑应该在右边的那条线,继续顺着右边的线寻找:
在右侧0x10261D3AC位置发现调用了下面的这个方法
1 | - (void)finishWritingWithCompletionHandler:(void (^)(void))handler; |
这个方法是系统提供的AVAssetWriter里面的方法,在视频写入完成之后要做的操作,这个里是要传入一个block的,因为只有一个参数所以对应的变量是x2,打印x2的值
1 | (lldb) po $x2 |
并且通过栈内存找到block位置为0x10261D4B0(需要减去ASLR的偏移)
1 | sub_10261D4B0 |
只调用了两个方法,一个是selRef_stopAmr
停止amr(一种音频格式),另一个是selRef_compressAudio
压缩音频,拍摄完成的下一步操作应该不会放在这两个方法里面,找了这么久也没有头绪,这个路看来走不通了,不要钻牛角尖,战略性撤退寻找其他入口。
逆向的乐趣就是一直寻找真相的路上,能体会到成功的乐趣,也有可能方向错了离真相反而越来越远,不要气馁调整方向继续前进!
2.“另辟蹊径”
(由于微信在后台偷偷升级了,下面的内容都是微信6.3.30版本的ASLR,上面的分析基于6.3.28版本)
注意到在点击朋友圈右上角的相机按钮底部会弹出一个Sheet,第一个就是Sight小视频,从这里入手,用cycript查看Sight按钮对应的事件是哪个
1 | iPhone-5S:~ root# cycript -p "WeChat" |
底部的Sheet是NewYearActionSheet,然后打印NewYearActionSheet的UI树状结构图(比较长不贴了)。然后找到Sight对应的UIButton是0x14f36d470
1 | cy# [#0x14f36d470 allTargets] |
通过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 | (lldb) po $x2 |
其中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 | [WCTimelineVC presentViewController:editSightVC animated:YES completion:^{ |
像这样展示岂不妙哉?不过通过观察SightMomentEditViewController的头文件,结合小视频发布时界面上的元素,推测创建这个控制器至少需要两个属性,一个是小视频的路径,另一个是小视频的缩略图,将这两个关键属性给了SightMomentEditViewController那么应该就可以正常展示了
1 | SightMomentEditViewController *editSightVC = [[%c(SightMomentEditViewController) alloc] init]; |
小视频的发布界面可以正常显示并且所有功能都可以正常使用,唯一的问题是返回按钮没有效果,并不能销毁SightMomentEditViewController。用cycript查看左侧按钮的actionEvent找到它的响应函数是- (void)popSelf;
,点击左侧返回触发的是pop方法,但是这个控制器并不在navgationController里面,所以无效,我们要对这个方法进行重写
1 | - (void)popSelf |
此时再点击返回按钮就可以正常退出了,此外,在WCContentItemViewTemplateNewSight中发现了一个方法叫做- (void)sendSightToFriend;
,可以直接将小视频转发给好友,至此小视频转发的功能已经找到了。