Hook 直译为中文就是“钩子”。在 Android 平台上,Hook 的目标事件的粒度一般为函数级别,即在指定函数被调用时,根据我们的逻辑拦截修改原有的函数。
通过 Hook 实现 AOP 编程、插桩性能监控、热补丁、在线修复等
hook技术:
1.反射:虚拟机提供的能力,稳定高但范围小(java部分)
2.ClassLoader:修改类的加载顺序,需要准备好dex(java全部)
3.Xposed:适配性差,每个版本需要改动(java全部)
当然还有其他技术。。。
反射(android9.0限制@hide被反射)
Android P 9.0 引入了针对非 SDK 接口(俗称为隐藏API @hide)的使用限制。这是继 Android N上针对 NDK 中私有库的链接限制之后的又一次重大调整。从今以后,不论是native层的NDK还是 Java层的SDK,我们只能使用Google提供的、公开的标准接口。
如果不是系统类,且API处于黑名单,禁止调用
android 9.0 10.0 可以使用【元反射】方法:
通过反射去获取Class.class类上getDeclaredMethod方法,获取到的Method可以称为“元反射方法”,通过“元反射方法” 去调用隐藏API,就意味着调用者是java.lang.Class,这个类属于系统类,可以正常调用
比如:
1 2 3 4 5 6 7 8 9 10
| public static Field getDeclaredField(Class<?> cls, String name) { try { Method dMethod = Class.class.getDeclaredMethod("getDeclaredField", String.class);
return (Field) dMethod.invoke(cls, name); } catch (Exception e) { e.printStackTrace(); } return null; }
|
https://github.com/tiann/FreeReflection
android 11.0 上【元反射】开始失效,因为增加了 调用者上下文判断 机制。机制给 调用者 跟 被调用的方法 增加了domian,调用者domain要等于或者小于 被调用的方法domain,才能允许访问。
1 2 3 4 5
| enum class Domain : char { kCorePlatform = 0, kPlatform, kApplication, };
|
https://bbs.kanxue.com/thread-268936.htm
https://github.com/whulzz1993/RePublic
Hook 选择的关键点
Hook 的选择点:
尽量静态变量和单例,因为一旦创建对象,它们不容易变化,非常容易定位
Hook 过程:
寻找 Hook 点,原则是尽量静态变量或者单例对象,尽量 Hook public 的对象和方法。
选择合适的代理方式,如果是接口可以用动态代理。
偷梁换柱——用代理对象替换原始对象。
Android 的 API 版本比较多,方法和类可能不一样,所以要做好 API 的兼容工作。
Hook 必须掌握的知识
反射
需要了解反射操作
java 的动态代理(动态代理机制InvocationHandler)
动态代理是指在运行时动态生成代理类,不需要我们像静态代理那个去手动写一个个的代理类。在 java 中,我们可以使用 InvocationHandler 实现动态代理,有兴趣的,可以查看我的这一篇博客 java 代理模式详解
Hook举例
Hook Click(不过这种方式没有做到普遍性,需要自己手动取每个地方设置)
1 2 3 4 5 6
| public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; }
|
反射getListenerInfo方法,在反射获取mOnClickListener变量
//先设置ClickListener,再hook取代
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| TextView tx = findViewById(R.id.tx); tx.setOnClickListener(…); HookClickManager.hook(tx);
public static void hook(View v) { try { Method method = View.class.getDeclaredMethod("getListenerInfo"); method.setAccessible(true); Object listnerInfo = method.invoke(v);
Class c = Class.forName("android.view.View$ListenerInfo"); Field field = c.getDeclaredField("mOnClickListener"); View.OnClickListener clickListener = (View.OnClickListener) field.get(listnerInfo);
ProxyOnClickListener proxy = new ProxyOnClickListener(clickListener); field.set(listnerInfo, proxy);
} catch (Exception e) { e.printStackTrace(); } }
public class ProxyOnClickListener implements View.OnClickListener { View.OnClickListener listener; public ProxyOnClickListener(View.OnClickListener l) { this.listener = l; }
@Override public void onClick(View v) { Log.d("ProxyOnClickListener", "this is hook"); if (listener != null) { listener.onClick(v); } } }
|
HooK Activity
hook mInstrumentation即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| HookActivityManager.hook(this); startActivity(new Intent(this, xxxActivity.class))
public class HookActivityManager { public static void hook(Activity activity) { try { Field mInstrumentation = Activity.class.getDeclaredField("mInstrumentation"); mInstrumentation.setAccessible(true); Instrumentation instrumentation = (Instrumentation) mInstrumentation.get(activity); ProxyInstrumentation proxy = new ProxyInstrumentation(instrumentation); mInstrumentation.set(activity, proxy); } catch (Exception e) { e.printStackTrace(); } } }
public class ProxyInstrumentation extends Instrumentation { private Instrumentation instrumentation; public ProxyInstrumentation(Instrumentation i) { this.instrumentation = i; } @SuppressLint("DiscouragedPrivateApi") public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { Log.d("ProxyInstrumentation", "Proxy Instrumentation success !"); try { Class<?> c = Class.forName("android.app.Instrumentation"); Method execStartActivity = c.getDeclaredMethod( "execStartActivity", Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class); return (ActivityResult) execStartActivity.invoke(instrumentation, who, contextThread, token, target, intent, requestCode, options); } catch (Exception e) { e.printStackTrace(); } return null; } }
|
Lancet本质上是 Gradle Plugin,通过依赖 Android 的打包插件提供的 Transform API,在打包过程中获取到所有的代码。依赖 ASM 提供的字节码注入能力,通过我们解析自定义的注解,在目标点注入相应的代码。通过注解进行 AOP 这点和 AspectJ 很相似,但是更加轻量和简洁,使用方式也有所不同。
这里要区分一个概念,编译期注入和运行期注入。
编译期:即在编译时对字节码做插桩修改,达到 AOP 的目的。优点是运行时无额外的性能损耗,但因为编译时的限制,只能修改最终打包到APK中的代码,即 Android Framework 的代码是固化在 ROM 中的,无法修改。运行期:是只在运行时动态的修改代码的执行,因而可以修改 Framework 中代码的执行流程,在hook点上执行性能上有所损耗。
Lancet,编译期注入
AspectJ,既支持编译期也支持运行期的注入,运行期的注入一般是依赖JVM提供的AttachAPI,因为Android 没有 JVM 的环境,实际上 class 还会继续转换成 dex,因此 AspectJ 在 Android 平台是只能做到编译期注入
Dexposed,运行期注入
参考文章:
https://juejin.cn/post/7023717952180617247