插件化原理之hook系统函数
插件化主要问题之一是如startActivity一个未在注册表里面注册的acitivity。
我们都知道开启一个activity是涉及到app进程和系统服务进程的交互过程,其中验证要打开的acitivity是否在清单文件中也是在系统服务进程进行的,那么”如何”欺骗系统服务进程?
l 方案一是设置一个代理ProxyActivity,这个ProxyActivity在清单文件中注册过,然后在该ProxyActivity里面注入一个真实的Activity,ProxyActivity所有的生命周期方法里面都回调真实的Activity生命周期方法。关于这个方案有个框架实现的挺好的,可以看动态代理框架。这个方案调用startyActivity是要特别处理,首先要把真实的activity信息隐藏在intent里面,然后在代理ProxyActivity里面在解析出真正的activity信息并且实例化,然后回调真正的activity生命周期方法。
github地址:https://github.com/singwhatiwanna/dynamic-load-apk
l 方案二 是直接hook app的startActivity方法,这里先梳理一下startActivity的逻辑。startActivity虽然只有一行代码,但里面涉及的调用却很复杂,app持有一个AMS的Binder引用,在app端最终调用ActivityManagerNative.getDefault().startActivity后,系统服务进程开始做一些准备处理,而ActivityManagerNative.getDefault()是个单例模式,我们可以在这里传递一个注册表里面注册过的activity过去,这样系统服务进程验证的时候就会通过,然后回调给ApplicationThreadNative,ApplicationThreadNative再通过一个内部类H发送消息到ActivityThread,这样消息发送过来的时候我们再换回真实的activity,如此一来app就会认为这个activity已经被系统验证过了,生命周期的调用和其他acitivity的生命周期方法调用过程一模一样。查看startActivity调用流程时,发现ActivityManagerNative.getDefault()是个单例,我们可以反射重新设置,ActivityThread里面所有AMS发送过来的消息都会通过内部类H发送到主线程,查看Handler的源码我们发现,Hander在处理消息的方法是会先查看内部一个 变量Callback是否存在,如果存在则先处理Callback的方法,
Handler的dispatchMessage方法源码,我们通常是复写handleMessage,如果我们要拦截哪个方法,我们看一看设置Handler的Callback 并且设置handleMessage返回true,这样就总不会走Handler的handleMessage方法了。
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
我们可以在ActivityThread的H里面的mCallback里面拦截startActivity发出来的消息,然后在这里把真实的activity替换。
本文讲解的是第二种方案,当然这种方案要对startActivity具体流程有个清楚的认识,不认识的可以查看我的另一篇博客
从startActivity说起 https://blog.csdn.net/mr_lu_/article/details/80137617
主要方法如 下
public void hookSystem() throws Exception{
// 1 加载ActivityManagerNative类信息
Class>
activityManagerNative=Class.forName("android.app.ActivityManagerNative");
//2 获取gDefault字段属性
Field field= getField(activityManagerNative,"gDefault");
//3 获取一个对象。。。。Singleton
Object o= field.get(null);
DebugLog.d(o.toString());
//4获取Singleton类信息
Class> singleton=Class.forName("android.util.Singleton");
//5 获取mInstance 字段信息
Field field2= getField(singleton,"mInstance");
//6 获取该第三步对象里面的变量对象
Object mInstance=field2.get(o);
DebugLog.d(mInstance.toString());
MyInvocationHandler myInvocationHandler=new MyInvocationHandler(mInstance);
//7 生成代理类
Object proxy= Proxy.newProxyInstance(mInstance.getClass().getClassLoader(),mInstance.getClass().getInterfaces(),myInvocationHandler);
// //8 替换Singleton类里面的mInstance属性
field2.set(o,proxy);
//9获取ActivityThread类信息
activityThreadClass=Class.forName("android.app.ActivityThread");
//10获取mH字段信息,该变量是个Handler对象
Field mHField=getField(activityThreadClass,"mH");
//11 ActivityThread类里面有个唯一对象,就是sCurrentActivityThread属性
Field
sCurrentActivityThread=getField(activityThreadClass,"sCurrentActivityThread");
//12 获取到ActivityThread对象
activityThread=sCurrentActivityThread.get(null);
//13 获取ActivityThread对象 mH对象
Object mH= mHField.get(activityThread);
//14 获取Handler类里面的mCallback字段信息
Field field1=getField(Handler.class,"mCallback");
//15 设置mH对面里面的callback为我们自己写的callback
field1.set(mH,myCallback);
}
第二步我们就获取到ActivityManagerNative类里面的gDefault属性了
由于该变量是个static 所以第三步我们调用get传入null参数就可以获取该对象了,然后通过动态代理生成一个代理对象,由于代理对象的方法都会调用我们设置的MyInvocationHandler对象的invoke方法,故我们可以在这里拦截startActivity方法,我们把真实的activity随便替换一个在注册表里面注册过的activity信息传输到AMS那里。
查看Singleton源代码我们可以知道,该get方法返回的就是create方法创建的对象,也是内部的一个变量,我们只要把这个对象替换成我们的对象就可以了。
public abstract class Singleton<T> {
private T mInstance;
protected abstract T create();
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}
到第八步的时候我们已经把ActivityManagerNative里面的gDefault变量里面的mInstance变量换成我们自己的代理对象,这样AcitivityManagerNative.getDefault对象也就是我们的代理对象了。
12步的时候我们已经获取到当前ActivityThread类的唯一对象,这个和我们在自定义Application返回Application实例一样。
13步的我们已经获取到ActivityThread里面mH这个变量
15步的时候我们已经把ActivityThread里面的mH对象里面的mCallback设置成我们自己的callback,这样AMS发送给APP的消息,我们这里都能进行拦截。
MyInvocationHandler类代码如下
这里我们拦截了APP的startActivity方法,把要开启的Activity信息保存起来,替换一个新注册过的Activity信息放在里面
class MyInvocationHandler implements InvocationHandler {
Object target;
public MyInvocationHandler(Object o){
this.target=o;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("startActivity")){
int index=-1;
DebugLog.d("invoke:"+method.getName());
for (Object o:args){
index++;
if (o instanceof Intent){
// intent.setComponent(componentName);
Intent oldIntent= (Intent) o;
//把原来的intent的跳转类信息保存起来
// Intent intent=new Intent();
DebugLog.d("原来要跳转的getAction:"+oldIntent.getAction());
DebugLog.d("原来要跳转的getPackage:"+oldIntent.getPackage());
DebugLog.d("原来要跳转的Component():"+(oldIntent.getComponent()==null?"null":oldIntent.getComponent().toString()));
//替换
ComponentName componentName=new ComponentName(context,TestAidlActivity.class);
oldIntent.putExtra("realComponentName",oldIntent.getComponent());
oldIntent.setComponent(componentName);
break;
}
}
// if (index!=-1){
// args[index]=
// }
}
return method.invoke(target,args);
}
}
我们自己的callback
Handler.Callback myCallback=new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what==100){
DebugLog.d("handleMessage");
handleStartActivity(msg);
}
return false;
};
};
查看H源码我们知道开启一个activity消息是100.所以我们这里处理msg.what==100的情况,看下面知道所有的生命周期回调都有对应的消息,我们都能进行拦截处理。目前我们暂时处理开启activity的消息
这个方法就好理解了,获取msg里面的intent对象,然后把里面的Component设置成我们保持的那个Component信息
private void handleStartActivity(Message msg){
Object activityClientRecord= msg.obj;
try {
isReplaceOncreate=false;
Field field=getField(activityClientRecord.getClass(),"intent");
//拿到intent对象。
Intent intent= (Intent) field.get(activityClientRecord);
ComponentName componentName=intent.getParcelableExtra("realComponentName");
String className=componentName.getClassName();
Class> clazz=Class.forName(className);
if (clazz.newInstance() instanceof AppCompatActivity){
DebugLog.d("AppCompatActivity............");
isReplaceOncreate=true;
}
//重新替换过来
intent.setComponent(componentName);
} catch (Exception e) {
e.printStackTrace();
}
}
Manifest.xml信息如下
TestAidi2Activity我并没有在清单文件中注册。运行后打印信息如下
所有代码上次到github:https://github.com/helloworld777/hello-jni