1.前言
前段时间,DroidPlugin和VirtualApp开源插件很火,因为他们实现了应用免安装、应用多开的功能。满足了有些公司“奇异”的需求。当然我们公司就是其中一个,所以不得不去研究它们。这两个插件都是利用反射和Hook动态代理去Hook掉系统的一些类(如AMS、PMS)换成自己的代理类从而实现拦截系统功能执行自己的功能。
DroidPlugin插件地址:https://github.com/DroidPluginTeam/DroidPlugin/
VirtualApp插件地址:https://github.com/asLody/VirtualApp
下面是我在研究过程中整理的一些资料,在这里做下记录。
2.apk安装原理
- 应用安装涉及到如下几个目录
system/app — 系统自带的应用程序,获得adb root权限才能删除。
data/app — 用户程序安装的目录。用户安装时把apk文件复制到此目录。
data/data — 存放应用程序的数据。
data/dalvik-cache — 将apk中的dex文件安装到dalvik-cache目录下(dex文件是dalvik虚拟机的可执行文件,其大小约为原始apk文件大小的四分之一) - 安装过程
复制apk安装包到data/app目录下,解压并扫描安装包,把dex文件(Dalvik字节码)把存到dalvik-cache目录,并data/data目录下创建对应的应用数据目录。 - 卸载过程
删除安装过程中在上述三个目录下创建的文件及目录。
apk安装主要分为四种:系统安装、网络下载安装、ADB工具安装、从SD卡安装。 - 开机安装为例简单描述安装过程:
PackageManagerService处理各种应用的安装、卸载、管理等工作,开机时有sysetemServer启动此服务。
1、首先扫描安装“system\framework”目录下的jar包
2、扫描安装系统system/app的应用程序
3、制造商的目录下/vendor/app应用包
4、扫描“data\app”目录,即用户安装的第三方应用
5、扫描“data\app-private”目录,即安装DRM保护的apk文件(一个受保护的歌曲或受保护的视频是使用DRM保护的文件),并且从该扫描方法中可以看出调用了scanPackageLI(),跟踪此方法发现,程序中经过多次的if else的筛选,最后判定可以安装apk后调用了mInstaller.install(),最后只会installed在C语言的文件中完成工作。 - PackageManagerService小结:
a、从apk,xml中载入package信息,存储到内部成员变量中,用于后面的查找,关键的方法是scanPackageLI()
b、各种查询操作,包括query Intent操作
c、install package和delete package的操作,还有后面的关键方法installPackageLI()
其余三种安装 都是各自绕了一段,最终都会走到和开机安装相同的部分(此处省略)。 - 总结:
a、系统应用安装 —— 开机时完成
b、网络下载应用安装 —— 最后会调用 系统应用安装的 scanPackageLI接口
c、ADB工具安装 —— 最后会调用 网络下载应用安装 的installPackage 接口,从而相当于 走的还会系统安装的路径
d、第三方应用安装 —— 由packageinstall.apk应用处理安装及卸载,最后也会调用 网络下载应用安装 的 installPackage 接口,从而相当于走的还是系统应用安装的路径。
3.反射机制
- Class类:用于描述一切类/接口的行为和状态(字节码)。枚举是一种类,注解是一种接口。
- 获取Class的实例:
① 类名.class 如:Date.class
② Class.forName(String className); className:全限定名;如:com.example.test
③ 每个对象都有一个getClass()方法; 如:obj.getClass();得到对象的真实类型; - 一个类在jvm中只有一份字节码。
- 基本数据类型没有类的权限定名,没有getClass()方法;可以通过类名.class来获取;Class clz = int.class;
基本数据类型:byte/short/int/long/double/float/boolean/char/void关键字 - 基本数据类型的包装类都有一个常量:TYPE 表示:该包装类对应的基本数据类型的Class实例(除void外)。如:Integer.TYPE = int.class;
- 得到构造函数(方法类似):设置绕过安全检查:setAccessible(true);
Class clz2 = Class.forName(“text.ClassInvoke”);
Constructor constructor = clz2.getDeclaredConstructor(String.class);
constructor.setAccessible(true);//设置绕过安全检查
constructor.newInstance(“fffff”); 得到方法:invoke(obj,params):当此方法是静态的时,obj可以传null; getDeclaredMethod():得到此类不管权限的所有方法(不包括父类方法)
Method method = clz2.getDeclaredMethod(“getInt”,int.class);
method.setAccessible(true);
int a = (int) method.invoke(clz2.newInstance(),1);
注意:某些方法参数是可变参数时(如:name(String… names)),若直接传参数,可能会报错。因为系统在解析参数时会做解包操作。所以我们约定:
method.invoke(clz2.newInstance(),new Object[]{new String[]{name,name1,name2}});//可变字符串型
或method.invoke(clz2.newInstance(),new Object[]{1});//简单参数型
- 反射调用一个泛型参数的方法:如:Arrays.asList(T list);
Class clz = Arrays.class;
Method m = clz.getMethod(“asList”,Object[].class);
m.invoke(null,new Object[]{new String[]{“a”,”b”}}) - 反射获取一个泛型参数信息:如 Map
Class clz = Object.class;
Field f = clz.getDeclaredField(“cache”);
field.setAccessible(true);
ParameterizedType type = (ParameterizedType) field.getGenericType();
Type[] types = type.getActualTypeArguments();
System.out.println(types[0]);//String
System.out.println(types[1]);//Object - eclipse导相同项目时,会导不进去,此时可以修改项目的.project文件里的项目名即可导进去。
- path:表示去哪个路径找编译工具javac、运行工具java等。
classpath:表示去哪个路径找字节码文件。 - 加载配置文件:
Properties properties = new Properties();
properties.load(stream);
//fileName:绝对路径
FileInputStream stream = new FileInputStream(fileName);
ClassLoader loader = text.class.getClassLoader();//一种获取实例
ClassLoader loader = Thread.currentThread().getContextClassLoader();//二种获取实例
//fileName:文件名,相对于classpath根目录
FileInputStream stream = loader.getResourceAsStream(fileName);
4.Hook动态代理
- HOOK:我们自己创建代理对象,然后把原始对象替换为我们的代理对象;
- 找HOOK点原则:静态变量和单例;在一个进程之内,静态变量和单例变量是相对不容易发生变化的,因此非常容易定位,而普通的对象则要么无法标志,要么容易改变。
- 动态代理机制:
两个重要的类或接口:一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class)
每一个动态代理类都必须要实现InvocationHandler这个接口,并且每个代理类的实例都关联到了一个handler,
当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。
invoke()方法的三个参数:
proxy: 指代我们所代理的那个真实对象
method: 指代的是我们所要调用真实对象的某个方法的Method对象
args: 指代的是调用真实对象某个方法时接受的参数 如何创建动态代理:Object mProxy = Proxy.newProxyInstance(ClassLoader loader,Class
5.Hook之Binder Hook
Android系统通过Binder机制给应用程序提供了一系列的系统服务,诸如ActivityManangerService,ClipboardManager,AudioManager等。
系统的各个远程service对象都是以Binder的形式存在的,而这些Binder有一个管理职者,那就是ServiceManager;
系统Service的使用其实就分为两步:
1、IBinder b = ServiceManager.getService(“service_name”); // 获取原始的IBinder对象
2、IXXInterface in = IXXInterface.Stub.asInterface(b); // 转换为Service接口
要达到修改系统服务的目的,需要如下两个步骤:
1、首先肯定需要伪造一个系统服务对象接下来就要想办法让asInterface能够返回我们这个伪造对象而不是原始的系统服务对象。
2、通过上下文我们知道,只要让getService返回IBinder对象的queryLocalInterface方法直接返回我们伪造过的系统服务对象就能达到目的。所以,我们需要伪造一个IBinder对象,只要是修改它的queryLocalInterface方法,让它返回我们伪造的系统服务对象;然后把这个伪造对象放置在ServiceManager的缓存map里面即可。
我们通过Binder机制的优先查找本地Binder对象的这个特性达到了Hook掉系统服务对象的目的。因此queryLocalInterface也失去了它原本的意义(只查找本地Binder对象,没有本地对象返回null),这个方法只是一个傀儡,是我们实现hook系统对象的桥梁:我们通过这个“漏洞”让asInterface永远都返回我们伪造过的对象。由于我们接管了asInterface这个方法的全部,我们伪造过的这个系统服务对象不能是只拥有本地Binder对象(原始queryLocalInterface方法返回的对象)的能力,还要有Binder代理对象操纵驱动的能力。
6.Binder
- Android系统的四大组件,AMS,PMS等系统服务无一不与Binder挂钩。
- Binder通信模型:
Binder框架定义了四个角色:Server,Client,ServiceManager以及Binder驱动。其中Server,Client,ServiceManager运行于用户区间,驱动运行去内核空间。这四个角色的关系和互联网类似:Server是服务器,Client是客户端,ServiceManager是域名服务器(DNS),驱动是路由器。
Android使用Binder机制跨进程通信主要是性能好和安全,Binder机制从协议本身就支持对通信双方做身份校验,因而大大提升了安全性。
Server进程的本地对象仅有一个,其它进程所拥有的全部是它的代理。Client进程只不过是持有了Server端的代理:代理对象协助驱动完成了跨进程通信。 - Binder的手机采用了面向对象的思想,在Binder通信某星中有四个角色:
1、通常意义下,Binder指的是一种通信机制,我们说的AIDL使用Binder进行通信,指的就是Binder这种IPC机制。
2、对于Server进程而言,Binder指的是Binder本地对象
3、对于Client来说,Binder指的是Binder代理对象,它只是Binder本地对象的一个远程代理;对于这个Binder对象使用者而言,它无需关心这事一个Binder代理对象还是Binder本地对象;对于代理对象的操作和对本地对象的操作对它来说没有区别。
对于传输过程而言,Binder是可以尽享跨进程传递的对象,Binder驱动会对具体跨进程传递能力的对象做特殊处理:自动完成代理对象和本地对象的转换。面相对象思想的引入将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体(本地对象)位于一个进程中,而它的引用(代理对象)却遍布于系统的各个进程之中。
7.关于so文件需要了解的一些知识
- Android系统目前支持以下其中不同的CPU架构:ARMv5, ARMv7(2010年起), x86(2011年起), MIPS(2012年起),ARMv8, MIPS64和x86_64(2014年起),每一种都关联着一个相应的ABI。应用程序二进制接口(Application Binary Interface)定义了二进制文件(尤其是.so文件)如果运行在相应的系统平台上,从使用的指令集,内存对齐到可用的系统函数库。在Android系统上,每一个CPU架构对应一个ABI:armeabi, armeabi-v7a, x86, mips, arm64-v8a, mips64, x86_64。
我们可以通过Build.SUPPORTED_ABIS得到根据偏好排序的设备支持的ABI列表。但你不应该从你的应用程序中读取它,因为Android包管理器安装APK时,会自动选择APK包中为对应系统ABI预编译好的.so文件,如果在对应的lib/abi目录中处在.so文件的话。 - App中可能出错的地方:
处理.so文件时有一条简单却并不知名的重要法则:尽可能的提供专为每个ABI优化过的.so文件,但要么全部支持,要么都不支持,不应该混合使用。应该为每个ABI目录提供对应的.so文件。
当一个应用安装在设备上,只有该设备支持的CPU架构对应的.so文件会被安装。在x86设备上,libs/x86目录中如果存在.so文件的话,会被安装,如果不存在,则会选择armeabi-v7a中的.so文件,如果也不存在,则选中armeabi目录中的.so文件(因为x86设备也支持armeabi-v7a和armeabi)。 - 其它地方也可能出错:
当你引入一个.so文件时,不止影响到CPU架构,还可以看到一系列常见的错误,其中最多的是“UnsatisfiedLinkError”,“dlopen:failed”以及其他类型的crash或者低下的性能。NDK不是后向兼容而是前向兼容的。所以.so文件推荐使用minSdkVersion对应的编译平台去编译。
64位设备能够运行32位的函数库,但是以32位模式运行,在64位平台上运行32位版本的ART和Android组件,将丢失专为64位优化过的性能(ART, webview, media等)。
8.研究Native Hook之NDK相关资料
1. 开发NDK程序流程
- 创建Android工程
- 声明native方法
方法名使用 native 修饰, 没有方法体 和 参数, eg : public native String helloFromJNI(); - 创建c文件
在工程根目录下创建 jni 目录, 然后创建一个c语言源文件, C语言方法声明格式 jstring Java_shuliang.han.ndkhelloworld_MainActivity_helloFromJNI(JNIEnv *env) , jstring 是 Java语言中的String类型, 方法名格式为 : Java_完整包名类名_方法名();
JNIEnv参数 : 代表的是Java环境, 通过这个环境可以调用Java里面的方法;
jobject参数 : 调用C语言方法的对象, thiz对象表示当前的对象, 即调用JNI方法所在 的类; - 编写Android.mk文件
LOCAL_PATH : 代表mk文件所在的目录;
LOCAL_MODULE : 编译后的 .so 后缀文件叫什么名字;
LOCAL_SRC_FILES: 指定编译的源文件名称;
include $(BUILD_SHARED_LIBRARY) : 告诉编译器需要生成动态库; - NDK编译生成动态库
点击Project->Properties->Builders->New,新建立一个Builder。在弹出的对话框上面点击 Program在弹出的对话框【Edit Configuration】中,配置选项卡【Main】:
Location中需要填入nkd-build.cmd的路径(NDK安装目录下)。
Working Diretcoty中需要填入TestNDK的工程根目录。
具体参考:http://www.cnblogs.com/common1140/p/3998730.html - Java中加载动态库
在Java类中的静态代码块中使用System.LoadLibrary()方法加载编译好的 .so 动态库; - 更多资料
链接:http://blog.csdn.net/shulianghan/article/details/18964835
2.C语言相关知识
- 定义常量:#define WIDTH 5 或 const int LENGTH = 10;
- &a:a变量的地址
- 指针:指针是一个变量,其值为另一个变量的地址。
所有指针的值的实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,都是一样的,都是一个代表内存地址的长的十六进制数
ip:表示某个变量的地址,*ip:表示该地址对应的变量的值。 - 数组指针:double a[50]; a 是一个指向&a[0]的指针,即数组a的第一个元素的地址。*(a + 4)是一种访问a[4]数据的合法方式。
- typedef:使用它来为类型取一个新的名字
- 结构体-函数指针:如:
struct DEMO {
int x,y;
int (*func)(int,int); //函数指针
};
int add1(int x,int y) {
return x*y;
}
调用方式:
struct DEMO demo;
demo.func=add1; //结构体函数指针赋值
//demo.func=&add1; //结构体函数指针赋值
printf(“func(3,4)=%d\n”,demo.func(3,4)); - 方法签名:签名是由两部分组成,()里面代表的是方法的参数,后面外面的部分代表的是该方法的返回值,如:
public int test3(int i) { return i;} 签名:(I)I
基本数据类型:Boolean 比较特殊, 对应的是 Z , Long 对应J
Z boolean
B byte
C char
S short
I int
J long
F float
D double
引用数据类型:以“L”开头,以“;”结束,中间对应的是该类型的路径, 数组表示的时候以“[” 为标志,一个“[”表示一维数组。
如:(Ljava/lang/String;)V , ([[J)V:参数为二维long数组 - vm->GetEnv((void **) &env, JNI_VERSION_1_6):获得JNIEnv
(*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL):获得JNIEnv - env->DeleteLocalRef(javaClass); //立即释放引用
- 实现JNI的另一种方法:使用RegisterNatives方法传递
和传统方法相比,使用RegisterNatives的好处有:
1、C++中函数命名自由,不必像javah自动生成的函数声明那样,拘泥特定的命名方式;
2、效率高。传统方式下,Java类call本地函数时,通常是依靠VM去动态寻找.so中的本地函数(因此它们才需要特定规则的命名格式)
而使用RegisterNatives将本地函数向VM进行登记,可以让其更有效率的找到函数
typedef struct {
char *name;
char *signature;
void *fnPtr;
} JNINativeMethod;
name是java中定义的native函数的名字.
signature是java native函数的签名,可以认为是参数和返回值。
fnPtr是函数指针,也就是C++中java native函数的实现。
资料链接:http://www.cnblogs.com/wainiwann/p/3835904.html - 判断java虚拟机是art还是dalvik:System.getProperty(“java.vm.version”).startsWith(“2”);
- jmethodID结构体:
insns:如果这个方法不是Native的话,则这里存放了指向方法具体的Dalvik指令的指针
(这个变量指向的是实际加载到内存中的Dalvik指令,而不是在Dex文件中的)。
如果这个方法是一个Dalvik虚拟机自带的Native函数(Internal Native)的话,则这个变量会是Null。
如果这个方法是一个普通的Native函数的话,则这里存放了指向JNI实际函数机器码的首地址;
nativeFuc:在Dalvik虚拟机下面,这个指向的是桥接函数;在Art虚拟机下,指向的是native函数。
accessFlags:可以判断一个方法是不是Native的(和0x00100相与),如果是Native的话,就直接执行nativeFunc所指向的本地代码,
如果不是的话就执行insns所指向的Dalvik代码。
9.DroidPlugin研究记录
- com.morgoo.droidplugin:
该包下主要功能是启动插件的入口和记录插件遇到的错误信息。
MyCrashHandler和PluginPatchManager这两个类是捕获和记录插件错误。
PluginHelper和PluginManagerService这两个类分别是安装一些hook系统的钩子和启动插件服务。 - com.morgoo.droidplugin.am:
该包下主要功能是管理插件启动后的activity、service、process。
MyActivityManagerService类是一个复杂的进程管理类,预定义N个进程。每个进程下有4中launchmod的activity,1个服务,一个ContentProvider。该类会记录启动的activity、service和process。会回收已被销毁的进程。
RunningActivities类记录运行的activity。
RunningProcesList类记录运行的进程。 - com.morgoo.droidplugin.core:
Env类:插件的常量字段。
PluginClassLoader类:加载插件的classLoader。
PluginDirHelper类:插件的缓存数据的目录管理类,包括安装apk后,apk文件缓存路径、插件sharePrefence、db路径、加载dex缓存路径、插件的so文件路径。
PluginProcessManager类:插件进程管理类,包括记录些进程的processName、classloader、以及hook掉系统的含有application引用的类。修改为插件的context和classloader。 - com.morgoo.droidplugin.hook:
HookedMethodHandler类:是动态代理方法的一个控制类,将所有代理的方法分为方法执行前(beforeInvoke)和方法执行后(afterInvoke)。
BaseHookHandle类:该类有个map数组,用来存hook掉的方法名和该方法的代理。
Hook类:mEnable该字段表示该hook是否生效,mHookHandles该字段是方法的代理控制类,以及安装和卸载hook的方法。
HookFactory类:是安装hook钩子的工具类。 - com.morgoo.droidplugin.hook.binder:
该包下主要都是hook系统的一些类并通过createHookHandle方法创建方法代理。 - com.morgoo.droidplugin.hook.handle:
该包下主要是实现hook掉系统方法的动态代理。 - com.morgoo.droidplugin.hook.xhook:
主要是对数据库的hook,但目前没有用到。 - com.morgoo.droidplugin.pm:
该包下是插件管理的相关类,主要是插件的安装、更新、卸载、得到插件的信息如包名、classloader、签名等。 - com.morgoo.droidplugin.pm.parser:
一些兼容系统版本的apk文件解析类。 - com.morgoo.droidplugin.reflect:
一些反射的工具类 - com.morgoo.droidplugin.stub:
该包下是一些预先定义的activity、service、provider等。ServcesManager类是服务的管理类。 - com.morgoo.helper:
一些辅助工具。 - com.morgoo.helper.compat:
一些与系统类对应的辅助类,这些类的作用是通过反射直接可以得到系统类的Class对象或者系统类的某个字段。
10.集成DroidPlugin插件新增的功能:
解决64位环境的应用在32位环境的宿主中无法安装问题
第一、修改com.morgoo.helper.compat此包下的NativeLibraryHelperCompat类的isVM64()方法里的第二个判断,其内容改为return false;意思是当插件里没有so文件时,默认该插件运行环境为32位的。
第二、将宿主项目下libs文件夹下的arm64-v8a、x86_64、mips64文件夹都删掉。
解决免安装应用里内部更新问题(更新也是在宿主中更新,并不会安装在系统上)
解决思路:其实更新apk也就是重新安装一遍apk,只是数据不删除,也就是调用系统的startActivity方法来安装apk的。所以我们只要hook掉startActivity方法就可以拦截系统的安装。修改地方如下:
修改com.morgoo.droidplugin.hook.handle包下的IActivityManagerHookHandle类,新增了一个interceptSystemInstall方法用来拦截更新,此方法在此类208行出调用。
修改com.morgoo.droidplugin.pm下的IPluginManager文件,添加一个updateApk接口
然后再IPluginManagerImpl此类里实现该方法。
解决免安装应用的Umeng统计报错问题
原因:umeng统计报错是因为起内部将数据库路径写死了,由于在插件环境下,很多路径都改了,所有打开应用时就报错了,找不到数据库路径。
解决方法:在免安装应用进程里找到相应的classLoader加载器,通过反射将其写死的数据库路径改掉。修改地方:
修改com.morgoo.droidplugin.hook.handle包下的PluginCallback类的406行,新增了hookUmengDb这个方法。
- 解决在免安装应用里按返回键直接回到系统桌面问题
解决方法:找到系统提供的回到系统桌面的方法,将其hook掉。修改地方:
修改com.morgoo.droidplugin.hook.handle包下的IActivityManagerHookHandle类的148行,新增一个moveActivityTaskToBack类,解决返回桌面问题。
解决免安装应用生成快捷方式问题
此插件默认就支持生成快捷方式,不需要次功能,注释掉就可以了。
修改com.morgoo.droidplugin.hook.handle包下的IActivityManagerHookHandle类的634行。 - 改免安装应用的数据缓存路径
修改com.morgoo.droidplugin.core包下的PluginDirHelper类,在此类里打有TODO标记的就是修改过的。 - 解决改变安装应用的数据缓存路径导致的问题
1.如果有免安装应用的缓存数据在,要想直接能打开此应用,必须去检测该应用的so文件,在宿主的数据缓存路径下是否已经存在了,如果不存在就得拷贝。修改处:
修改com.morgoo.droidplugin.hook.handle包下的PluginCallback类的390行,添加检测和拷贝插件的so文件功能。
2.如果免安装应用里用到了动态加载dex的功能,其设置的dex缓存路径被我们改为了sd卡,所以应用一打开也会报错。修改处:
修改com.morgoo.droidplugin.hook.handle包下的LibCoreHookHandle类的190行,重写父类的beforeInvoke方法。将路径改为私有路径。
11.推荐链接
- 田维术的博客,可以说把Hook机制讲的清楚透彻,激情澎湃,淋淋尽致,曾经还有发邮件请教过他一些问题,给我非常大的帮助,非常感谢这位作者。
博客地址:http://weishu.me/ - 这位博客的作者可以说是教会了我怎么去Hook Native层的原理。
博客地址:http://www.jianshu.com/p/052b6dd45659 - Hook Native相关的博客:http://bbs.pediy.com/thread-196877.htm