Hook 英文直译是“钩子”的意思,在程序中将其理解为“劫持”更好理解,意思是,通过 Hook 技术来劫持某个对象,从而控制它与其它对象的交互。
Hook 技术是一种用于改变 API 执行结果的技术,Android 系统中有一套自己的事件分发机制,所有的代码调用和回调都是按照一定顺序执行的。Hook 技术存在的意义在于,Hook 可以帮助我们在 Android SDK 的源代码逻辑执行过程中,通过代码手动拦截执行该逻辑,加入自己的代码逻辑。
说到 Hook 技术就要先提到逆向工程,逆向工程源于商业及军事领域中的硬件分析,其主要目的是在不能轻易获得必要的生产信息的情况下,直接从成品分析,推导出产品的设计原理。
逆向分析分为静态分析和动态分析,其中静态分析指的是一种在不执行程序的情况下对程序行为进行分析的技术;动态分析是指在程序运行时对程序进行调试的技术。Hook 技术属于动态分析,它不仅在 Android 平台中被应用,早在 Windows 平台中就已经被应用了。
为了保证 Hook 的稳定性,一般拦截的点都会选择比较容易找到并且不易发生变化的对象,比如静态变量和单例。
1 Hook 技术概述
Hook 原理:创建一个代理对象,然后把原始对象替换为我们的代理对象,这样就可以在这个代理对象“为所欲为”,修改参数或替换返回值。
Android 系统的代码调用和回调都是按照一定顺序执行的,举个简单的例子:
对象 A 调用对象 B,对象 B 处理后将数据回调给对象 A。接着来看采用 Hook 的调用流程:
上图中的 Hook 可以是一个方法或者一个对象,它像一个钩子一样挂在对象 A 和对象 B 之间,当对象 A 调用对象 B 之前做一些处理(比如修改方法的参数或者返回值),起到了“欺上瞒下”的作用。因此,与其说 Hook 是钩子,不如说是劫持来的更贴切些。
应用程序进程之间是彼此独立的,应用程序进程和系统进程之间也是如此,想要在应用程序进程更改系统进程的某些行为很难直接实现,有了 Hook 技术,就可以在进程间进行行为更改:
通过上图可以看到 Hook 可以将自己融入到它所要劫持的对象(对象 B)所在的进程中,成为系统进程的一部分,这样就可以通过 Hook 来更改对象 B 的行为。被劫持的对象(对象 B),称为 Hook 点,为了保证 Hook 的稳定性,Hook 点一般选择容易找到并且不易变化的对象,静态变量和单例就符合这一条件。
Hook 的过程:
- 找到 Hook 点,原则是静态变量或者单例对象,尽量 Hook public 的对象或者方法,非 public 不保证每个版本都一样,需要适配;
- 选择合适的代理方法,如果是接口可以用动态代理;如果是类,可以用静态代理;
- 偷梁换柱,用代理对象替换原始对象;
2 Hook 技术分类
Hook 技术根据不同的角度会有很多种分类。
根据 Hook 的 API 语言划分,分为 Hook Java 和 Hook Native:
- Hook Java 主要通过反射和代理来实现,应用于 SDK 开发环境中修改 Java 代码;
- Hook Native 则应用于在 NDK 开发环境和系统开发中修改 Native 代码;
根据 Hook 的进程划分,分为应用程序进程 Hook 和全局 Hook:
- 应用程序进程 Hook 只能 Hook 当前所在的应用程序进程;
- 应用程序进程是 Zygote 进程 fork 出来的,如果对 Zygote 进行 Hook,就可以实现 Hook 系统所有的应用成勋进程,这就是全局 Hook;
根据 Hook 的实现方式划分,分为如下两种:
- 通过反射和代理实现,只能 Hook 当前的应用程序进程;
- 通过 Hook 框架来实现,比如 XPosed,可以实现全局 Hook,但是需要 root;
Hook Native、全局 Hook 和通过 Hook 框架实现这些分类和插件化技术关联不大。
3 反射
反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法和属性。
反射使得在运行状态中时,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有方法和属性),这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
4 代理模式
设计模式(五)—— 代理模式
Java 提供了动态的代理接口 InvocationHandler,实现该接口需要重写 invoke 方法。 以下是动态代理的代码实现:
public class DynamicSinger implements InvocationHandler {
private Object obj;
public DynamicSinger(Object obj) {
this.obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("sing")) {
System.out.println("say hello");
Object result = method.invoke(obj, args);
System.out.println("say thanks");
return result;
}
return null;
}
}
在动态代理中声明一个 Object 的引用,该引用指向被代理类,我们调用被代理类的具体方法在 invoke 方法中执行,接下来修改客户端类代码:
class Client {
public static void main(String[] args) {
ISinger singer = new Singer();
DynamicSinger dynamicSinger = new DynamicSinger(singer);
ISinger iSinger = (ISinger) Proxy.newProxyInstance(
dynamicSinger.getClass().getClassLoader(), new Class[]{ISinger.class},
dynamicSinger);
iSinger.sing();
}
}
调用 Proxy.newProxyInstance() 来生成动态代理类,调用 iSinger.sing() 方法会调用 DynamicSinger.invoke 方法。代理模式从编码的角度可以分为静态代理和动态代理。
5 Hook startActivity 方法
Hook 可以用来劫持对象,被劫持的对象叫做 Hook 点,用代理对象来替代 Hook 点,这样我们聚可以在代理上实现自己想要做的操作。
下面以 Hook 常用的 startActivity 方法来举例,startActivity 方法分为两个:
startActivity(Intent);
getApplicationContext().startActivity(Intent);
第一个是 Activity 的 startActivity 方法,第二个是 Context 的 startActivity 方法,这两个方法的调用链是不同的。
5.1 Hook Activity 的 startActivity 方法
以下是相关源码:
// frameworks/base/core/java/android/app/Activity.java
@Override
public void startActivity(Intent intent) {
this.startActivity(intent, null); // 1
}
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
getAutofillClientController().onStartActivity(intent, mIntent);
if (options != null) {
startActivityForResult(intent, -1, options); // 2
} else {
startActivityForResult(intent, -1); // 3
}
}
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode) {
startActivityForResult(intent, requestCode, null); // 4
}
private Instrumentation mInstrumentation;
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options); // 5
if (ar != null) {
mMainThread.sendActivityResult(
mToken, mEmbeddedID, requestCode, ar.getResultCode(),
ar.getResultData());
}
if (requestCode >= 0) {
mStartedActivity = true;
}
cancelInputsAndStartExitTransition(options);
} else {
if (options != null) {
mParent.startActivityFromChild(this, intent, requestCode, options);
} else {
mParent.startActivityFromChild(this, intent, requestCode);
}
}
}
注释 5 处调用了 mInstrumentation.execStartActivity 方法来启动 Activity,这个 mInstrumentation 是 Activity 的成员变量,这里就选择 Instrumentation 作为 Hook 点,用代理 Instrumentation 来替代原始的 Instrumentation 来完成 Hook。首先写出代理 Instrumentation 类:
public class InstrumentationProxy extends Instrumentation {
private static final String TAG = "InstrumentationProxy";
Instrumentation mInstrumentation;
public InstrumentationProxy(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
}
public ActivityResult execStartActivity(Context who, IBinder contextThread,
IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
System.out.println("【InstrumentationProxy】[execStartActivity] who: " + who);
try {
Method execStartActivity =
Instrumentation.class.getDeclaredMethod("execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
return (ActivityResult) execStartActivity.invoke(mInstrumentation, who,
contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
InstrumentationProxy 继承了 Instrumentation,并包含 Instrumentation 的引用。InstrumentationProxy 实现了 execStartActivity 方法,其内部会通过反射找到并调用 Instrumentation.execStartActivity 方法。接下来用 InstrumentaionProxy 来替换 Instrumentation,代码如下:
public void replaceActivityInstrumentation(Activity activity) {
try {
// 得到 Activity 的 mInstrumentation
Field field = Activity.class.getDeclaredField("mInstrumentation"); // 1
// 取消 Java 的权限控制检查
field.setAccessible(true); // 2
Instrumentation instrumentation = (Instrumentation) field.get(activity); // 3
Instrumentation instrumentationProxy = new InstrumentationProxy(instrumentation); // 4
field.set(activity, instrumentationProxy);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
注释 1 处得到 Activity 的成员变量 mInstrumentation,这个成员变量是私有的,因此在注释 2 处取消 Java 的权限控制检查,这样就可以访问 mInstrumentation 字段。在注释 3 处得到 Activity 中的 Instrumentation 对象,在注释 4 处创建 InstrumentationProxy 并传入注释 3 处得到的 Instrumentation 对象,最后用 InstrumentationProxy 来替换 Instrumentation,这样就实现了代理 Instrumentation 替换 Instrumentation 的目的。最后在 MainActivity.onCreate 方法中使用 replaceActivityInstrumentation 方法,如下所示:
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
replaceActivityInstrumentation(this);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.baidu.com"));
startActivity(intent);
}
}
// System.out: 【InstrumentationProxy】[execStartActivity] who: com.example.myapplication.MainActivity@8df42b9
5.2 Hook Context 的 startActivity 方法
Context 的实现类为 ContextImpl,ContextImpl.startActivity 方法如下:
// frameworks/base/core/java/android/app/ContextImpl.java
@Override
public void startActivity(Intent intent) {
warnIfCallingFromSystemProcess();
startActivity(intent, null); // 1
}
@Override
public void startActivity(Intent intent, Bundle options) {
warnIfCallingFromSystemProcess();
final int targetSdkVersion = getApplicationInfo().targetSdkVersion;
if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
&& (targetSdkVersion < Build.VERSION_CODES.N
|| targetSdkVersion >= Build.VERSION_CODES.P)
&& (options == null
|| ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
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); // 2
}
注释 2 处调用了 ActivityThread.getInstrumentation() 方法获取 Instrumentation。ActivityThread 是主线程的管理类,Instrumentation 是 ActivityThread 的成员变量,一个进程中只有一个 ActivityThread,因此仍然选择 Instrumentation 作为 Hook 点,用代理 Instrumentation 来替换 Instrumentation,代理 Instrumentation 和 5.1 节的代码是一样的,接下来用 InstrumentationProxy 来替换 Instrumentation,代码如下所示:
public void replaceContextInstrumentation() {
try {
// 获取 ActivityThread 类
Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");
Field activityThreadField =
activityThreadClazz.getDeclaredField("sCurrentActivityThread"); // 1
activityThreadField.setAccessible(true);
Object currentActivityThread = activityThreadField.get(null); // 2
// 获取 ActivityThread 中的 mInstrumentation 字段
Field mInstrumentationField =
activityThreadClazz.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation =
(Instrumentation) mInstrumentationField.get(currentActivityThread);
Instrumentation mInstrumentationProxy = new InstrumentationProxy(mInstrumentation); // 3
mInstrumentationField.set(currentActivityThread, mInstrumentationProxy);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
首先通过反射获取 ActivityThread 类,ActivityThread 类中有一个静态变量 sCurrentActivityThread,用于表示当前的 ActivityThread 对象,因此在注释 1 处获取 ActivityThread 中定义的 sCurrentActivityThread 字段,在注释 2 处获取 Field 类型的 activityThreadField 对象的值,这个值就是 sCurrentActivityThread 对象。同理获取当前 ActivityThread 的 mInstrumentation 对象,最后用 InstrumentationProxy 来替换 mInstrumentation。在 MainActivity 中使用 replaceContextInstrumentation 方法,如下所示:
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
replaceContextInstrumentation();
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.baidu.com"));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getApplicationContext().startActivity(intent);
}
}
// System.out: 【InstrumentationProxy】[execStartActivity] who: android.app.Application@5147617
Hook Activity 的 startActivity 方法和 Hook Context 的 startActivity 方法最大的区别就是替换 Instrumentation 不同,前者是 Activity 中的 Instrumentation,后者是 ActivityThread 中的 Instrumentation。另外有一点需要注意的是,这里替换 Instrumentation 都是在 Activity.onCreate 方法中进行替换的,但是,这里未必是最佳的替换时间点。可以在 Activity.attachBaseContext 中进行 Instrumentation 替换,因为这个方法要优先于 Activity.onCreate 方法被调用。