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

栏目: Objective-C · 发布时间: 7年前

内容简介:逆向及修复最新 iOS 版少数派客户端的闪退 bug

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

问题描述:

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

找到崩溃原因

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

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

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

基本操作,

// iphone
$ debugsever *:1234 -a pid
// mac
$ lldb
(lldb): process connect connect://ip:1234
逆向及修复最新 iOS 版少数派客户端的闪退 bug
逆向及修复最新 iOS 版少数派客户端的闪退 bug

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

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

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

使用命令 bt 打印调用堆栈

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

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

  • sspai应该是使用了WebKit框架的WKWebView来请求浏览的网页
  • 通过这个类方法名可以看到这是一个关于 delegate 的类,应该是这个类对于我们设置的delegate做了一些事情导致的。
  • 可能是sspai设置了WKWebView的delegate。当然现在只是猜想,之后需要通过Hopper来看一下这个sspai的Mach-O文件。

    由行为猜想

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

  • 时间发生在进入文章后返回,这涉及到了两个控制器,可能是两个控制器之间的 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)

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

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

:warning:: 这里也告诉我们,调用一个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 方法,直接使用之前逆向的代码段来加载一篇少数派的文章。程序运行前先让我们愉快的打上全局断点,在程序运行后,发现程序确实崩溃了,而且停留在了:

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

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

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

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

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

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

通过断点确认猜测

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

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

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

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

如下:

[WKScrollView setDelegate: WKWebView];

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

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

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

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

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

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

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

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

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

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

然后进入retain函数内部:

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

如截图,使用po打印调用者发现输出的不是对象,并且有很明确的提示,访问了一个被释放的对象,使用p/x输出内存地址,发现跟之前保存的 SecondViewController 的内存地址一致,所以可以更加断定这个程序的 crash 是由于访问了被释放的 SecondViewController 对象造成的。

多一次确认肯定没错,现在我们让程序运行到 objc_msgSend 来调用这个 retain 方法,

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

运行下一行:

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

可以发现立马崩溃了。

总结

  • 根据多次的确认,可以断定这个 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 中发现,确实如此:

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

在 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 的审核规则,需要有大神可以补充解答。

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Ruby原理剖析

Ruby原理剖析

[美] Patrick Shaughnessy / 张汉东、秦凡鹏 / 华中科技大学出版社 / 2016-12-1 / 78.80元

《Ruby原理剖析》解开Ruby编程语言的魔法面纱。全书图文并茂、深入浅出地剖析了Ruby编程语言的核心工作原理。作者本着科学实证的精神,设计了一系列实验,帮助读者轻松了解这门编程语言的工作奥秘,包括Ruby如何用虚拟机执行代码,Ruby的垃圾回收算法,以及类和模块在Ruby内部的关系等。一起来看看 《Ruby原理剖析》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试