上一篇文章里面已经把动态代理的作用以及实现方法分析了一下,很明显我们可以用HooK做很多事情,比如例子里面的代理做了拿了回扣和偷换行货这种肮脏龌龊的事情。
在真正应用的时候我们可以做更多的事情,比如用户登录的时候动态代理他的验证方法,是不是就可以获取用户的账号密码呢?还有比如Activity的启动,我们使用动态代理的手段将contextImp
对象进行动态代理,对startActivity()
函数进行修改,比如修改Intent的flag或者Intent内部数据等等,导致跳转的页面发生变化,或者改变传递的信息等等。
而实现HOOK的步骤一般分为三步:
1. 寻找Hook点,原则是静态变量或者单例对象,尽量Hook pulic的对象和方法,非public不保证每个版本都一样,需要适配。
2. 选择合适的代理方式,如果是接口可以用动态代理;如果是类可以手动写代理也可以使用cglib。
3. 偷梁换柱——用代理对象替换原始对象
下面以改变startActivity()
逻辑为例来展示HOOK的威力。
首先我们得找到被Hook的对象,我称之为Hook点;什么样的对象比较好Hook呢?自然是容易找到的对象。什么样的对象容易找到?静态变量和单例;在一个进程之内,静态变量和单例变量是相对不容易发生变化的,因此非常容易定位,而普通的对象则要么无法标志,要么容易改变。我们根据这个原则找到所谓的Hook点。
然后我们分析一下startActivity的调用链,找出合适的Hook点。
我们知道对于Context.startActivity
和 Activity.startActivity
的调用链与之不同,由于Context的实现实际上是ContextImpl;我们看ConetxtImpl类的startActivity
方法:
@Override
public void startActivity(Intent intent, Bundle options) {
warnIfCallingFromSystemProcess();
if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
+ " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ " Is this really what you want?");
}
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity)null, intent, -1, options);
}
可以看到contextImp的startActivity的逻辑是:
(1) 先判断intent的flag是不是FLAG_ACTIVITY_NEW_TASK
类型的,如果是走步骤2;如果不是,说明startActivity
不是在activity中调用的,在activity中调用的话,走的流程如下:
Step 1. Activity.startActivity通过指定名称“activity.subactivity”来告诉应用程序框架层,它要隐式地启动SubActivity。所不同的是传入的参数intent没有Intent.FLAG_ACTIVITY_NEW_TASK标志,表示这个SubActivity和启动它的MainActivity运行在同一个Task中。
Step 2. Activity.startActivityForResult
Step 3. Instrumentation.execStartActivity
Step 4. ActivityManagerProxy.startActivity
详见罗老师源码分析:[Android应用程序内部启动Activity过程(startActivity)的源代码分析](http://blog.csdn.net/Luoshengyang/article/details/6703247)
(2) 调用了ActivityThread类的mInstrumentation成员的execStartActivity方法。
注意到,ActivityThread 实际上是主线程,而主线程一个进程只有一个mInstrumentation,只在应用刚刚打开第一个activity的时候创建(单例模式),之后不会发生变化,而且看到不管是Activity中startActivity还是在其他地方,调用的都是mInstrumentation.execStartActivity()
,因此mInstrumentation对象是一个良好的Hook点。
分析完我们的HOOK的点后,接下来就要替换了我们的mInstrumentation对象了,代码如下:
第一步首先通过反射把当前进程的ActivityThread对象拿到手:
// 先获取到当前的ActivityThread对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
第二步,虽然动态代理可以非常方便的进行代理对象,但是我们的mInstrumentation对象不是接口,因此没有办法采用动态代理方式创建代理类,那就没办法只能通过继承来静态代理我们的mInstrumentation,然后覆写我们的mInstrumentation的execStartActivity方法,代码如下:
public class EvilInstrumentation extends Instrumentation {
private static final String TAG = "EvilInstrumentation";
// ActivityThread中原始的对象, 保存起来
Instrumentation mBase;
public EvilInstrumentation(Instrumentation base) {
mBase = base;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
// Hook之前, XXX到此一游!
Log.d(TAG, "\n执行了startActivity, 参数如下: \n" + "who = [" + who + "], " +
"\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
"\ntarget = [" + target + "], \nintent = [" + intent +
"], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");
// 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
// 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
try {
Method execStartActivity = Instrumentation.class.getDeclaredMethod(
"execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
execStartActivity.setAccessible(true);
return (ActivityResult) execStartActivity.invoke(mBase, who,
contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
// 某该死的rom修改了 需要手动适配
throw new RuntimeException("do not support!!! pls adapt it");
}
}
}
上面代码就是静态代理的代码,execStartActivity中先是打印一些信息,然后通过反射拿到Instrumentation的execStartActivity方法,进行调用,之所以要反射是因为这个方法不可见,必须要反射才能调用,这是静态代理所不能实现的功能。
创建了Instrumentation的代理对象,又找到了HOOK点,最后就只需要把需要替换的对象换掉就可以了。
第三步,使用反射进行Instrumentation对象的替换:代码如下:
public static void attachContext() throws Exception{
// 1先获取到当前的ActivityThread对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 2拿到原始的 mInstrumentation字段
Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
//3 创建代理对象,使用反射偷梁换柱
Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);
mInstrumentationField.set(currentActivityThread, evilInstrumentation);
}
最后都完成了,那么就要测试一下了,看看能不能打印出我们代理函数里面的数据了,打印结果如下:
07-11 22:19:20 9207-9207/com.dynamic_proxy_hook.app D/EvilInstrumentation:执行了startActivity,参数如下:
who = [android.app.Application@76726c01],
contextThread = [android.app.ActivityThread$ApplicationThread@4353489dd1],
token = [null],
target = [null],
intent = [Intent { act=android.intent.action.test dat=sadjksadk flg-0x10000000}],
requestCode = [-1],
options = [null]
可以看到打印出来了,结果就是HOOK成功了。