编者按:随着移动设备硬件能力的提升,Android系统开放的特质开始显现,各种开发的奇技淫巧、黑科技不断涌现,InfoQ特联合《深入理解Android》系列图书作者邓凡平,开设深入理解Android专栏,探索Android从框架到应用开发的奥秘。
Xposed ,大名鼎鼎得Xposed,是Android平台上最负盛名的一个框架。在这个框架下,我们可以加载很多插件App,这些插件App可以直接或间接操纵系统层面的东西,比如操纵一些本来只对系统厂商才open的功能(实际上是因为Android系统很多API是不公开的,而第三方APP又没有权限)。有了 Xposed 后,理论上我们的插件APP可以hook到系统任意一个Java进程(zygote,systemserver,systemui好不啦!)。
功能太强大,自然也有缺点。Xposed不仅仅是一个插件加载功能,而是它从根上Hook了Android Java虚拟机,所以它需要root,所以每次为它启用新插件APP都需要重新启动。而如果仅是一个插件加载模块的话,当前有很多开源的插件加载模块,就没这么复杂了。
Anyway,Xposed强大,我们可以学习其中的精髓,并且可以把它的思想和技术用到自己的插件加载模块里。这就是我们要学习Xposed的意义。
Xposed支持32位和64位的dalvik以及ART,同时支持selinux。我仔细看了下,如果拓展把这些东西都讲的话,一个是很枯燥,另外一个是背离了我们本章讲解插件的主线。所以,本章将围绕下面几个点开展介绍:
l 32位dalvik上非selinux模式下的xposed实现原理
64位、selinux模式其实难度不在虚拟机上,而是在selinux上。
本章先来介绍下Xposed,这是一个大工程,包含多个项目,我也是花了不少时间才把它整个玩转起来的。Xposed包含如下几个工程:
提示,提示,提示:
我把所有相关代码都下载并放到下面的地址了:
https://code.csdn.net/Innost/xposed-learning 里边包含:
1 xposed所有代码库的内容
2 XposedDemo:Xposed插件APP Demo,非常简单
3 XposedDemoTarget:XposedDemo将hook上的目标App
下面介绍下如何编译Xposed,这里以Android 4.4.4为例。做Android开发最好配一个Nexus手机或Pad。我用得是Nexus 7 2013Wi-Fi版。Anyway,Xposed的编译和具体机器无关,不过下面的前提条件需要满足:
好,马上开始我们的步骤:
根据XposedTools的说明,我们先要修改下AOSP源码里的.repo,具体步骤如下:
这个文件是什么内容呢?来看图1:
这个文件内容是啥意思呢?
配置好后,请在AOSP目录下执行repo sync。这样它会根据manifests更新AOSP源码。当然,也可以只是下载frameworks/base/cmds/xposed工程和更新build工程。
注意,repo sync是一个重型操作,会导致所有工程都进行一次同步。我建议的方法是直接下载和更新对应的工程
比如,下载xposed工程,用repo sync frameworks/base/cmds/xposed即可。对于build目录,先把原来的build挪到其他地方。然后repo sync build即可。
好了,到此,所有源码都已经ready了。
下面我们进入XposedTools目录,然后修改其中的build.conf文件。该文件用于指示AOSP源码等参数。如图2所示:
XposdTools提供了一个 build.conf.sample 模板,图2中的 build.conf 文件是在这个模板基础上修改而来。红框中是我修改的结果。其他选项没有变化。
到XposedTools目录下,执行: ./build.pl -t arm:19 命令,这表明我要编译arm平台上SDK=19版本的xposed框架。注意,./build.pl --help会打印出使用方法。 build.pl 是一个perl脚本。图3是编译过程截图:
图3中,你可以发现build.pl跑到AOSP源码目录下,执行了:
在使用build.pl时,它还依赖一些Perl的类库,请童鞋们按照下面步骤下载这些依赖库:
sudo apt-get install libconfig-inifiles-perl
sudo apt-get install libio-all-perl
sudo apt-get install libfile-readbackwards-perl
sudo apt-get install libfile-tail-perl
sudo apt-get install libtie-ixhash-perl
build.pl执行过程中,如果报还有其他依赖库未找到,请通过下面命令
apt-cache search perl XXX 来查找需要apt-get install哪个目标库。XXX是build.pl执行过程中报错时提供的库信息
编译完成后,将产生一个zip包到AOSP/out/sdk19/arm下。AOSP/out是我在build.conf中指定的目录。如图4所示:
编译结果是一个 xposed-v65-arm-custom-build-xyz-20151030.zip 包,这个包可以通过recovery刷到手机上。包的内容就是files文件夹下的内容,包含:
XposedInstaller是Xposed的App,用于管理Xposed框架和插件App。本节我们主要讨论它是如何安装Xposed框架和插件App的。
XposedInstaller启动界面如图5所示:
图5中可知XposedInstaller提供好几项子页面。第一个“框架”用来安装或卸载Xposed框架的。我们来看它。
如图6所示:
注意,图6右上角的“程序自带”两个版本号,分别是app_process版本号为58,XposedBridge.jar版本号是54.
而“激活”这两项为空,因为我们的系统还没有安装Xposed框架。
“程序自带”是什么意思?原来,XposedInstaller在自己的assets目录下携带了所需要的xposed框架程序和模块,如图7所示:
从图7可知,XposdInstaller自带了xposed版zyote(比如app_process_xposed_sdk16),为了更好得支持不同版本的Android,它还区分了SDK版本。另外,XposedInstaller也支持刷机包把xposed框架模块刷入系统,比如 Xposed-Installer-Recovery.zip ,里边包含的主要内容就是一个脚本,在recovery模式下运行,其内部也是把assets里的文件拷贝到 /system 相关目录中。这一块我们后续看代码就知道怎么玩儿了。
安装Xposed框架的主要功能由 InstallerFragment.java 提供,我们看看相关代码。
onCreateView函数是Fragment里初始化UI的核心回调,其代码如图8所示:
图8代码中最后的refreshVersions用于获取版本号,也就是图6中右上角“程序自带”要显示的信息,它包括两个东西:
检查版本主要是为了兼容性考虑。代码中的refreshVersions用于获取他们的版本号,代码如图9所示:
图9中:
想知道怎么获取版本系想你吗?来看图10:
太简单了,不惜得说。
注意XposedBridge.jar包安装版的位置,它在/data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar里
InstallerFragment的install函数用于安装xposed框架相关的模块。这个函数一点也不复杂,不过还是给大家看看。如图11所示:
install函数没什么难度,但是我们要总结下xposed框架安装到底做了什么手脚:
嗯嗯,也没什么太多可说的。
xposed插件,在xposed世界里我们说它是插件,但是放到Android世界里它就是一种特殊的APP。这种类型的APP由xposed框架识别并加载,然后hook到其他的App进程。
当然,这里既然提到进程,那么这些APP就必须要被先启动起来才是。
XposedInstaller的插件管控由ModulesFragment界面来处理。本节主要想介绍下XposedInstaller是怎么对待Xposed插件APP的。
来看代码,如图12所示:
我们重点来看看Xposed插件APP是怎么被load的,代码其实在ModuleUtil的reloadInstalledModules函数中。代码很简单如图13所示:
图13左下角已经告诉各位Xposed插件APP该是什么样的了,右下角是XposedDemo示例。
在AndroidManifest.xml里定义这些东西只是告诉Xposed自己是一个插件APP。但是作为一个挂钩插件,Xposed还需要知道这个APP里哪个类是用来挂钩的。这句话的意思是:
1 这个APP是一个插件APP。该APP包含很多功能。
2 这个APP包含的众多功能中,有一个功能是给目标进程挂钩。挂钩操作是Xposed框架来做的,所以它需要知道该APP中哪个类是继承了Xposed钩子接口。这样,Xposed框架找到插件APP之后会触发这个app的钩子接口进行挂钩。
要做到第二步就得在assets/下放一个名叫xposed_init文件,里边指明插件APP的挂钩类名。XposedDemo的
assets/xposed_init的内容就是:com.xposed.demo.MyXposedModule。当然,这个插件类必须实现Xposed的IXposedHookLoadPackage接口类。这个我们以后再讨论。
Xposed框架分为xposed版app_process和XposedBridge.jar两部分。app_process就是zygote,我们先看看xposed版的zygote干了些什么。
注意,本章只分析32位,dalvik版的xposed app_process,其入口main函数位于app_main.cpp里。
图14所示的代码展示了Xposed版zygote与众不同之处。
图14中,左上角的框是app_main的main函数,里边有两处不同之处:
图9展示了initialize函数的内容,主要是最后一个把 /system/bin/XposedBridge.jar (这个jar包的位置和我们在XposedInstaller那里看到得不同,原因前面解释过了)加到 CLASSPATH 比较重要。
注意,我们这里虽然对initialize介绍的内容很少,但实际上这个函数要真正看明白还是很需要技术实力的:
1 logcat:start:里边fork了logcat进程用来存储xposed自己的log
2 service:startAll:为了完美支持selinux,这里的处理更是很有技巧。selinux是一个完整的知识体系,想彻底掌握它的童鞋请参考我的三部曲文章 《深入理解SELinux SEAndroid》
1&2其实很真实得反映出xposed的作者在Android、Linux上水平很高,经验很丰富。
最后,如果xposed框架启用成功,那么zygote的入口类将由以前的 com.android.internal.os.ZygoteInit 变成 de.robv.android.xposed.XposedBridge 。
下面我们按照执行流程,把相关函数分析一遍:
图16为代码:
onVmCreated比较简单了:
xposed官方说MIUI大量用了xposed的东西,并且不共享,以后碰到MIUI的问题他们不再支持。Don't know what to say....
main函数代码如图17所示:
main函数里有三个重要函数:
initNative很重要,来看代码,如图18所示:
图18中,我们重点看一下 callback_XposedBridge_initNative 和 register_natives_XResources 这两个函数。这两个函数比较简单,我们统一放到图19中:
不多说了,没什么难度。
从这个函数开始,xposed就开始给系统一些关键函数挂钩子了。我们看看它怎么玩儿的。代码如图20所示:
图20中我在eclipse里用了代码缩略显示的方法,可知一共有五个框,分别hook了一些关键内容。我们从上到下一次分析。先来分析下Xposed框架提供的挂钩函数 findAndHookMethod 。
(1) findAndHookMethod介绍
findAndHookMethod用来对指定类的指定函数进行挂钩。这个函数很重要,开发插件APP时用得最多。来看它的代码,如图21所示:
findAndHookMethod 代码Java层面的逻辑还是比较好理解的:
来看hookMethod,如图22所示:
由 hookMethod 可知,一个目标函数可以挂多个钩子,这些钩子由一个集合来存储。然后我们将转到JNI层去看看 hookMethodNative 干了什么事情。这才是hook的核心。代码如图23所示:
hookMethodNative 完成了真正的挂钩处理,其思想很简单:
我们在 《深入理解Android之dalvik》 一文中介绍过,JVM调用java函数时候,发现这个函数为native的话,就调用它的 nativeFunc 。下面我们看看钩子函数的调用。
(2) 调用钩子函数
hook钩子函数后我们就要调用它。上一节我们发现xposed在挂钩子的时候会把原函数改造成native属性(即Dalvik会按native函数的方式调用它),对应的nativeFunc是hookedMethodCallback,其代码如图24所示:
注意喔,我们现在已经在处理被挂钩函数的调用了喔....从JNI会进入到java层的钩子函数dispatch总入口,及 handleHookedMethod ,这个函数比较复杂,我们一段一段来看它。
很简单。接着看第二段,如图26所示:
貌似也很简单喔...
现在来看原目标函数的调用,即 invokeOriginalMethodNative 。代码如图27所示:
同样很容易,不多说了。到此,我们已经看到了Xposed挂钩的所有过程。好像也没什么复杂的,只要对dalvik稍微属性点,应该是比较容易做的。
当然,本文只是给大家show了主要流程,真要自己动手做,我觉得难点在于参数传递等方面。anyway,了解了大体流程,后面的事情也好办,多尝试几次就好。
下面我们回过头来看initForZygote里加的几个钩子都干了什么。
(3) initForZygote钩子设置
图20的initForZygote代码示意中可知xposed对下面几个函数进行hook(此处先不讨论钩子函数干了什么)
main函数在initForZygote之后的下一个动作就是loadModules,这就是加载所有的插件APP。我们来看看这个函数,代码如图28所示:
我们来看下loadModules的处理:
图30展示了 hookLoadPackage 和 hookInitPackageResource 两个函数的内容,特别简单。
嗯嗯,就是把对应的钩子函数保存起来先。
好了,下面我们就来开始分析initForZygote里挂上的几个钩子分别有什么用。
initForZygote为hanldeBindApplication设置了前处理钩子,代码如图31所示:
前面说过,在APP生命周期内, handleBindApplication 是APP刚准备好相关信息的一个重要点,在这个点去进行挂钩处理简直是最好不过了。当然,此处的挂钩处理也就是准备好这个APP的相关信息然后调用所有的 IXposedHookLoadPackage 类型的钩子。
注意,除了 handleBindApplication 之外,由于一个APP进程事实上可以加载多个APK(比如那些申明同样的uid和运行在同一进程的APP),在 LoadedApk 的构造函数中也做了类似的处理
IXposedHookLoadPackage钩子一般会干些什么呢?图31对XposedInstaller的处理就很明显了。一般而言,这种钩子会对目标APP中感兴趣的函数进行挂钩(调用findAndHookMethod),比如XposedInstaller对getActiveXposedVersion进行了挂钩,用于返回系统里正在使用的Xposed框架版本。
我们应该在钩子函数里干些什么?这是一个重要问题。我也不废话了,直接上XposedDemo的源码,如图32所示:
图32是我在xposed-learning项目中提供的XposedDemo示例,可知:
简单点说,我们在IXposedHookLoadPackage的handleLoadPackage中把该挂的钩子都挂上就好。
initAndLoop是system_server进程的关键函数,在这个函数里Android Framework的绝大部分Service都将被创建。真是艺高人胆大,这个进程居然都提供了挂钩处理。其代码如图33所示:
代码倒是很简单,无非是针对system_server进行hook。插件函数如果想区分被hook的进程是否为system_server的话,只需要判断packageName是否为"android"即可。
再次强调,system_server是Android Java层Framework的核心,要hook它需要万分小心,否则或导致手机系统出现会各种不稳定,崩溃,重启等情况。
下面我们介绍下Xposed框架对资源是怎么Hook的。
前面章节介绍了Xposed框架如何对代码调用逻辑进行hook。在Android APP中,除了代码逻辑外,Xposed还支持对资源进行Hook。对资源Hook的原理其实和对代码调用进行Hook的原理类似。这里我们简单介绍下Xposed框架如何对资源进行Hook。
在Android APP中,资源有三个重要类:
就这么简单。我们一步一步来看。
注意,XposedDemo并没有hook资源,请感兴趣的童鞋们自行加上该功能进行测试。
hookResource对ResourceManager等进行了挂钩处理。来看代码,如图34所示:
hookResources主要是对ResourcesManager的getTopLevelResources进行了Hook。APP中原来使用的是Resources代表资源,Hook之后,Xposed用XResources代替了Resources。图35展示了XResources的派生关系。
接着看hookResources第二段代码,如图36所示:
第二段代码中,Xposed资源Hook框架调用了资源hook类型的钩子。同样,我们需要关注在这种类型的钩子里插件APP要干得事情,那就是:
上面替换的还是APP自己的资源,除此之外,Xposed还能替换系统资源(即framework-res.apk里声明的资源),这部分代码也在hookResources里,来看图37:
图37中,Xposed将Resources里代表系统资源的 mSystem 对象也进行了替换。不过这里没有独立调用回调。没关系,因为在第二部分的回调中,插件APP通过 XResources 的 setSystemWideReplacement 可以对系统资源进行替换。
注意,hookResources之后,APP里调用的Resources相关的函数就全部转到XResources来处理了。比如获取字符串的getString函数,其最终会调用到XResources的getText函数,代码如图38所示:
到此,我们对Xposed框架如何hook资源进行了介绍。不过,layout资源的hook我这里并没有介绍,请童鞋们阅读XResources的getLayout函数和init函数。
到此Xposed 32位dalvik版框架基本介绍完了。这一趟绝对不是本篇这20多页文章这么轻松的事情。正如我在《深入理解Android之Dalvik》一文写得那样,我是在研究xposed的时候,发现必须要搞清楚dalvik,所以才先写了dalvik的文章,然后才能走到今天这一步。
Xposed是一个成熟框架,高度体现了开发者在Java虚拟机这块有着非常深厚的知识积累。同时,如果加上selinux的话,那开发者对linux系统也是相当相当熟悉。另外,貌似开发者是用业余时间搞出来的,这在当下上班时间强制为996的码农而言几乎是不可能的事情。
再次向开发者致敬,也同时呼吁基于xposed框架的派生框架开发者遵守相关开源协议。