少数派是国内最大的一个分析高品质数字消费指南的平台,致力于更好地运用数字产品或科学方法,帮助用户提升工作效率和生活品质。当推出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
lldb后,使用c
命令运行程序,操作触发崩溃后可以看到如下输出:
可以看到我们非常熟悉的关键字:reason=EXE_BAD_ACCESS
。由此可以判断崩溃是由于访问坏内存导致的。
使用命令bt
打印调用堆栈
可以看到,程序是运行到一个WebKit
的内部方法[WKScrollViewDelegateForwarder forwardingTargetForSelector:]
之后访问换内存导致闪退的。天哪,我不会发现了一个Apple API的bug吧,继续往下分析。
- sspai应该是使用了WebKit框架的WKWebView来请求浏览的网页
- 通过这个类方法名可以看到这是一个关于
delegate
的类,应该是这个类对于我们设置的delegate做了一些事情导致的。 - 可能是sspai设置了WKWebView的delegate。当然现在只是猜想,之后需要通过Hopper来看一下这个sspai的Mach-O文件。
由行为猜想
根据崩溃的发生位置和时间
- 时间发生在进入文章后返回,这涉及到了两个控制器,可能是两个控制器之间的
delegate
- 因为是在返回之后才会闪退,也可能是返回后控制器
dealloc
做的一些事情导致的
通过逆向查找bug点
逆向后知道类名如下:
- 文章列表控制器(首页):
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的方法,如果就这样结束了,也许我就不写这篇文章了,我决定继续往下分析
分析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.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分析crash原因
快速创建 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
方法,
运行下一行:
可以发现立马崩溃了。
总结
- 根据多次的确认,可以断定这个 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 的审核规则,需要有大神可以补充解答。
第一次写这么长的文章,谢谢看完,逆向过程不是单独的线索一条线,也会有连蒙带猜,乐趣无穷。