很久前的一篇文章了,发出来纪念一下:)
在前一篇文章中提到(https://www.cnblogs.com/areful/p/10399409.html),我们app有个因xposed软件导致的crash,本文来看看怎么使用xposed复现crash。
crash复现及代码分析
〇、 前言
在《crash日志分析》中我们提到,在XPosed框架基础上,“008神器”这款应用劫持了安卓系统十几个API,并间接导致了我们的应用崩溃。当时推测是bugly的SDK持续监视系统所有log,并捕获相近的log作为crash日志计入bug统计平台,因为我们的应用和“008神器”之间并没有直接的关系,根据crash log无法将两者联系起来。
不过与腾讯相关工程师讨论后知道,只有应用crash了才会捕获log计入统计平台,那可能的情况就分成两种,一种是确实是我们的应用崩溃了,因为不知名的原因看不到我们应用crash的log,而只有“008神器”的log;另一种就是,确实是“008神器”导致了我们的应用崩溃。
进一步反编译分析“008神器”代码及其逻辑,照其逻辑编写XPosed模块,使用XPosed框架劫持部分系统API,并在劫持后的处理方法中做特殊处理,使模块生效,再运行目标应用(对本文来说,目标应用是demo.apk,劫持的API是android.webkit.WebView的loadUrl()),只要目标应用调用特定系统API,就可以复现该crash。
接下来首先介绍crash的复现步骤,之后对crash复现代码做进一步分析。
一、 crash复现
1. 复现环境要求
1) CPU兼容armv7a-eabi架构
2) 安卓系统版本4.1~4.4
3) 系统已root,并且应用可以获取root权限
以上为XPosed框架安装及运行必需。“008神器”依赖XPosed框架,所以需要满足上述要求。
2. 复现步骤
复现crash总共需要安装3款应用:wsm.apk,xposed-demo.apk,及目标应用demo.apk。
(PS:因为在xposed-demo.apk实现的模块中,劫持的方法是android.webkit.WebView的loadUrl(),劫持的是包名为com.chebada的应用,所以只要包名为com.chebada,并且调用了WebView的loadUrl(),都可以触发crash。据此,目标应用可以不是“巴士管家”,也可以自己新建一个包名为com.chebada的应用,在其中调用WebView的loadUrl(),运行后一样可以触发crash。在“巴士管家”中调用WebView的loadUrl (),结果也是一样)
1) 安装wsm.apk(XPosed框架安装程序,同时也是XPosed模块管理程序)
2) 运行wsm应用,安装XPosed框架(需要root权限,需重启)
3) 安装xposed-module.apk,并运行一次(XPosed模块的实现,其中包含bug代码。
PS:每次修改demo并运行后都需要重启系统使其生效)
4) 在wsm应用中启用XPosed模块,并重启系统使其生效
(PS:启用一次即可。Xposed-demo.apk卸载后再安装需重新启用该模块并重新启动系统。如果修改了代码,也需要在编译安装后重启系统使其生效)
5) 运行目标应用,调用demo中劫持指定的API,等待5秒至15秒,触发crash (此文档中demo劫持的API是com.webkit.WebView.loadUrl())
二、代码分析
XPosed框架代码是开源的,有兴趣的读者可以自行下载研究。其基本原理是利用root权限替换了安卓系统的Java虚拟机进程对应程序,对Zygote做了手脚,在加载Java虚拟机时额外添加了XPosed框架的入口,从而实现劫持系统API的功能,劫持的基本单位是Java层面的类、对象的方法。关于XPosed框架此处不再赘述,只分析xposed-demo.apk的实现源代码,分析过程会引用部分“008神器”中引起crash的相关逻辑代码(反编译代码)。 Xposed-demo.apk作为XPosed模块,被XPosed框架加载后,就可以劫持各种系统API。此处我们只劫持包名为com.chebada的应用调用的API,劫持的API是WebView的loadUrl()方法。
Xposed-emo.apk源代码见附件,其中com.lishu.net包下的类是根据“008神器”反编译代码重建的,重建目标是代码尽可能简洁,并能完整重现crash时的应用场景。
首先看看劫持入口代码:
Tutorial008实现了IXposedHookLoadPackage接口,在该模块生效后,XPosed框架会劫持所有已安装应用的所有方法,然后转来调用handleLoadPackage()方法。在这里我们只劫持包名为com.chebada的应用的方法,如果包名不是com.chebada,就直接返回。如果包名为com.chebada,接下来执行findAndHookMethod()方法,Hook住WebView的loadUrl()方法,然后模拟“008神器”逻辑代码,新起一个线程发送网络请求:
这里LishuNet类也是根据“008神器”逻辑重写的类,在其中调用poseMessage方法新建线程并运行。内部类继承Thread,在run()方法中通过apache的DefaultHttpClient实现post请求。这里我们不关心具体网络请求参数传的什么,因为在这里网络请求总是失败的。这里注意看这个receiver,它在LishuNet初始化时,并不是Null,但最终是它引起了NullPointerException异常。看看LishuNet类:
我们在线程初始化时读出NetReceiver对象:
这里NetReceiver不是null的,但是在网络请求进行过程中却可能是空的,在几十秒请求过程中,可能由其他代码或者操作,间接导致这个NetReceiver对象变成空的,比如,一个Activity实现了这个NetReceiver接口并通过初始化传过来,然后在线程运行期间,用户切换到其他页面,之后Activity被系统回收了,这个时候就变成null了。接下来看catch部分:
直接调了这个对象的方法,并没有判空:(
这就是引起com.lishu.net.LishuNet$2类的NullPointerException异常的原因,也就是统计平台统计到的bug了,统计平台log中收集到的com.lishu.net.LishuNet$2的NullPointerException,也是在catch(Exception e)这里打印出来的。这一点可以这样印证,在对应的catch{}里使用Log打印一些其他的日志出来,统计平台就可以统计到了。
分析到这里,接下来的工作就比较简单了,想复现bug,就先手动把NetReceiver引用设为空,再延迟5秒钟,就像上图代码中那样(系统超时也设置为5秒)。之后启动网络访问请求,等待网络返回。这个网络访问一般会很快就返回,因为我们的参数不符合“008神器”指定url的参数要求,服务器直接拒绝(也可能是无法连接到服务器,或者访问超时等),从而触发IOException。这个时候在catch (IOException){}中调用NetReceiver引用的方法,就会触发NullPointerException了。
OK,从源代码开始编译运行demo,打开wsm应用,启用该XPosed模块,重启系统使其生效,然后再运行巴士管家,打开任意一个WebView实现的UI页面,然后等待5秒左右,巴士管家就崩溃了。等一段时间后登录bugly平台,就可以看到相关的log,比较一下看看,是不是和“008神器”导致crash的log一样?
可能有人会说,“008神器”这个方法里用到很多成员对象,哪里会有这么巧合,为什么不是其他对象导致的NullPointerException,非要是NetReceiver呢,这一点可以通过穷举大法一一排除,反编译后将所有用到LishuNet的地方的参数全部穷举一遍,有些参数传递过来后使用的时候就判空,有些传递过来后用一次就不再使用,唯有这个NetReceiver在线程运行期间可能是空的。如果,“008神器”的作者在catch中加一个判空,就不会有任何问题。
这里细心的读者可能有个疑问,为什么“008神器”上报的异常只有NullPointerException,但是我们的xposed-demo有一堆其他的log。因为反编译后可以看到,“008神器”作者依据XPosed源代码,在其应用里重新实现了XPosed的劫持类,屏蔽了相关的异常信息。而我们的xposed-demo直接用了XPosed框架里编译好的类,所以相关异常信息默认是打印出来的。
另一个问题可能大家都很奇怪,为什么明明是xposed-demo应用代码的bug,但是crash的不是xposed-demo,而是被劫持的应用(在这里是巴士管家)呢?
这个问题需要换个角度考虑。如果xposed-demo是普通的应用程序,引起巴士管家的崩溃就很奇怪了,但是xposed-demo在这里并不是作为应用程序运行的,而是作为XPosed框架的一个模块。在运行一次xposed-demo并重启系统之后,它的使命就完成了,不再需要xposed-demo运行。XPosed框架需要的是xposed-demo实现的XPosed模块的拦截器接口。XPosed劫持了所有类、方法,并通过接口调用拦截器代码。在重启系统后,XPosed就获取了xposed-demo实现的拦截器接口,然后通过反射运行拦截器中的代码。在哪里运行呢?自然是在被劫持的应用的Java虚拟机进程里了。还记得拦截器接口的Param参数吗,看看它的成员就知道了。Xposed就是通过这个参数,在巴士管家调用WebView的loadUrl()方法的时候,先在巴士管家虚拟机进程中执行xposed-demo拦截器实现的这段代码,后运行巴士管家的loadUrl()方法的。这一点可以通过代码验证,在xposed-demo里拦截器接口实现的afterInvoke方法中给第一个参数(loadUrl()的参数)重新赋值,在被劫持的应用中,调用loadUrl()之前打印一下url,在loadUrl()方法中,再次打印一下url,可以看到url变化了。这个变化就是通过上述劫持过程实现的。这手乾坤大挪移真的好漂亮,一旦这段代码引起Exception,就引起巴士管家崩溃。
附件中有个demo.apk,包名为com.chebada,只包含一个Activity,在Activity中使用WebView的loadUrl方法加载“http://www.baidu.com”,在XPosed模块生效后,安装运行该应用,可以看到大约5秒后程序崩溃,日志如下,再贴一次:)
卸载了demo,再安装巴士管家,打开包含WebView的页面,再等待十几秒,应用直接退出了,看看log,是不是似曾相识?
上bugly平台看看,统计到的信息(从应用crash到收入统计平台有一定延迟):
再看看跟踪日志:
大功告成,至此可以说明,确实是“008神器”的代码导致我们的应用崩溃了*^_^*。
来源:https://www.cnblogs.com/areful/p/10399462.html