在逆向腾讯某产品时,发现很多方法的前面都有这么一段代码:
if(NotDoVerifyClasses.DO_VERIFY_CLASSES) {
System.out.print(AntiLazyLoad.class);
}
NotDoVerifyClasses和AntiLazyLoad在dex中居然找不到,这勾起我的兴趣。先来debug看看,到底是从哪里加载的这两个类:
ok,发现是从files下的verify.jar加载的,找到这个jar发编译一下:
package com.tencent;
public class AntiLazyLoad {
public AntiLazyLoad() {
super();
}
}
package com.tencent;
public class NotDoVerifyClasses {
public static boolean DO_VERIFY_CLASSES;
static {
NotDoVerifyClasses.DO_VERIFY_CLASSES = false;
}
public NotDoVerifyClasses() {
super();
}
}
代码很简单,感觉像是个桩,但这个有什么用?
从文件路径我们知道有hotfix,应该和热修复有关,深入研究一下。
我们先来看看另外一个问题:在前面的截图中,我们看到classloader是PathClassLoader,其pathList中居然被增加一个files目录下的jar,这个是怎么做到的?一般来说动态加载应该是DexClassLoader才对。
通过搜索verify.jar,发现了关键代码:
public static boolean inject(Context ctx, String dexPath, String arg5, String odexPath, String clazzName, boolean arg8) {
boolean v0 = false;
if(dexPath != null && (new File(dexPath).exists())) {
if(DexUtil.isAliyunOs()) {
try {
DexUtil.injectInAliyunOs(ctx, dexPath, arg5, odexPath, clazzName, arg8);
v0 = true;
}
catch(Throwable v0_1) {
PatchLog.e("SystemClassLoaderInjector", "fail to inject", v0_1);
throw v0_1;
}
}
else if(!DexUtil.hasBaseDexClassLoader()) {
try {
DexUtil.injectBelowApiLevel14(ctx, dexPath, arg5, odexPath, clazzName, arg8);
v0 = true;
}
catch(Throwable v0_1) {
PatchLog.e("SystemClassLoaderInjector", "fail to inject", v0_1);
throw v0_1;
}
}
else {
try {
DexUtil.injectAboveEqualApiLevel14(ctx, dexPath, arg5, odexPath, clazzName, arg8);
v0 = true;
}
catch(Throwable v0_1) {
PatchLog.e("SystemClassLoaderInjector", "fail to inject", v0_1);
throw v0_1;
}
}
}
return v0;
}
不同版本做了不同的Classloader注入方案,我是4.4的手机,对应看看injectAboveEqualApiLevel14
:
private static void injectAboveEqualApiLevel14(Context ctx, String dexPath, String libPath, String odexPath, String arg9, boolean arg10) {
ClassLoader oldClassloader = ctx.getClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, odexPath, libPath, ctx.getClassLoader());
Object v2 = DexUtil.combineArray(DexUtil.getDexElements(DexUtil.getPathList(oldClassloader)),
DexUtil.getDexElements(DexUtil.getPathList(dexClassLoader)), arg10);
Object v0_1 = DexUtil.getPathList(oldClassloader);
DexUtil.setField(v0_1, v0_1.getClass(), "dexElements", v2);
if(!TextUtils.isEmpty(((CharSequence)arg9))) {
dexClassLoader.loadClass(arg9);
PatchLog.e("DexUtil", "load clas " + arg9 + " success ");
}
}
原理弄明白了:先通过DexClassLoader加载files下的jar,然后反射获取其dexElements,然后合并到PathClassLoader的dexElements中,很巧妙的做法,其他注入方案不再详述。
上述这个过程在Application的attachBaseContext中就完成,即app一运行就会加载。也就是说,加入某个时候更新了files下的verify.jar,在下次启动app时,修改后的verify.jar代码就会被加载。 前面最开始看到有很多方法执行前就调用的桩,如果精心去设计NotDoVerifyClasses和AntiLazyLoad的代码,在方法体执行前就能被执行,确实可以做到热修复。
然而,其hotfix目录下不仅仅是上面的代码,上面也没有讲述其是如何从服务器端拉取热修复的代码的, hotfix下代码如下:
代码有点多,我耐着性子看完了,截图中的这一坨代码是为了做补丁管理的,其中就包括从服务端去拉取补丁,然后保存到files下的hotfix目录。假如服务端返回的补丁名字叫做verify,那么就会覆盖verify.jar,我想腾讯他们在推送热修复补丁时,肯定是推送这样一个补丁。
我记得阿里之前开源了一个修改自xposed的热修复框架:dexposed,其利用xposed的hook原理,可以做到hook任何一个java方法,效果也是不错的,但xposed因为很多适配问题,所以要产品化效果不太好。
腾讯的这种热修复方案是通过硬编码方式植入桩,假如植入方式能够工具化,也是一个不错的选择,至少没有适配问题。
关于上面的热修复原理,腾讯有官方介绍,发现自己之前有一些理解不到位的地方,原理参考: https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a&scene=1&srcid=1106Imu9ZgwybID13e7y2nEi#wechat_redirect
搜索了一下热修复,原来是2015年很火的一个技术(孤陋寡闻了),现在已经有很多成熟的方案,上述的hotfix只是其中之一,想了解更多可以自行去搜索。