一.简介
Hook技术是一种用于改变API执行结果的技术,Android系统中有一套自己的事件分发机制,所有的代码调用和回调都是按照一定顺序执行的,Hook技术存在的意义就在于,Hook可以帮助我们在Android中在SDK源代码逻辑执行过程中,通过代码手动拦截执行该逻辑,加入自己的代码逻辑。
为了保证hook的稳定性,一般拦截的点都会选择比较容易找到并且不易发生变化的对象,比如静态变量和单例。
提到Hook,就不得不说一下Java的反射机制。
二.反射
Java反射机制主要提供了以下功能:
在运行时判断任意一个对象所属的类
在运行时构造任意一个类的对象
在运行时判断任意一个类所具有的成员变量和方法
在运行时调用任意一个对象的方法
生成动态代理
在运行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制。
三.Hook使用案例分析
1.实现启动未注册的activity
比如我们想启动一个activity,如果未在AndroidManifest.xml里面注册的话,调用Context.startActivity()时会出现以下异常:
1-25 10:44:54.811 E/AndroidRuntime(25309): Caused by: android.content.ActivityNotFoundException: Unable to
find explicit activity class {com.hly.learn/com.hly.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.hly.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);
}
1-25 11:28:37.627 D/Seven ( 1852): ------Hook打印execStartActivity------
1-25 11:28:37.665 D/Seven ( 1852): ------newActivity内进行替换------
至此,一个未在AndroidManifest.xml里面注册的activity通过hook技术方式就可以启动了。
2.拦截点击事件统计点击次数
a.寻找hook点
对一个view设置点击事件的调用流程如下:
hkBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
static class ListenerInfo {
......
/**
* Listener used to dispatch click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
public OnClickListener mOnClickListener;
......
}
Hook操作onClickListener实例,需要首先获取到onClickListener的拥有者,即:ListenerInfo,最后通过ListenerInfo获取到原始的mOnClickListener。
b.选择合适的代理方式
从上面的流程可以发现,要将ListenerInfo类里面的mOnClickListener替换成修改过的代理对象;要替换ListenerInfo里面的字段,得先拿到ListenerInfo,View类中有一个方法getListenerInfo() ,通过它可以拿到这个ListenerInfo,实现如下:
public static void hookOnClickListener(View view) {
//step1:反射执行View类的getListenerInfo()方法,拿到view的mListenerInfo对象,这个对象是点击事件mOnClickListener的持有者
try {
Class<?> viewClz = Class.forName("android.view.View");
Method method = viewClz.getDeclaredMethod("getListenerInfo");
//由于getListenerInfo()方法并不是public的,所以要加这个代码来保证访问权限
method.setAccessible(true);
//拿到mListenerInfo,也就是点击事件的持有者
Object listenerInfo = method.invoke(view);
//step2:找到mListenerInfo持有的点击事件对象mOnClickListener
//内部类的表示方法:android.view.View$ListenerInfo
Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
Field onClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
final View.OnClickListener onClickListenerInstance = (View.OnClickListener) onClickListener.get(
listenerInfo);
//step3:创建自己点击事件的OnClickListener代理类
动态或静态创建OnClickListener的代理类proxyOnClickListener
//step4:将持有者拥有的点击事件替换成代理对象[将listenerInfo里面的onClickListener变量替换为proxyOnClickListener]
onClickListener.set(listenerInfo, proxyOnClickListener);
} catch (NoSuchMethodException | IllegalAccessException | ClassNotFoundException | NoSuchFieldException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
接下来要创建OnClickListener的代理对象,由于OnClickListener是一个接口,因此可以使用JDK动态代理方式Java动态代理,也可以用静态代理类实现,实现方式如下:
//方式1:自己实现代理类,将原始的View.OnClickListener对象onClickListenerInstance作为参数传入
ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);
static class ProxyOnClickListener implements View.OnClickListener{
private View.OnClickListener listener;
private int clickCount = 0;
ProxyOnClickListener(View.OnClickListener listener){
this.listener = listener;
}
@Override
public void onClick(View v) {
clickCount++;
Log.d("Seven", "Hook OnClickListener 2 click count " + clickCount);
if(this.listener != null){
this.listener.onClick(v);
}
}
//方式2:由于View.OnClickListener是一个接口,所以可以直接用动态代理模式
//参数:类的加载器,要代理实现的接口(用Class数组表示,支持多接口),代理类的实际逻辑封装在new出来的InvocationHandler内
Object proxyOnClickListener = Proxy.newProxyInstance(View.OnClickListener.class.getClassLoader(),
new Class[]{View.OnClickListener.class}, new InvocationHandler() {
private int clickCount = 0;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
clickCount++;
Log.d("Seven", "Hook OnClickListener 1 click count " + clickCount);
return method.invoke(onClickListenerInstance, args);
}
});
c.启动运行
hkBtn.setOnClickListener(this);
HookUtils.hookonClickListener(hkBtn);
运行结果如下:
1-25 10:51:12.190 D/Seven (26125): Hook OnClickListener 1 click count 1
1-25 10:51:21.428 D/Seven (26125): Hook OnClickListener 1 click count 2
1-25 10:51:21.607 D/Seven (26125): Hook OnClickListener 1 click count 3
1-25 10:51:21.770 D/Seven (26125): Hook OnClickListener 1 click count 4
1-25 10:51:21.918 D/Seven (26125): Hook OnClickListener 1 click count 5
1-25 10:51:22.068 D/Seven (26125): Hook OnClickListener 1 click count 6
1-25 10:51:22.217 D/Seven (26125): Hook OnClickListener 1 click count 7
1-25 10:51:22.394 D/Seven (26125): Hook OnClickListener 1 click count 8
1-25 10:51:22.543 D/Seven (26125): Hook OnClickListener 1 click count 9
1-25 10:51:22.722 D/Seven (26125): Hook OnClickListener 1 click count 10
1-25 10:51:22.854 D/Seven (26125): Hook OnClickListener 1 click count 11
1-25 10:51:23.009 D/Seven (26125): Hook OnClickListener 1 click count 12
1-25 10:51:23.158 D/Seven (26125): Hook OnClickListener 1 click count 13
至此已经实现了对点击事件的hook。
3.总结
针对以上两个案例分析,我们可以看到在进行hook时主要分为以下几步:
a.寻找合适的hook点;
b.找到Hook点对应的class;
c.直接通过class或class内部的method找到对应的filed;
d.替换对象是interface的话,直接用动态代理方式创建对象;否则,直接写个类继承要替换的类;
e.调用filed.set(obj, proxy)来对变量进行替换;
f.针对hook点进行相应的处理;
hook技术涉及到的知识点主要有反射、代理及android源码的熟练程度。