Hook 的这个本领,使它能够将自身的代码「融入」被勾住(Hook)的程序的进程中,成为目标进程的一个部分。API Hook 技能是一种用于改变 API 实行结果的技能,能够将系统的 API 函数实行重定向。在 Android 系统中利用了沙箱机制,普通用户程序的进程空间都是独立的,程序的运行互不滋扰。这就使我们希望通过一个程序改变其他程序的某些行为的想法不能直接实现,但是 Hook 的涌现给我们开拓理解决此类问题的道路。当然,根据 Hook 工具与 Hook 后处理的事宜办法不同,Hook 还分为不同的种类,比如 Hook、API Hook 等。
利用 Java 反射实现 API Hook通过对 Android 平台的虚拟机注入与 Java 反射的办法,来改变 Android 虚拟机调用函数的办法(ClassLoader),从而达到 Java 函数重定向的目的,在这里我们将此类操作称为 Java API Hook。下面通过 Hook View 的 OnClickListener 来解释 Hook 的利用方法。
首先进入 View 的 setOnClickListener 方法,我们看到 OnClickListener 工具被保存在了一个叫做 ListenerInfo 内部类里,个中 mListenerInfo 是 View 的成员变量。ListeneInfo 里面保存了 View 的各种监听事宜,比如 OnClickListener、OnLongClickListener、OnKeyListener 等等。

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;}
我们的目标是 Hook OnClickListener,以是就要在给 View 设置监听事宜后,更换 OnClickListener 工具,注入自定义的操作。
private void hookOnClickListener(View view) { try { // 得到 View 的 ListenerInfo 工具 Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo"); getListenerInfo.setAccessible(true); Object listenerInfo = getListenerInfo.invoke(view); // 得到 原始的 OnClickListener 工具 Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo"); Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener"); mOnClickListener.setAccessible(true); View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo); // 用自定义的 OnClickListener 更换原始的 OnClickListener View.OnClickListener hookedOnClickListener = new HookedOnClickListener(originOnClickListener); mOnClickListener.set(listenerInfo, hookedOnClickListener); } catch (Exception e) { log.warn("hook clickListener failed!", e); }}class HookedOnClickListener implements View.OnClickListener { private View.OnClickListener origin; HookedOnClickListener(View.OnClickListener origin) { this.origin = origin; } @Override public void onClick(View v) { Toast.makeText(MainActivity.this, "hook click", Toast.LENGTH_SHORT).show(); log.info("Before click, do what you want to to."); if (origin != null) { origin.onClick(v); } log.info("After click, do what you want to to."); }}
到这里,我们成功 Hook 了 OnClickListener,在点击之前和点击之后可以实行某些操作,达到了我们的目的。下面是调用的部分,再给 Button 设置 OnClickListener 后,实行 Hook 操作。点击按钮后,日志的打印结果是:Before click → onClick → After click。
Button btnSend = (Button) findViewById(R.id.btn_send); btnSend.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { log.info("onClick"); } }); hookOnClickListener(btnSend);
我们再来看一个很常见的例子 startActivity
下面我们Hook掉 startActivity 这个方法,使得每次调用这个方法之前输出一条日志;(当然,这个输入日志有点点弱,只是为了展示事理,如果你想可以更换参数,拦截这个 startActivity 过程,使得调用它导致启动某个别的Activity,颠倒黑白!
)
我们知道对付Context.startActivity,Context的实现实际上是ContextImpl;我们看ConetxtImpl类的startActivity方法:
@Overridepublic 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);}
这里,实际上利用了 ActivityThread 类的 mInstrumentation 成员的 execStartActivity 方法;把稳到, ActivityThread 实际上是主线程,而主线程一个进程只有一个,因此这里是一个良好的Hook点。
接下来便是想要Hook掉我们的主线程工具,也便是把这个主线程工具里面的 mInstrumentation 被更换成我们修正过的代理工具;要更换主线程工具里面的字段,首先我们得拿到主线程工具的引用,如何获取呢? ActivityThread 类里面有一个静态方法 currentActivityThread 可以帮助我们拿到这个工具类;但是 ActivityThread 是一个隐蔽类,我们须要用反射去获取,代码如下:
// 先获取到当前的ActivityThread工具Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");currentActivityThreadMethod.setAccessible(true);Object currentActivityThread = currentActivityThreadMethod.invoke(null);
拿到这个 currentActivityThread 之后,我们须要修正它的 mInstrumentation 这个字段为我们的代理工具,我们先实现这个代理工具,由于JDK动态代理只支持接口,而这个 Instrumentation 是一个类,没办法,我们只有手动写静态代理类,覆盖掉原始的方法即可。( cglib 可以做到基于类的动态代理,这里先不先容)
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"); } }}
Ok,有了代理工具,我们要做的便是偷梁换柱!
代码比较大略,采取反射直接修正:
public static void attactContext() throws Exception{ // 先获取到当前的ActivityThread工具 Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); Field currentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread"); currentActivityThreadField.setAccessible(true); Object currentActivityThread = currentActivityThreadField.get(null); // 拿到原始的 mInstrumentation字段 Field mInstrumentationField = activityThreadClass.getField("mInstrumentation"); mInstrumentationField.setAccessible(true); Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread); // 创建代理工具 Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation); // 偷梁换柱 mInstrumentationField.set(currentActivityThread, evilInstrumentation); }
好了,我们启动一个Activity测试一下,结果如下:
总结一下:
Hook 过程:
探求 Hook 点,原则是静态变量或者单例工具,只管即便 Hook public 的工具和方法。
选择得当的代理办法,如果是接口可以用动态代理。
偷梁换柱——用代理工具更换原始工具。
Android 的 API 版本比较多,方法和类可能不一样,以是要做好 API 的兼容事情。
举个例子
Android10后添加了 ActivityTaskManager
int result = ActivityTaskManager.getService().startActivity(whoThread, who.getBasePackageName(), who.getAttributionTag(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
int result = ActivityManagerNative.getDefault() .startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, null, options);
2. Xposed
通过更换 /system/bin/app_process 程序掌握 Zygote 进程,使得 app_process 在启动过程中会加载 XposedBridge.jar 这个 Jar 包,从而完成对 Zygote 进程及其创建的 Dalvik 虚拟机的挟制。
Xposed 在开机的时候完成对所有的 Hook Function 的挟制,在原 Function 实行的前后加上自定义代码。
现在安装Xposed比较方便,由于Xposed作者开拓了一个Xposed Installer App,下载后按照提示傻瓜式安装(条件是root手机)。实在它的安装过程是这个样子的:首先探测手机型号,然后按照手机版本下载不同的刷机包,末了把Xposed刷机包刷入手机重启就好。刷机包下载 里面有所有版本的刷机包。
刷机包解压打开里面的问件构成是这个样子的:
META-INF/ 里面有文件配置脚本 flash-script.sh 配置各个文件安装位置。system/bin/ 更换zygote进程等文件system/framework/XposedBridge.jar jar包位置system/lib system/lib64 一些so文件所在位置xposed.prop xposed版本解释文件
以是安装Xposed的过程就上把上面这些文件放得手机里相同文件路径下。
通过查看文件安装脚本创造:
system/bin/下面的文件更换了app_process等文件,app_process便是zygote进程文件。以是Xposed通过更换zygote进程实现了掌握手机上所有app进程。由于所有app进程都是由Zygote fork出来的。
Xposed的基本事理是修正了ART/Davilk虚拟机,将须要hook的函数注册为Native层函数。当实行到这一函数是虚拟机会优先实行Native层函数,然后再去实行Java层函数,这样完成函数的hook。如下图:
通过读Xposed源码创造其启动过程:
手机启动时init进程会启动zygote这个进程。由于zygote进程文件app_process已被更换,以是启动的时Xposed版的zygote进程。Xposed_zygote进程启动后会初始化一些so文件(system/lib system/lib64),然后进入XposedBridge.jar中的XposedBridge.main中初始化jar包完成对一些关键Android系统函数的hook。Hook则是利用修正过的虚拟机将函数注册为native函数。然后再返回zygote中完本钱来zygote须要做的事情。这只是在宏不雅观层面轻微先容了下Xposed,要想详细理解须要读它的源码了。