逆向及修復最新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 的審覈規則,須要有大神能夠補充解答。

第一次寫這麼長的文章,謝謝看完,逆向過程不是單獨的線索一條線,也會有連蒙帶猜,樂趣無窮。

相關文章
相關標籤/搜索