首页
社区
课程
招聘
[原创]逆向及修复最新iOS版少数派客户端的闪退 bug
发表于: 2018-6-13 16:06 14027

[原创]逆向及修复最新iOS版少数派客户端的闪退 bug

2018-6-13 16:06
14027

说明
该文章为本人原创写作于去年,简书和掘金上的文章皆为本人发表,现将其发表于看雪。

少数派是国内最大的一个分析高品质数字消费指南的平台,致力于更好地运用数字产品或科学方法,帮助用户提升工作效率和生活品质。当推出iOS版本后,我立刻进行了下载和使用,作为一个开发者,首先必须是一个数字商品的消费者。最近期的一次更新中,发现了一个比较严重的bug,于是我利用逆向知识,对其进行了分析。

问题描述:

直观来说闪退最主要的原因有:找不到方法的实现,坏内存访问等。平时在使用Xcode开发自己的 app 时,可以直接在Xcode中快速找到这些 crash 的原因,相信这些定位崩溃的关键字大家已经很熟悉了。那么如何在没有源码的情况下定位这些 crash?

我们当然是直接使用lldb啦,

基本操作,


lldb后,使用c命令运行程序,操作触发崩溃后可以看到如下输出:

可以看到我们非常熟悉的关键字:reason=EXE_BAD_ACCESS。由此可以判断崩溃是由于访问坏内存导致的。

使用命令bt打印调用堆栈

可以看到,程序是运行到一个WebKit的内部方法[WKScrollViewDelegateForwarder forwardingTargetForSelector:]之后访问换内存导致闪退的。天哪,我不会发现了一个Apple API的bug吧,继续往下分析。

可能是sspai设置了WKWebView的delegate。当然现在只是猜想,之后需要通过Hopper来看一下这个sspai的Mach-O文件。

根据崩溃的发生位置和时间

时间发生在进入文章后返回,这涉及到了两个控制器,可能是两个控制器之间的delegate

逆向后知道类名如下:

因为是从HomeTableViewController进入的ArticleViewController,所以我们需要在HomeTableViewController.h文件中查找这个转跳入口,可以在Hopper中看到这个turnToArticleViewController:cell:非常可疑,根据我们的正向开发经验,通过这个方法名turnTo vc转跳并用cell参数传递了一个数据model

Hopper中查看方法如下:

我在其中加入了一些方法调用注释,可以看到方法的实现内容很简单,即使不懂汇编,通过这些@selector和正向经验也可以快速推断出来。
主要做了以下事情:

Hopper中查看到方法initWithArticle:如下,看到其中一段如下:

可以看到,内部通过systemVersionAPI判断了当前系统版本,然后决定调用loadUIWebView方法使用UIWebView,或者调用loadWkWebView来使用WKWebView
因为我手机是iOS9系统,所以应该是调用的loadWkWebView来初始化WKWebView,联想到之前在奔溃堆栈中看到的WKScrollViewDelegateForwarder方法,可能是在初始化配置WKWebViewloadWkWebView方法中出现了bug。

我们先不急着分析loadWkWebView方法的内部实现,首先需要验证一下我们的猜想,是否因为使用WKWebView导致的crash发生,在这里我们取个巧,使用theos创建一个本文的插件地址,直接hookloadWkWebView方法,然后在其中调用loadUIWebView方法:

编译运行后发现确实不存在坏内存访问的问题。
所以可以确定,确实是loadWkWebView方法中的一些代码导致了crash
到这里已经找到了解决bug的方法,如果就这样结束了,也许我就不写这篇文章了,我决定继续往下分析

我们在Hopper中查看方法loadWkWebView的内部实现,汇编代码真是又臭又长,但是为了读者也可以直接在文章中进行分析,我还是觉得将该方法的所有汇编代码贴出来:

可以看到内部的实现也不复杂,为了可以分析出 crash 点,我觉得hook掉这个方法,然后根据汇编代码重写这个方法的实现,来确定具体的问题代码(没有源码的调试定位bug确实麻烦,但是也很有意义)。

重写如下:

方法内主要是配置了WKWebView,然后将其添加到了控制器的view上。根据崩溃时的信息,着重注意有关delegate的设置,主要有三个:wkView.setUIDelegatewkView.setNavigationDelegatewkView.scrollView.delegate
通过逐一注释这些方法后测试来定位,最后发现在注释wkView.scrollView.delegate方法后程序没有 crash。
仔细思考可以发现这三个方法的调用,只有第三个不是直接使用Apple提供的API,查看WKWebView的文档内容可以发现,scrollViewWKWebView内部的一个属性。

⚠️:这里也告诉我们,调用一个API内部的属性其实是有风险的,因为在我们使用了内部属性后,我们并不知道这是否会影响其API内部对这个属性的使用,特别是这里是设置了内部属性的delegate

在这里我们可以大胆假设一下,这次程序的 crash 可能是因为Apple内部,对我们设置给scrollViewdelegate进行了调用,而此时该delegate已经被释放了(因为之前的判断是这次的 crash 是坏内存访问引起的)。

我们还可以简单看一下,程序设置scrollView的原因,可以在类中发现被遵守的三个代理方法:scrollViewDidScroll:;scrollViewWillBeginDragging:;scrollViewDidEndDragging:withDecelerate:,进入方法内部可以发现,程序是通过监听了scrollView的滚动状态来设置canShowImageInfo:属性。(据我所知,确实WKWebView没有提供外界接口来监听scrollView的滚动状态,所以该程序的开发者使用了这个直接了当的方法)。

当然,如果就这样结束分析,是说服不了我自己的(毕竟处女座是有脾(jie)气(pi)的)。
在继续分析之前,再次确认找到的这个 crash 点。在调用程序loadWkWebView之后,将wkView.scrollView.delegate设置为nil看看是否也不会 crash。

编译运行后发现,也不会 crash,所以这个时候可以判断这个 crash 点的准确性了。

仿佛已经可以结束了?但是作为处女座的我还是想知道到底是什么原因直接导致的 crash,或者说既然是访问了坏内存,那么程序到底是访问了哪个被释放的对象的内存。这个时候可能我们都会想到开启Address Sanitizer或者Zombie Objects来看看,但是我们没有源码!(之前在一个国外的博客中看到了,可以在开发tweaks的时候,开启Zombie Objects来观察整个被 hook app的内存,但是记忆模糊,找起来也麻烦)。并且之前已经逆向重写了整个loadWkWebView方法,所以干脆直接 copy 写一个 demo 。回归 Xcode 总是好的,将问题代码从app中分离到新的 demo 中也可以再次确认是否是这段代码出现了问题。

快速创建 demo ,可以在链接中找到这个 demo 的完整代码。代码很简单,首页控制器ViewController一个 UIButton转跳到SecondViewController控制器,SecondViewController内部的viewDidLoad方法,直接使用之前逆向的代码段来加载一篇少数派的文章。程序运行前先让我们愉快的打上全局断点,在程序运行后,发现程序确实崩溃了,而且停留在了:

那么让我们开启Address Sanitizer或者Zombie Objects,然后运行程序:

确实程序访问了一个坏内存,对象为SecondViewController,调用了retain方法。这个时候,特别困惑,demo非常简单,只有两个控制器的转跳,逻辑清晰,是谁在SecondViewController销毁后还在调用它,应该不是demo程序自身的对象调用了这个被销毁的对象,查看后发现,

查看堆栈后发现,确实不是我们的demo访问的坏内存,访问对象在CoreFoundation的 image 中,并且根据截图可以发现,WebKit框架在WKWebViewdealloc的时候调用了WKScrollView的私有方法_updateDelegate来更新 delegate。根据截图可以猜测_updateDelegate的内部应该是获取到了属性scrollView然后setDelegate。并且在设置delegate的时候retain保留了原来的delegate([secondViewController retain])。

根据截图,我们断两个符号断点后运行程序:

运行程序后,可以发现,在我们转跳到SecondViewController的时候断在了_updateDelegatec后如预期的断在了setDelegate,这个时候我们可以使用 lldb,来查看一下调用者和delegate参数值:

如下:
[WKScrollView setDelegate: WKWebView];
在这里我们发现,调用者并不是我们熟悉的UIScrollView类型,应该是一个私有类,然后我们可以在WKWebView的官方文档中查看:

UIScrollView类型(难道。。?没有难道),让我们查看一下两者是否如我们想的一样:

事实证明,两者是同一个对象,WKWebView的属性scrollView确实是一个WKScrollView类型的私有属性,只是苹果在文档中声明成了通用父类UIScrollView

既然WKWebView已经是scrollView的代理,我们是否可以在WKWebView中实现scrollView的代理方法(如果Apple没有实现的话),然后通过runtime添加代理属性来转发监听信息到我们自己的控制器(稍后可以尝试一下)

继续分析,这里开始因为程序重新运行,所以内存地址会与之前的不符合,但是没有关系。这次我们在SecondViewController中的dealloc方法中下断点,然后获取SecondViewController的内存地址:(之后用来判断坏内存的对象是否是这个SecondViewController

继续运行程序,断点会停留在_updateDelegate,然后c运行到setDelegate,根据之前的判断,是因为对坏内存调用了retain,所以我们让程序继续运行到第一个retain的地方:

然后进入retain函数内部:


如截图,使用po打印调用者发现输出的不是对象,并且有很明确的提示,访问了一个被释放的对象,使用p/x输出内存地址,发现跟之前保存的SecondViewController的内存地址一致,所以可以更加断定这个程序的 crash 是由于访问了被释放的SecondViewController对象造成的。
多一次确认肯定没错,现在我们让程序运行到objc_msgSend来调用这个retain方法,

运行下一行:

可以发现立马崩溃了。

The issue is when I call [viewController popViewControllerAnimated], it will crash on [UIScrollView setDelegate:]. I have fixed the issue by add viewController.UIView.WKWebView.scrollView.delegate = nil; in viewController's dealloc.

但是在写这篇文章,整理思路的时候,我发现这样直接修改api内部,并不是一个很好的解决方法,因为之前逆向发现,WKWebView已经是scrollView的代理,所以我决定通过给WKWebView添加分类的方法来监听scrollView的滚动。

之后,只需要设置scrollViewDelegate代理即可。因为我们是在分类中添加的scrollView的代理方法,如果原来Apple已经在WKWebView中实现了scrollView的代理方法?毕竟Apple不会无缘无故将WKWebView设置为scrollView的代理,它肯定是有效果要实现,我将WebKit拖到Hopper中发现,确实如此:

在 demo 中测试,发现我们确实可以监听scrollView滚动。但是因为我不知道原来的WKWebView的监听滚动用来实现怎么样的效果,所以无法确定原来的监听是否依然有效。当分类和原类定义一个同一个方法时,运行时只有一个方法会被调用。从逆向的角度出发,我想直接 hook WKWebView的滚动监听方法,然后调用原来方法的同时,实现自己的监听通知。

如上使用method_exchangeImplementations来实现hook,苹果会检查上架 app 的符号表,我们的实现并没有涉及到私有函数或属性,我想应该不会被拒吧?因为我也不是很熟悉 Apple 的审核规则,需要有大神可以补充解答。

第一次写这么长的文章,谢谢看完,逆向过程不是单独的线索一条线,也会有连蒙带猜,乐趣无穷。

 
 
 
 
 
 
 
 
 

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2019-4-1 22:35 被susnm编辑 ,原因: 修改错别字(处女座不能忍)
收藏
免费 3
支持
分享
最新回复 (1)
雪    币: 545
活跃值: (1502)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
不错,精神可嘉!
2018-11-20 17:26
0
游客
登录 | 注册 方可回帖
返回
//