标签:安卓安全

安卓开发

安卓源码的下载与编译

最近在研究对脱壳工具dexhunter进行自己需求的定制的时候涉及到了安卓源码的编译,因此记录一下,编译安卓源码是一个坑坑不息之路,各种让你意向不到的错误都会出现,而且因为编译环境的不同,每个人出现的错误可能完全不一样,因此在网上找的解决方案可能对你的环境而言根本不起作用,谁搞过谁就知道,如果说你能够第一次编译就成功的话,那我只能说你可以去买彩票了。 PS:最好自己下载安卓源码编译而不要使用从网上别人分享的网盘下载下来的源码编译,自己在使用别人下载的源码编译的时候遇到过各种奇怪的问题,但是使用自己从谷歌官网下载的源码的时候就一次性通过。  

热修复

也谈安卓dex的动态加载与MultiDex和热修复

在前面从java虚拟机ClassLoader到安卓dex动态加载之ClassLoader动态加载jar的文章中讲到了java中jar的动态加载,而安卓和java一样都是使用的java语言开发,因此最终文件肯定还是字节码,只不过安卓不在使用java虚拟机来加载这些字节码,而且字节码也不再是.class/jar的文件格式,而是安卓特有的dex文件格式。也正因为这些相似性,java中的类加载机制被安卓给借鉴过来了,只不过安卓实现了自己的类加载器,而不在是java中的那些ClassLoader,因为要加载的文件格式已经不同。在安卓中的类加载器主要包括PathClassLoader和DexClassLoader,这两者都是继承自BaseDexClassLoader,其中PathClassLoader是安卓中的默认加载器。本文重点讲解一下安卓dex的加载原理,以及基于该原理衍生出来的MultiDex,热修复等技术。 在讲解dex加载原理之前,这里先纠正下网上广泛流传的错误结论,网上很多文章都说PathClassLoader和DexClassLoader的区别是前者不能加载指定目录的dex,只能加载已经安装的apk的dex,而后者可以,我可以负责任的说这是一个错误的结论,这2种类加载器都可以加载指定目录下的dex文件,从本质上来讲这2个ClassLoader无任何区别,唯一的区别是在安卓API26之前,即安卓8.0系统之前前者不能指定dex优化后的odex文件存放目录而后者可以,另外从8.0开始谷歌为了安全考虑已不允许开发者指定odex存储路径,此时这2个ClassLoader从功能上来将已经完全等同了。 安卓中PathClassLoader和DexClassLoader都是继承自BaseDexClassLoader,因此我们只需要分析BaseDexClassLoader即可。我们知道加载一个类都是调用的ClassLoader的loadClass方法,而该方法会调用findClass方法。因此我们首先从BaseDexClassLoader的findClass方法入手。该方法源码如下: [crayon-5f03bb8ea120b972600458/] 可以看到BaseDexClassLoader的findClass方法实际上是调用了pathList这个成员的findClass方法,而pathList是一个DexPathList对象,该成员是在BaseDexClassLoader的构造函数中赋值的,也就是说当实例化一个BaseDexClassLoader对象时也随之创建了一个DexPathList对象,代码如下 [crayon-5f03bb8ea1211409206786/] 所以接下来看看DexPathList类的findClass方法。 [crayon-5f03bb8ea1213545721622/] 可以看到在DexPathList类的findClass方法中会迭代遍历dexElements成员,该成员是一个Element[]类型的数组,其中的每一个元素Element中就包含了DexFile这个数据结构,而DexFile就是安卓中dex文件在java层对应的数据描述,然后调用DexFile类的loadClassBinaryName方法来查找加载某个类,从findClass这个函数可以看到,在循环遍历Element[]中每一个Element中的DexFile时,如果在当前DexFile中加载到了要查找的类,会直接返回该类。否则继续迭代遍历。那么如果多个dex文件中包含了同一个类,肯定会优先返回在Element[]数组中靠前的DexFile中的那个类,注意这很重要,这是后面讲到的QQ空间热修复的原理所在。dex加载流程基本就讲完了,我们再来挖一些细节,我们来看下dexElements这个成员是在哪里赋值的,查看源码发现是在DexPathList的构造函数中赋值的,代码如下 [crayon-5f03bb8ea1217488825958/] 也就是说在构造DexPathList对象的时候会调用makeDexElements方法,该方法会返回一个Element[]数组,该数组中的每一个Element包含了要加载的dex文件在java层对应的描述DexFile,这个函数是非常重要的函数,因此我们看下具体实现: [crayon-5f03bb8ea121b609459487/] 可以看到该函数会根据不同的文件类型如dex,apk,jar,zip等进行不同的处理,但是最终都会调用loadDexFile函数来将file参数指向的dex文件转换为dex文件在java层对应的数据描述DexFile对象,然后将该对象添加到Element中,最终返回。而loadDexFile函数实际上是调用了DexFile类的loadDex函数, [crayon-5f03bb8ea1220673639203/] 而loadDex函数实际上也是直接调用DexFile的构造函数。从这个过程可以看到dex的最终加载是DexFile这个类来完成的。但是对于ClassLoader而言,dex的加载是通过DexPathList类的makeDexElements函数来完成的,该函数会根据传入的dex文件的路径(实质参数是dex文件路径所代表的File对象的集合)返回一个与dex文件相关联的包含DexFile的数组elements,而该数组elements正是从dex中加载类时迭代遍历的Element[]数组。既然只需要一个dex文件的路径就可以得到在加载某个类时查找的DexFile。那么如果我们能够通过反射拿到某个ClassLoader的DexPathList对象,然后通过反射调用其makeDexElements函数就可以加载我们我们自己指定的路径的dex文件了,比如加载Assets目录下的插件apk。当然除了通过makeDexElements将指定路径的dex文件加载到内存以外还需要修改原DexPathList类的elements字段的内容。事实上谷歌的MultiDex的原理正式基于此。 总结一下dex的加载过程,这个是理解MultiDex和热修复原理的理论基础。 当通过PathClassLoader加载某个类时,实质上是调用的BaseDexClassLoader的findClass,而BaseDexClassLoader的findClass实质上调用的是DexPathList类的findClass 当调用BaseDexClassLoader的findClass方法时已经产生了DexPathList对象(因为当构造BaseDexClassLoader对象的时候就会创建DexPathList对象),而在构造DexPathList对象时会通过该类的makeDexElements函数将dex文件加载到内存,同时返回一个包含java层的DexFile类的Element[]数组dexElements(如果读者还想进一步了解DexFile类是如何加载dex文件到内存的,实质是在native层调用linux系统函数mmap来将文件映射到内存的,详细分析可以看http://www.cnblogs.com/lanrenxinxin/p/4712224.html) 查找类时是从步骤2中得到的dexElements数组中迭代查找,如果找到会直接返回该类,也就是说会优先返回在dexElements数组中靠前的dex文件中的类。 其中步骤2衍生出了谷歌MultiDex原理,步骤3衍生出了QQ空间热修复原理。当然实现的话3个步骤都包含。另外从上述过程可以看到要实现加载指定路径的dex,比如Assets目录下的dex文件,有两种方式: 一种是通过反射获取当前ClassLoader的pathList成员,然后通过反射调用其makeDexElements函数,事实上谷歌的MultiDex就是采用的这种方式 另外一种是使用DexClassLoader,然后通过反射获取该DexClassLoader的pathList,然后通过反射获取pathList的dexElements成员,事实上与QQ空间热修复原理类似的开源的nuwa就是采用的这种方式。这种方式的本质还是调用了makeDexElements函数(在构造DexClassLoader时会创建DexPathList对象,而DexPathList的构造函数中调用了makeDexElements函数) 谷歌MultiDex的源码位于https://android.googlesource.com/platform/frameworks/multidex/+/d79604bd38c101b54e41745f85ddc2e04d978af2/library/src/android/support/multidex MultiDex的使用请参看https://developer.android.com/studio/build/multidex.html#about MultiDex的源码不复杂,总共只4个java文件。其中最重要的是MultiDex.java这个类。不管使用哪种方式配置,其本质都是调用了MultiDex的install方法。该方法很长,大家可以参看源码,但最核心的是调用了 [crayon-5f03bb8ea1227364305910/] 而该函数会根据安卓sdk的不同版本,调用不同的install函数来加载额外的dex。 [crayon-5f03bb8ea122b662495276/] 这里以V19为例进行分析,因为其他的都是大同小异,原理都是相同的,只不过不同的版本在细节处理上可能不同而已。 [crayon-5f03bb8ea122f647362809/] 可以看到原理即是基于前面dex加载流程的原理,具体实现是通过反射拿到当前ClassLoader的pathList成员,然后通过反射调用pathList的makeDexElements函数实现加载file参数指向的dex文件,然后将pathList的dexElements设置为调用makeDexElements函数时返回的Elements数组和原ClassLoader已经加载的dex的Elements数组之和,这样就实现了加载多dex的效果。 QQ空间热修复的原理和MultiDex极其相似,只不过在将pathList的dexElements设置为调用makeDexElements函数时返回的Elements数组和原ClassLoader已经加载的dex的Elements数组之和这一步是将patch对应的Elements数组放到原dex对应的Elements数组的前面,这样查找相同的类时就会优先查找到patch中修复的类。从而实现热修复。QQ空间热修复的本质是类替换,在使用同一个类时不在是使用原dex中的类,而是使用优先查找到的patch中对应的类。当然在具体实现过程也有一些细节要进行处理,比如说类校验等,具体的可以参考QQ空间团队官方的博客。【QQ空间技术团队】安卓App热补丁动态修复技术介绍

安卓安全

安卓逆向分析之apktool与dex2jar等工具的使用

记得自己刚学安卓的时候看到那些知名App的界面都做的那么好看,如腾讯QQ,就很想知道这么漂亮的界面布局layout是怎么做的,那个时候知道apk本质就是一个zip压缩包文件,因此会用解压缩的软件把apk解压出来,虽然解压缩之后可以得到那些精美的图片素材,但是layout布局文件打开内容是乱码,看不到有价值的信息。现在开始从事安卓安全方面的工作了,开始知道这正是apktool能够做到的,apktool是谷歌官方提供的apk编译工具,使用该工具可以将apk资源文件decode为几乎原始的形式,然后修改之后还可以重新构建为apk文件。另外如果我们还想知道某一个功能是如何做出来的,或者是使用了那些相关开源框架,我们需要得到该apk的java代码,如果App未加密的话使用dex2jar工具即可完成。 最后本人将自己从官网下载下来的文中用到的工具打包为了zip包,大家可以直接下载使用,下载地址:

NDK

安卓NDK开发之Hello JNI

在讲NDK开发之前首先解释几个概念: JNI:java native interface,java本地接口,我们知道java之所以能够做到跨平台就是因为java提供了一个java虚拟机来屏蔽掉和硬件相关的函数调用,我们写的java程序在java虚拟机上运行,但是很明显很多系统功能调用还是需要本地函数来完成(通常为C/C++代码),因此在java源码的很多代码中我们能够看到native关键字命名的函数,如java中的Object类存在大量的native函数,如: [crayon-5f03bb8ea1678696939237/] 其中的native关键字就是用来说明该函数是jni函数,函数的声明为java代码,但是函数的具体实现是本地native代码,通常为C/C++代码,调用该函数最终调用的是native层的C/C++代码。 NDK:Native Development Kit,是一系列的本地开发工具包,前面说过JNI是在java层调用本地C/C++代码,这个过程比较复杂,而NDK就是谷歌公司开发的用来简化在安卓程序中使用JNI调用本地代码过程的工具。 主要包括以下几个原因: 为了更安全:保护自己的App不那么轻易被人破解,因为安卓是基于java语言的,但是java代码编译为dex文件很容易通过逆向分析得到源代码,因此一些重要的涉及到核心技术的代码都不应该直接放在java层实现,一般都是通过C/C++代码生成的so来供安卓java层调用,因为逆向分析so代码的难度远高于分析java代码。 对已经存在的本地代码的复用:任何一门语言都包括了大量的已经稳定的库,为了使用c/c++层已经成熟的一些功能库。 为了更好的控制硬件:控制硬件的代码通常是用C语言编写的,因为C语言包含指针,可以更容易的访问内存。这一点java语言很难做到。 注意:使用Android Studio实际上很容易创建NDK工程,只需要在使用AS向导创建一个新工程的时候勾选include C++ support选项即可。但是此文打算先讲解下如何手动给已有的Android项目增加JNI代码,而不是利用向导直接创建,原因在文末会提到。但是在实际开发中肯定是推荐大家使用AS向导直接创建。 假设我们已经有一个安卓工程了,现在打算给该安卓项目中的某些函数的实现放到native层。这里为了简单起见,安卓工程只包含一个Activity,整个Activity主界面只包括一个TextView,其内容从本地C++代码中的getString()函数获取的。 [crayon-5f03bb8ea167e996601515/] 然后创建名为NDKTest的类,在该类中定义一个native函数getString(),使用System.loadLibrary(“ndk”);来加载so文件,其中so的名称可以随意,这里指定为”ndk”,代码如下: [crayon-5f03bb8ea1680445726435/] 然后打开app的build.gradle文件,在android/defaultConfig下面添加ndk节点,如图: 其中moduleName是最终生成的.so文件的名称,和前面System.loadLibrary(“ndk”)中保持一致,abiFilters用来说明生成哪些硬件架构的so,这里只是简单的包括x86和armeabi-v7a,关于对不同硬件架构的so的支持,大家可以参看http://kvh.io/cn/android-ndk-so.html ,ndk中的具体信息需要根据jni代码的使用情况来配置,上面只是一个简单的例子而已,但是一般都包含moduleName和abiFilters信息。 ndk信息配置好之后,依此点击Android Studio菜单栏的Build->Make Project.然后点击Terminal按钮打开终端面板,可以看到当前目录定位到了我们的项目所在的目录,然后在终端中输入如下命令:cd app/build/intermediates/classes/debug将目录切换到项目的app/build/intermediates/classes/debug目录下,之所以切换到该目录下,是因为我们接下来需要要执行javah命令来生成NDKTest类中native函数的JNI头文件,而javah命令会根据包名所在的路径来查找对应的.class文件,而当我们执行Build->Make Project之后,生成的.class文件会在app/build/intermediates/classes/debug目录下生成,如图: 所以我们需要切换到debug目录下,定位到debug目录下之后,输入命令:javah com.htq.baidu.ndk.NDKTest,即javah 包名+类名(其中类为定义了JNI函数的类),然后可以看到在debug目录下,为我们自动生成了JNI的.h文件,如图: 然后我们在整个工程上右键New->Folder->JNI Folder创建一个jni文件,位置默认即可,然后可以看到在main文件夹下产生了一个jni文件,然后我们把前面生成的jni的.h文件复制到该文件夹下,然后在该文件夹下创建一个.cpp的C++文件用来实现该头文件中的函数体,注意cpp文件名与头文件保持一致,在实现函数体的时候建议大家在书写函数名的时候直接从.h文件中复制过去,而不要手动输入,不然很容易出错的。此时工程主要结构如下: 其中.cpp中代码如下: [crayon-5f03bb8ea1684717101236/] 如果.cpp中jni.h出现红色,提示cannot find jni.h可能是ndk没配置,需要在File->Project Structure中选择你安装的ndk的路径,如图: 然后运行程序,可以看到界面显示hello.this is from native code,这句话正是我们在本地.cpp文件中返回的字符串,说明jni调用成功。如图: 然后我们依此打开app/build/intermediates/ndk/debug/lib目录可以看到自动生成了”x86″和”armeabi-v7a”两种架构的so文件,这两种架构就是前面在ndk节点的abiFilters中配置的信息,其中so的名称为libndk.so,即lib+前面在ndk节点的moduleName的信息+.so。如图: 然后我们用解压缩软件打开apk文件可以看到上面两种架构的so文件已经被打包到apk中了,这样使用android studio进行ndk开发的步骤就完成了。 如果不是在一个已经存在的安卓工程中增加JNI模块,而是直接创建一个包含JNI模块的安卓工程的话,可以直接使用AS向导在创建一个新工程的时候勾选include C++ support选项即可。如下所示: 勾选该选项之后AS向导会为我们自动创建一个包含cpp目录的jni工程,该cpp目录下即是我们要进行JNI开发的c/c++代码,只需要在该cpp目录增加我们的native函数实现即可。 注意:在实际开发中推荐大家直接使用AS向导创建NDK工程。前面之所以手动带领大家讲解下如何生成native函数头文件是为了让大家初次接触NDK时对细节有更清楚的掌握。比如了解如何使用javah命令生成头文件能够让我们更清楚的知道native函数在JNI中静态注册的命名规则,函数参数/签名/返回值描述符等概念,方便后面学习JNI函数的动态注册。  

安卓安全

安卓上使用Xposed进行hook

hook技术本质上是一种动态注入技术,我们知道任何一个App都是按照程序代码既定的流程一步一步的执行的,那么如果我们想要动态的去更改一个程序的执行流程或者截取某个流程中我们感兴趣的数据怎么办?如登陆劫持,盗取QQ账号。有2种思路: 修改原程序代码。比如通过逆向分析出QQ账号登陆的逻辑位于哪个函数。然后通过AOP框架插入打log的字节码或者smali插桩方式。但是这种方式修改了原apk后不一定能够成功二次打包成功。 不修改原程序,但是让原程序在执行到某个地方时流程能够跳到我们自己写的程序中,然后在我们自己写的程序中打印log。类似《水浒传》占山为王,等你路过我地盘时再下手。既然程序的代码是他人已经写好的,那我们唯一能做的就是添加我们自己的代码,也就是说在原程序的某个函数的执行过程中添加我们的代码(这里的添加是从执行流程的结果上来说的,不是真的添加),让程序能够重新走我们的代码流程,这样在我们的流程中我们就可以掌控一切。能够达到这个目的的框架就是XPosed。 不过Xposed需要在root手机上才能使用,而且开发的Xposed插件需要依赖Xposed框架才能运行。所以这个基本上无实际应用价值,但是可以作为逆向研究分析。     注:本文首次发表于huqi.tech,谢绝转载,如需转载,请注明出处:huqi.tech