逆向及修复最新 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
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
创建一个 本文的插件地址 ,直接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.setUIDelegate
; wkView.setNavigationDelegate
; wkView.scrollView.delegate
。
通过逐一注释这些方法后测试来定位,最后发现在注释 wkView.scrollView.delegate
方法后程序没有 crash。
仔细思考可以发现这三个方法的调用,只有第三个不是直接使用Apple提供的API,查看 WKWebView
的文档内容可以发现, scrollView
是 WKWebView
内部的一个属性。
:warning:: 这里也告诉我们,调用一个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 的审核规则,需要有大神可以补充解答。
第一次写这么长的文章,谢谢看完,逆向过程不是单独的线索一条线,也会有连蒙带猜,乐趣无穷。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 孤独的少数派,抵制 systemd 的 Devuan 能走多远?
- 少数派是如何用 Teambition 进行项目管理和团队协作的?
- Tinker源码解析-代码修复和资源修复
- Ruby 2.5.1 正式发布,包含 bug 修复和安全修复
- struts2架构网站漏洞修复详情与利用漏洞修复方案
- 禅道 11.5.1 版本发布,新增免密登录,修复一键安装包漏洞,修复 bug
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。