一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第25天,点击查看活动详情。
一.简介
Hook技术是一种用于改变API执行结果的技术,Android系统中有一套自己的事件分发机制,所有的代码调用和回调都是按照一定顺序执行的,Hook技术存在的意义就在于,Hook可以帮助我们在Android中在SDK源代码逻辑执行过程中,通过代码手动拦截执行该逻辑,加入自己的代码逻辑。
为了保证hook的稳定性,一般拦截的点都会选择比较容易找到并且不易发生变化的对象,比如静态变量和单例。
提到Hook,就不得不说一下Java的反射机制。
二.反射
Java反射机制主要提供了以下功能:
在运行时判断任意一个对象所属的类
在运行时构造任意一个类的对象
在运行时判断任意一个类所具有的成员变量和方法
在运行时调用任意一个对象的方法
生成动态代理
在运行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制。
三.Hook使用案例分析
1.实现启动未注册的activity
比如我们想启动一个activity,如果未在AndroidManifest.xml里面注册的话,调用Context.startActivity()时会出现以下异常:
10:44:54.811 E/AndroidRuntime(25309): Caused by: android.content.ActivityNotFoundException: Unable to
find explicit activity class {com.xxx.learn/com.xxx.learn.HookActivity}; have you declared this activity in
your AndroidManifest.xml?
下面就一起来实现启动一个未在AndroidManifest.xml注册的activity。
a.寻找hook点
对于Context.startActivity,由于Context的实现类为ContextImpl,因此直接分析ContextImpl类的startActivity()的方法:
@Override
public void startActivity(Intent intent) {
warnIfCallingFromSystemProcess();
startActivity(intent, null);
}
@Override
public void startActivity(Intent intent, Bundle options) {
......
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity) null, intent, -1, options);
}
从上面可以看到,最终会调用mMainThread.getInstrumentation().execStartActivity(),mMainThread是ActivityThread实例,getInstrumentation()返回的是Instrumentation实例,实际上使用了ActivityThread类的mInstrumentation成员的execStartActivity方法;而ActivityThread 实际上是主线程,因为主线程一个进程只有一个,所以这里是一个良好的Hook点,即Hook主线程对象。
b.选择合适代理方式
要将这个主线程对象里面的mInstrumentation替换成修改过的代理对象;要替换主线程对象里面的字段,得先拿到主线程对象的引用,如何获取呢?
ActivityThread类里面有一个静态方法currentActivityThread,通过它可以拿到这个对象类;但ActivityThread是一个隐藏类,需用反射去获取拿到currentActivityThread后,要修改它的mInstrumentation字段为修改后的代理对象。实现如下:
public static void hookInstrumentation(Context context) {
try {
//step1:先获取到当前的ActivityThread对象, 该对象是mInstrumentation的持有者
Class<?> activityThreadClz = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClz.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
//step2:从ActivityThread里面拿到原始的mInstrumentation
Field instrumentation = activityThreadClz.getDeclaredField("mInstrumentation");
instrumentation.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) instrumentation.get(currentActivityThread);
//step3:创建Instrumentation的代理对象[接下来会讲到]
Instrumentation proxyInstrumentation = new ProxyInstrumentation(mInstrumentation, context.getPackageManager());
//step4:将持有的Instrumentation原始对象替换成代理对象[将currentActivityThread里面的instrumentation变量替换为proxyInstrumentation]
instrumentation.set(currentActivityThread, proxyInstrumentation);
} catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
接下来实现这个代理对象,由于JDK动态代理只支持接口,而这个Instrumentation是一个类,因此只能手写一个静态代理类,用来覆盖掉原始的方法,实现如下:
public class ProxyInstrumentation extends Instrumentation {
private Instrumentation mBase;
private PackageManager mPm;
public ProxyInstrumentation(Instrumentation base, PackageManager pm) {
mBase = base;
mPm = pm;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
Log.d("Seven", "------Hook打印execStartActivity------");
List<ResolveInfo> resolveInfos = mPm.queryIntentActivities(intent, PackageManager.MATCH_ALL);
//检查要跳转的activity是否在Manifest.xml里面注册
if (resolveInfos == null || resolveInfos.size() == 0) {
//把要跳转的activity记录下来,在接下来newActivity还原的时候要拿来还原
intent.putExtra("intent_name", intent.getComponent().getClassName());
//把要跳转的activity改成已经在Manifest.xml里面注册过的MainActivity
intent.setClassName(who, "com.xxx.learn.MainActivity");
}
// 开始调用Instrumentation原始的方法,由于这个方法是隐藏的,因此需要使用反射调用;
try {
//找到这个方法
Method execStartActivity = Instrumentation.class.getDeclaredMethod(
"execStartActivity", Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
execStartActivity.setAccessible(true);
//通过反射调用Instrumentation的execStartActivity方法
return (ActivityResult) execStartActivity.invoke(mBase, who,
contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
throw new RuntimeException("do not support!");
}
}
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
//取出上步记录下来的要跳转的activity并进行替换,来启动未注册的activity
String intentName = intent.getStringExtra("intent_name");
Log.d("Seven", "------newActivity内进行替换------");
if (!TextUtils.isEmpty(intentName)) {
return super.newActivity(cl, intentName, intent);
}
return super.newActivity(cl, className, intent);
}
}
c.启动运行
HookUtils.hookInstrumentation(mContext);
private void hookActivity() {
Intent i = new Intent();
i.setClass(mContext, HookActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.getApplicationContext().startActivity(i);
}
11:28:37.627 D/Seven ( 1852): ------Hook打印execStartActivity------
11:28:37.665 D/Seven ( 1852): ------newActivity内进行替换------
至此,一个未在AndroidManifest.xml里面注册的activity通过hook技术方式就可以启动了。