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