少數派是國內最大的一個分析高品質數字消費指南的平臺,致力於更好地運用數字產品或科學方法,幫助用戶提高工做效率和生活品質。當推出iOS版本後,我馬上進行了下載和使用,做爲一個開發者,首先必須是一個數字商品的消費者。最近期的一次更新中,發現了一個比較嚴重的bug,因而我利用逆向知識,對其進行了分析。javascript
問題描述:java
WebKit
框架)直觀來講閃退最主要的緣由有:找不到方法的實現,壞內存訪問等。平時在使用Xcode開發本身的 app 時,能夠直接在Xcode中快速找到這些 crash 的緣由,相信這些定位崩潰的關鍵字你們已經很熟悉了。那麼如何在沒有源碼的狀況下定位這些 crash?ios
咱們固然是使用使用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吧,繼續往下分析。框架
delegate
的類,應該是這個類對於咱們設置的delegate作了一些事情致使的。多是sspai設置了WKWebView的delegate。固然如今只是猜測,以後須要經過Hopper來看一下這個sspai的Mach-O文件。iphone
根據崩潰的發生位置和時間
時間發生在進入文章後返回,這涉及到了兩個控制器,多是兩個控制器之間的delegate
dealloc
作的一些事情致使的逆向後知道類名以下:
HomeTableViewController
ArticleViewController
ArticleViewController
的方法,查看應用是如何初始化的ArticleViewController
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
和正向經驗也能夠快速推斷出來。
主要作了如下事情:
- 初始化一個
ArticleViewController
類,並傳遞了一個ArticleModel
的文章數據model
- 設置了評論數和點贊數的block回調
- 控制器轉跳時隱藏
BottomBar
- 而後轉跳
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
方法,多是在初始化配置WKWebView
的loadWkWebView
方法中出現了bug。
咱們先不急着分析loadWkWebView
方法的內部實現,首先須要驗證一下咱們的猜測,是否由於使用WKWebView
致使的crash發生,在這裏咱們取個巧,使用theos
建立一個本文的插件地址,直接hookloadWkWebView
方法,而後在其中調用loadUIWebView
方法:
%hook ArticleViewController
- (void)loadWkWebView {
HBLogInfo(@"%s", __func__);
[self loadUIWebView];
}
%end複製代碼
編譯運行後發現確實不存在壞內存訪問的問題。
因此能夠肯定,確實是loadWkWebView
方法中的一些代碼致使了crash
到這裏已經找到了解決bug的方法,若是就這樣結束了,也許我就不寫這篇文章了,我決定繼續往下分析
咱們在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.setUIDelegate
;wkView.setNavigationDelegate
;wkView.scrollView.delegate
。
經過逐一註釋這些方法後測試來定位,最後發如今註釋wkView.scrollView.delegate
方法後程序沒有 crash。
仔細思考能夠發現這三個方法的調用,只有第三個不是直接使用Apple提供的API,查看WKWebView
的文檔內容能夠發現,scrollView
是WKWebView
內部的一個屬性。⚠️:這裏也告訴咱們,調用一個API內部的屬性實際上是有風險的,由於在咱們使用了內部屬性後,咱們並不知道這是否會影響其API內部對這個屬性的使用,特別是這裏是設置了內部屬性的delegate
在這裏咱們能夠大膽假設一下,此次程序的 crash 多是由於Apple內部,對咱們設置給
scrollView
的delegate
進行了調用,而此時該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 ,能夠在連接中找到這個 demo 的完整代碼。代碼很簡單,首頁控制器ViewController
一個 UIButton
轉跳到SecondViewController
控制器,SecondViewController
內部的viewDidLoad
方法,直接使用以前逆向的代碼段來加載一篇少數派的文章。程序運行前先讓咱們愉快的打上全局斷點,在程序運行後,發現程序確實崩潰了,並且停留在了:
那麼讓咱們開啓Address Sanitizer
或者Zombie Objects
,而後運行程序:
確實程序訪問了一個壞內存,對象爲SecondViewController
,調用了retain方法。這個時候,特別困惑,demo很是簡單,只有兩個控制器的轉跳,邏輯清晰,是誰在SecondViewController
銷燬後還在調用它,應該不是demo程序自身的對象調用了這個被銷燬的對象,查看後發現,
查看堆棧後發現,確實不是咱們的demo訪問的壞內存,訪問對象在CoreFoundation
的 image 中,而且根據截圖能夠發現,WebKit
框架在WKWebView
被dealloc
的時候調用了WKScrollView
的私有方法_updateDelegate
來更新 delegate。根據截圖能夠猜想_updateDelegate
的內部應該是獲取到了屬性scrollView
而後setDelegate
。而且在設置delegate的時候retain
保留了原來的delegate([secondViewController retain])。
根據截圖,咱們斷兩個符號斷點後運行程序:
運行程序後,能夠發現,在咱們轉跳到SecondViewController
的時候斷在了_updateDelegate
,c
後如預期的斷在了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
方法,
運行下一行:
能夠發現立馬崩潰了。
SecondViewController
被銷燬後,WKWebView
在銷燬時,內部調用了_updateDelegate
來更新delegate
,而後獲取了屬性scrollView
,設置其delegate
時,會先retain
原來的delegate
對象(這裏的SecondViewController
,此時已被銷燬)。WebKit
框架並無提供監聽內部屬性scrollView
的滾動監聽方法,因此會本身動手,能夠豐衣足食的同時,也會帶來風險!,讀取api內部的屬性還好,可是一旦涉及到修改其內容,會存在一些風險,由於咱們不知道這會對api內部的調用產生怎麼樣的影響。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.
可是在寫這篇文章,整理思路的時候,我發現這樣直接修改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 的審覈規則,須要有大神能夠補充解答。
第一次寫這麼長的文章,謝謝看完,逆向過程不是單獨的線索一條線,也會有連蒙帶猜,樂趣無窮。