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

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

问题描述:java

  • 最新版 v.1.0.4 在访问文章后返回会致使crash
  • 逆向分析后发如今 iOS8 以上会有这个问题。(在 iOS8 以上系统中使用了 WebKit 框架)

找到崩溃缘由

直观来讲闪退最主要的缘由有:找不到方法的实现,坏内存访问等。平时在使用Xcode开发本身的 app 时,能够直接在Xcode中快速找到这些 crash 的缘由,相信这些定位崩溃的关键字你们已经很熟悉了。那么如何在没有源码的状况下定位这些 crash?ios

  • 能够直接使用新版Mac的控制台来查看 iPhone 的日志输出
  • 直接使用 lldb

咱们固然是使用使用lldb啦,git

基本操做,github

// iphone
$ debugsever *:1234 -a pid
// mac
$ lldb
(lldb): process connect connect://ip:1234复制代码

lldb后,使用c命令运行程序,操做触发崩溃后能够看到以下输出:web

能够看到咱们很是熟悉的关键字:reason=EXE_BAD_ACCESS。由此能够判断崩溃是因为访问坏内存致使的。api

使用命令bt打印调用堆栈app

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

  • sspai应该是使用了WebKit框架的WKWebView来请求浏览的网页
  • 经过这个类方法名能够看到这是一个关于delegate的类,应该是这个类对于咱们设置的delegate作了一些事情致使的。
  • 多是sspai设置了WKWebView的delegate。固然如今只是猜测,以后须要经过Hopper来看一下这个sspai的Mach-O文件。iphone

    由行为猜测

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

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

  • 由于是在返回以后才会闪退,也多是返回后控制器dealloc作的一些事情致使的

经过逆向查找bug点

逆向后知道类名以下:

  • 文章列表控制器(首页):HomeTableViewController
  • 文章浏览控制器:ArticleViewController
  1. 找到进入ArticleViewController的方法,查看应用是如何初始化的ArticleViewController
  2. 浏览ArticleViewController类,查看能够方法

由于是从HomeTableViewController进入的ArticleViewController,因此咱们须要在HomeTableViewController.h文件中查找这个转跳入口,能够在Hopper中看到这个turnToArticleViewController:cell:很是可疑,根据咱们的正向开发经验,经过这个方法名turnTo vc转跳并用cell参数传递了一个数据model

Hopper中查看方法以下:

-[HomeTableViewController turnToArticleViewController:cell:]:
sub        sp, sp, #0x90 ; Objective C Implementation defined at 0x1006d3658 (instance method), DATA XREF=0x1006d3658
stp        x24, x23, [sp, #0x50]
stp        x22, x21, [sp, #0x60]
stp        x20, x19, [sp, #0x70]
stp        x29, x30, [sp, #0x80]
add        x29, sp, #0x80
mov        x19, x3
mov        x20, x0
mov        x0, x2
bl         imp___stubs__objc_retain
mov        x21, x0
mov        x0, x19
bl         imp___stubs__objc_retain
mov        x22, x0
adrp       x8, #0x1007e0000 ; @selector(setCurTableView:)
ldr        x0, [x8, #0xab8] ; objc_cls_ref_ArticleViewController,__objc_class_ArticleViewController_class
adrp       x8, #0x1007ca000
ldr        x1, [x8, #0x498] ; "alloc",@selector(alloc)
bl         imp___stubs__objc_msgSend
adrp       x8, #0x1007ca000
ldr        x1, [x8, #0x810] ; "initWithArticle:",@selector(initWithArticle:)
mov        x2, x21
bl         imp___stubs__objc_msgSend ; articleVC = [[ArticleViewController alloc]initWithArticle: articleModel ]
mov        x19, x0
mov        x0, x21
bl         imp___stubs__objc_release
adrp       x23, #0x10065c000
ldr        x23, [x23, #0x480] ; __NSConcreteStackBlock_10065c480,__NSConcreteStackBlock
str        x23, [sp, #0x28]
movz       w24, #0xc200
stp        w24, wzr, [sp, #0x30]
adr        x8, #0x100076ae4
nop
str        x8, [sp, #0x38]
adrp       x8, #0x100662000
add        x8, x8, #0x990 ; 0x100662990
str        x8, [sp, #0x40]
mov        x0, x22
bl         imp___stubs__objc_retain
mov        x21, x0
str        x21, [sp, #0x48]
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x5c8] ; "setUpdateCommentCount:",@selector(setUpdateCommentCount:)
add        x2, sp, #0x28
mov        x0, x19
bl         imp___stubs__objc_msgSend ; [articleVC setUpdateCommentCount: block]
str        x23, sp
stp        w24, wzr, [sp, #0x8]
adr        x8, #0x100076b48
nop
str        x8, [sp, #0x10]
adrp       x8, #0x100662000
add        x8, x8, #0x9c0 ; 0x1006629c0
stp        x8, x21, [sp, #0x18]
adrp       x8, #0x1007cc000 ; @selector(showAlert)
ldr        x22, [x8, #0xa00] ; "setUpdateLikeCount:",@selector(setUpdateLikeCount:)
mov        x0, x21
bl         imp___stubs__objc_retain
mov        x21, x0
mov        x2, sp
mov        x0, x19
mov        x1, x22
bl         imp___stubs__objc_msgSend
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x2e8] ; "setHidesBottomBarWhenPushed:",@selector(setHidesBottomBarWhenPushed:)
orr        w2, wzr, #0x1
mov        x0, x19
bl         imp___stubs__objc_msgSend
adrp       x8, #0x1007ca000
ldr        x1, [x8, #0x6f0] ; "navigationController",@selector(navigationController)
mov        x0, x20
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x20, x0
adrp       x8, #0x1007ca000
ldr        x1, [x8, #0x818] ; "pushViewController:animated:",@selector(pushViewController:animated:)
orr        w3, wzr, #0x1
mov        x2, x19
bl         imp___stubs__objc_msgSend
mov        x0, x20
bl         imp___stubs__objc_release
ldr        x0, [sp, #0x20]
bl         imp___stubs__objc_release
ldr        x0, [sp, #0x48]
bl         imp___stubs__objc_release
mov        x0, x21
bl         imp___stubs__objc_release
mov        x0, x19
bl         imp___stubs__objc_release
ldp        x29, x30, [sp, #0x80]
ldp        x20, x19, [sp, #0x70]
ldp        x22, x21, [sp, #0x60]
ldp        x24, x23, [sp, #0x50]
add        sp, sp, #0x90
ret复制代码

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

  1. 初始化一个ArticleViewController类,并传递了一个ArticleModel的文章数据model
  2. 设置了评论数和点赞数的block回调
  3. 控制器转跳时隐藏BottomBar
  4. 而后转跳

顺藤摸瓜查看ArticleViewController的初始化

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

adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x2e8] ; "setHidesBottomBarWhenPushed:",@selector(setHidesBottomBarWhenPushed:)
orr        w2, wzr, #0x1
mov        x0, x20
bl         imp___stubs__objc_msgSend
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x2f0] ; "setArticle:",@selector(setArticle:)
mov        x0, x20
mov        x2, x19
bl         imp___stubs__objc_msgSend
adrp       x8, #0x1007e0000 ; @selector(setCurTableView:)
ldr        x0, [x8, #0xbe0] ; objc_cls_ref_UIDevice,_OBJC_CLASS_$_UIDevice
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x298] ; "currentDevice",@selector(currentDevice)
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x21, x0
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x2a0] ; "systemVersion",@selector(systemVersion)
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x22, x0
adrp       x8, #0x1007ca000
ldr        x1, [x8, #0x908] ; "floatValue",@selector(floatValue)
bl         imp___stubs__objc_msgSend
mov        v8, v0
mov        x0, x22
bl         imp___stubs__objc_release
mov        x0, x21
bl         imp___stubs__objc_release
fmov       s0, #0x4022000000000000
fcmp       s8, s0
b.ge       loc_1000254bc
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x300] ; "loadUIWebView",@selector(loadUIWebView)
b          loc_1000254c4
loc_1000254bc:
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:), CODE XREF=-[ArticleViewController initWithArticle:]+220
ldr        x1, [x8, #0x2f8] ; "loadWkWebView",@selector(loadWkWebView)复制代码

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

咱们先不急着分析loadWkWebView方法的内部实现,首先须要验证一下咱们的猜测,是否由于使用WKWebView致使的crash发生,在这里咱们取个巧,使用theos建立一个本文的插件地址,直接hookloadWkWebView方法,而后在其中调用loadUIWebView方法:

%hook ArticleViewController
- (void)loadWkWebView {
  HBLogInfo(@"%s", __func__);
  [self loadUIWebView];
}
%end复制代码

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

分析loadWkWebView方法

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

-[ArticleViewController loadWkWebView]:
sub        sp, sp, #0x70 ; Objective C Implementation defined at 0x1006c4828 (instance method), DATA XREF=0x1006c4828
stp        d9, d8, [sp, #0x10]
stp        x26, x25, [sp, #0x20]
stp        x24, x23, [sp, #0x30]
stp        x22, x21, [sp, #0x40]
stp        x20, x19, [sp, #0x50]
stp        x29, x30, [sp, #0x60]
add        x29, sp, #0x60
mov        x20, x0
adrp       x8, #0x1007e0000 ; @selector(setCurTableView:)
ldr        x0, [x8, #0xc20] ; objc_cls_ref_WKWebViewConfiguration,_OBJC_CLASS_$_WKWebViewConfiguration
adrp       x8, #0x1007ca000
ldr        x21, [x8, #0x498] ; "alloc",@selector(alloc)
mov        x1, x21
bl         imp___stubs__objc_msgSend
adrp       x8, #0x1007ca000
ldr        x22, [x8, #0x4a0] ; "init",@selector(init)
mov        x1, x22
bl         imp___stubs__objc_msgSend ; conf = [[WKWebViewConfiguration alloc] init]
mov        x19, x0
adrp       x8, #0x1007e0000 ; @selector(setCurTableView:)
ldr        x0, [x8, #0xc28] ; objc_cls_ref_WKPreferences,_OBJC_CLASS_$_WKPreferences
mov        x1, x21
bl         imp___stubs__objc_msgSend
mov        x1, x22
bl         imp___stubs__objc_msgSend ; preference = [[WKPreference alloc] init]
mov        x23, x0
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x438] ; "setPreferences:",@selector(setPreferences:)
mov        x0, x19
mov        x2, x23
bl         imp___stubs__objc_msgSend ; [conf setPreferences: preference]
mov        x0, x23
bl         imp___stubs__objc_release
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x23, [x8, #0x440] ; "preferences",@selector(preferences)
mov        x0, x19
mov        x1, x23
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x24, x0
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x448] ; "setJavaScriptEnabled:",@selector(setJavaScriptEnabled:)
orr        w2, wzr, #0x1
bl         imp___stubs__objc_msgSend ; [preference setJavaScriptEnabled: YES]
mov        x0, x24
bl         imp___stubs__objc_release
mov        x0, x19
mov        x1, x23
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x23, x0
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x450] ; "setJavaScriptCanOpenWindowsAutomatically:",@selector(setJavaScriptCanOpenWindowsAutomatically:)
movz       w2, #0x0
bl         imp___stubs__objc_msgSend ; [preference setJavaScriptCanOpenWindowsAutomatically: NO];
mov        x0, x23
bl         imp___stubs__objc_release
adrp       x8, #0x1007e0000 ; @selector(setCurTableView:)
ldr        x0, [x8, #0xc30] ; objc_cls_ref_WKUserContentController,_OBJC_CLASS_$_WKUserContentController
mov        x1, x21
bl         imp___stubs__objc_msgSend
mov        x1, x22
bl         imp___stubs__objc_msgSend
mov        x22, x0
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x458] ; "setUserContentController:",@selector(setUserContentController:)
mov        x0, x19
mov        x2, x22
bl         imp___stubs__objc_msgSend ; userCC = [[WKUserContentController alloc] init]
mov        x0, x22
bl         imp___stubs__objc_release
adrp       x8, #0x1007e0000 ; @selector(setCurTableView:)
ldr        x0, [x8, #0xc38] ; objc_cls_ref_WKWebView,_OBJC_CLASS_$_WKWebView
mov        x1, x21
bl         imp___stubs__objc_msgSend ; [WKWebView alloc]
mov        x22, x0
adrp       x26, #0x1007e0000 ; @selector(setCurTableView:)
ldr        x0, [x26, #0x9b0] ; objc_cls_ref_UIScreen,_OBJC_CLASS_$_UIScreen
adrp       x8, #0x1007ca000
ldr        x23, [x8, #0x250] ; "mainScreen",@selector(mainScreen)
mov        x1, x23
bl         imp___stubs__objc_msgSend ; [UIScreen mainScreen]
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x24, x0
adrp       x8, #0x1007ca000
ldr        x25, [x8, #0x258] ; "bounds",@selector(bounds)
mov        x1, x25
bl         imp___stubs__objc_msgSend
mov        v8, v2
ldr        x0, [x26, #0x9b0] ; objc_cls_ref_UIScreen,_OBJC_CLASS_$_UIScreen
mov        x1, x23
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x23, x0
mov        x1, x25
bl         imp___stubs__objc_msgSend
adrp       x8, #0x100525000
ldr        d0, [x8, #0x80] ; 0x100525080
fadd       d3, d3, d0
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x460] ; "initWithFrame:configuration:",@selector(initWithFrame:configuration:)
fmov       d1, #0x4035000000000000
movi       v0, #0x0
mov        x0, x22
mov        v2, v8
mov        x2, x19
bl         imp___stubs__objc_msgSend ; webView = [[WKWebView alloc] initWithFrame: [UIScreen mainScreen].bounds configuration: conf];
mov        x22, x0
mov        x0, x23
bl         imp___stubs__objc_release
mov        x0, x24
bl         imp___stubs__objc_release
adrp       x8, #0x1007e0000 ; @selector(setCurTableView:)
ldr        x0, [x8, #0xa50] ; objc_cls_ref_UIColor,_OBJC_CLASS_$_UIColor
adrp       x8, #0x1007ca000
ldr        x1, [x8, #0x550] ; "whiteColor",@selector(whiteColor)
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x23, x0
adrp       x8, #0x1007ca000
ldr        x1, [x8, #0x558] ; "setBackgroundColor:",@selector(setBackgroundColor:)
mov        x0, x22
mov        x2, x23
bl         imp___stubs__objc_msgSend ; [webView setBackgroundColor: [UIColor whiteColor]];
mov        x0, x23
bl         imp___stubs__objc_release
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x410] ; "setOpaque:",@selector(setOpaque:)
mov        x0, x22
movz       w2, #0x0
bl         imp___stubs__objc_msgSend ; [webView setOpaque: NO]
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x468] ; "setUIDelegate:",@selector(setUIDelegate:)
mov        x0, x22
mov        x2, x20
bl         imp___stubs__objc_msgSend
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x470] ; "setNavigationDelegate:",@selector(setNavigationDelegate:)
mov        x0, x22
mov        x2, x20
bl         imp___stubs__objc_msgSend ; [webView setNavigationDelegate: self];
adrp       x8, #0x1007ca000
ldr        x1, [x8, #0xb30] ; "scrollView",@selector(scrollView)
mov        x0, x22
bl         imp___stubs__objc_msgSend ; scrollView  = [webView scrollView];
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x24, x0
adrp       x8, #0x1007ca000
ldr        x23, [x8, #0x750] ; "setDelegate:",@selector(setDelegate:)
mov        x1, x23
mov        x2, x20
bl         imp___stubs__objc_msgSend ; [scrollView setDelegate: self]
mov        x0, x24
bl         imp___stubs__objc_release
adrp       x8, #0x1007ca000
ldr        x1, [x8, #0x278] ; "view",@selector(view)
mov        x0, x20
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x24, x0
adrp       x8, #0x1007ca000
ldr        x1, [x8, #0x520] ; "addSubview:",@selector(addSubview:)
mov        x2, x22
bl         imp___stubs__objc_msgSend ; [self.view addSubview: webView];
mov        x0, x24
bl         imp___stubs__objc_release
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x478] ; "setWkView:",@selector(setWkView:)
mov        x0, x20
mov        x2, x22
bl         imp___stubs__objc_msgSend ; self.wkView = webView;
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x480] ; "addObserver:forKeyPath:options:context:",@selector(addObserver:forKeyPath:options:context:)
adrp       x3, #0x100683000 ; @"share_light"
add        x3, x3, #0x80 ; @"estimatedProgress"
orr        w4, wzr, #0x3
mov        x0, x22
mov        x2, x20
movz       x5, #0x0
bl         imp___stubs__objc_msgSend ; [webView addObserver: self forKeyPath: @"estimatedProgress" options: 3 content: nil];
adrp       x8, #0x1007e0000 ; @selector(setCurTableView:)
ldr        x24, [x8, #0xad0] ; objc_cls_ref_NSString,_OBJC_CLASS_$_NSString
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x318] ; "article",@selector(article)
mov        x0, x20
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x25, x0
adrp       x8, #0x1007ca000
ldr        x1, [x8, #0x3e8] ; "ID",@selector(ID)
bl         imp___stubs__objc_msgSend ; NSString *idStr = [self.article ID];
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x26, x0
adrp       x8, #0x1007ca000
ldr        x1, [x8, #0xa10] ; "stringWithFormat:",@selector(stringWithFormat:)
str        x26, sp
adrp       x2, #0x100683000 ; @"share_light"
add        x2, x2, #0x60 ; @"https://ios.sspai.com/api/v1/index/article/detail/get/%@"
mov        x0, x24
bl         imp___stubs__objc_msgSend ; urlStr = [NSString stringWithFormat: @"https://ios.sspai.com/api/v1/index/article/detail/get/%@", idStr];
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x24, x0
mov        x0, x26
bl         imp___stubs__objc_release
mov        x0, x25
bl         imp___stubs__objc_release
adrp       x8, #0x1007e0000 ; @selector(setCurTableView:)
ldr        x0, [x8, #0x9d0] ; objc_cls_ref_NSURL,_OBJC_CLASS_$_NSURL
adrp       x8, #0x1007ca000
ldr        x1, [x8, #0x300] ; "URLWithString:",@selector(URLWithString:)
mov        x2, x24
bl         imp___stubs__objc_msgSend ; url = [NSURL URLWithString: urlStr];
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x25, x0
adrp       x8, #0x1007e0000 ; @selector(setCurTableView:)
ldr        x0, [x8, #0xc40] ; objc_cls_ref_NSMutableURLRequest,_OBJC_CLASS_$_NSMutableURLRequest
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x428] ; "requestWithURL:",@selector(requestWithURL:)
mov        x2, x25
bl         imp___stubs__objc_msgSend ; request = [NSMutableRequest requestWithURL: url];
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x26, x0
adrp       x8, #0x1007e0000 ; @selector(setCurTableView:)
ldr        x0, [x8, #0xc48] ; objc_cls_ref_UITapGestureRecognizer,_OBJC_CLASS_$_UITapGestureRecognizer
mov        x1, x21
bl         imp___stubs__objc_msgSend
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x3, [x8, #0x488] ; "handleLongPress:",@selector(handleLongPress:)
nop
ldr        x1, [x8, #0x490] ; "initWithTarget:action:",@selector(initWithTarget:action:)
mov        x2, x20
bl         imp___stubs__objc_msgSend ; tapGes = [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(handleLongPress:)]
mov        x21, x0
mov        x1, x23
mov        x2, x20
bl         imp___stubs__objc_msgSend
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x498] ; "addGestureRecognizer:",@selector(addGestureRecognizer:)
mov        x0, x22
mov        x2, x21
bl         imp___stubs__objc_msgSend ; [webView addGestureRecognizer: tapGes];
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x4a0] ; "wkView",@selector(wkView)
mov        x0, x20
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x20, x0
adrp       x8, #0x1007cb000 ; @selector(cancelButtonClickAction:)
ldr        x1, [x8, #0x430] ; "loadRequest:",@selector(loadRequest:)
mov        x2, x26
bl         imp___stubs__objc_msgSend ; [self.WKWebView loadRequest: request];
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
bl         imp___stubs__objc_release
mov        x0, x20
bl         imp___stubs__objc_release
mov        x0, x21
bl         imp___stubs__objc_release
mov        x0, x26
bl         imp___stubs__objc_release
mov        x0, x25
bl         imp___stubs__objc_release
mov        x0, x24
bl         imp___stubs__objc_release
mov        x0, x22
bl         imp___stubs__objc_release
mov        x0, x19
ldp        x29, x30, [sp, #0x60]
ldp        x20, x19, [sp, #0x50]
ldp        x22, x21, [sp, #0x40]
ldp        x24, x23, [sp, #0x30]
ldp        x26, x25, [sp, #0x20]
ldp        d9, d8, [sp, #0x10]
add        sp, sp, #0x70
b          imp___stubs__objc_release复制代码

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

重写以下:

%hook ArticleViewController
- (void)loadWkWebView {
  HBLogInfo(@"%s", __func__);
  WKWebViewConfiguration *conf = [[WKWebViewConfiguration alloc] init];
  WKPreferences *preferences = [[WKPreferences alloc] init];
  [conf setPreferences: preferences];
  [preferences setJavaScriptEnabled: YES];
  [preferences setJavaScriptCanOpenWindowsAutomatically: NO];

  // WKUserContentController *userCC = [[WKUserContentController alloc] init];
  WKWebView *webView = [[WKWebView alloc] initWithFrame: [UIScreen mainScreen].bounds configuration: conf];
  [webView setBackgroundColor: [UIColor whiteColor]];
  [webView setOpaque: NO];
  [webView setUIDelegate: (id)self];
  [webView setNavigationDelegate: (id)self];

  [webView.scrollView setDelegate: (id)self];

  [self.view addSubview: webView];
  self.wkView = webView;
  [webView addObserver: self forKeyPath: @"estimatedProgress" options: 3 context: nil];
  NSString *idStr = [self.article ID];
  NSString *urlStr = [NSString stringWithFormat: @"https://ios.sspai.com/api/v1/index/article/detail/get/%@", idStr];
  NSURL *url = [NSURL URLWithString: urlStr];
  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL: url];

  UITapGestureRecognizer *tapGes = [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(handleLongPress:)];
  [webView addGestureRecognizer: tapGes];
  [self.wkView loadRequest: request];
}
%end复制代码

方法内主要是配置了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。

%hook ArticleViewController
- (void)loadWkWebView {
  HBLogInfo(@"%s", __func__);
  %orig;
  [[self.wkView scrollView] setDelegate: nil];
  return;
}
%end复制代码

编译运行后发现,也不会 crash,因此这个时候能够判断这个 crash 点的准确性了。

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

demo分析crash缘由

快速建立 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方法,

运行下一行:

能够发现立马崩溃了。

总结

  • 根据屡次的确认,能够判定这个 crash 是因为在SecondViewController被销毁后,WKWebView在销毁时,内部调用了_updateDelegate来更新delegate,而后获取了属性scrollView,设置其delegate时,会先retain原来的delegate对象(这里的SecondViewController,此时已被销毁)。
  • 在平常开发中,会常用Apple提供的api,可是这些api可能没法知足咱们的需求,就像这里,由于WebKit框架并无提供监听内部属性scrollView的滚动监听方法,因此会本身动手,能够丰衣足食的同时,也会带来风险!,读取api内部的属性还好,可是一旦涉及到修改其内容,会存在一些风险,由于咱们不知道这会对api内部的调用产生怎么样的影响。
  • 从设计的角度来说,app中这样修改来知足咱们的监听滚动的需求也是不合理的,由于这会直接修改api的内部,咱们只能从第三方的角度来给框架添加功能。(具体能够看个人demo中的实现,我的以为个人实现还算优雅,,下面也会有介绍)
  • 做为iOS开发者,Apple的WebKit框架确定不止我一个在用,我相信确定还会有其余的开发者跟我遇到同样的问题,因而我在stackoverflow中一搜索,果真发现一个:stackoverflow,文章的解决方法跟我一开始逆向的时候的解决方法同样:

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.

扩展

扩展WKWebView方法,添加监听scrollView滚动的代理

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

@interface WKWebView (ScrollViewDelegate)<UIScrollViewDelegate>

@property (nonatomic, weak) id<UIScrollViewDelegate> scrollViewDelegate;

@end

@implementation WKWebView (ScrollViewDelegate)

- (NSObject *)scrollViewDelegate {
  return objc_getAssociatedObject(self, @selector(scrollViewDelegate));
}

- (void)setScrollViewDelegate:(NSObject *)delegate {
  objc_setAssociatedObject(self, @selector(scrollViewDelegate), delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
  NSLog(@"%s", __func__);
  [self.scrollViewDelegate scrollViewWillBeginDragging:scrollView];
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
  NSLog(@"%s", __func__);
  [self.scrollViewDelegate scrollViewDidEndScrollingAnimation:scrollView];
}

@end复制代码

以后,只须要设置scrollViewDelegate代理便可。由于咱们是在分类中添加的scrollView的代理方法,若是原来Apple已经在WKWebView中实现了scrollView的代理方法?毕竟Apple不会平白无故将WKWebView设置为scrollView的代理,它确定是有效果要实现,我将WebKit拖到Hopper中发现,确实如此:

在 demo 中测试,发现咱们确实能够监听scrollView滚动。可是由于我不知道原来的WKWebView的监听滚动用来实现怎么样的效果,因此没法肯定原来的监听是否依然有效。当分类和原类定义一个同一个方法时,运行时只有一个方法会被调用。从逆向的角度出发,我想直接 hook WKWebView的滚动监听方法,而后调用原来方法的同时,实现本身的监听通知。

+(void)load {
  Method scrollViewWillBeginDragging = class_getInstanceMethod(self, @selector(scrollViewWillBeginDragging:));
  Method hook_scrollViewWillBeginDragging = class_getInstanceMethod(self, @selector(hook_scrollViewWillBeginDragging:));

  Method scrollViewDidEndScrollingAnimation = class_getInstanceMethod(self, @selector(scrollViewDidEndScrollingAnimation:));
  Method hook_scrollViewDidEndScrollingAnimation = class_getInstanceMethod(self, @selector(hook_scrollViewDidEndScrollingAnimation:));

  method_exchangeImplementations(scrollViewWillBeginDragging, hook_scrollViewWillBeginDragging);
  method_exchangeImplementations(scrollViewDidEndScrollingAnimation, hook_scrollViewDidEndScrollingAnimation);
}复制代码

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

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

相关文章
相关标签/搜索