概述
利用DexClassLoader类加载原理,apk包含多个dex文件,会从dex中依次查找类,如果找到了就不继续往后面找了。我们把补丁包.dex放到最前面,就优先从补丁包中查找类。
原理分析
Dex分包
dex是java文件编译的二进制产物,可以理解成Android优化后的.class合并文件。原先所有java文件都会被打包单个dex,但由于dex的65536问题,会分包成多个dex。
DexClassLoader机制
Android提供了从Dex中加载类的DexClassLoader。
我们把修复后的com.a.fix.M生成patch.dex,
想办法把path.dex插入到dexElements最前面,
当loader要查找com.a.fix.M时,会从前往后遍历dexElements数组,查到了就终止遍历。
DexClassLoader源码
查看DexClassLoader源码,以7.0的源码为例,选取部分代码
//DexClassLoader的基类,代码有省略
public class BaseDexClassLoader extends ClassLoader {
//具体加载的事宜都交给了DexPathList,私有属性,可以反射调用
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, ...) {
this.pathList = new DexPathList(this, dexPath, ...);
}
@Override
protected Class<?> findClass(String name){
return pathList.findClass(name, suppressedExceptions);
}
/**
* @hide,隐藏方法,可以反射调用。可以用来插入patch.dex
*/
public void addDexPath(String dexPath) {
pathList.addDexPath(dexPath, null /*optimizedDirectory*/);
}
//只能反射使用这个类
/*package*/ final class DexPathList {
//这个dexElements,数组很关键实际上Element 可能是dex文件,包含dex的apk文件,包含dex的jar文件
private Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,...) {
this.dexElements = makeDexElements(splitDexPath(dexPath), ...);
}
//只能反射调用,可以用来插入patch.dex。
public void addDexPath(String dexPath, File optimizedDirectory) {
final Element[] newElements = makeDexElements(splitDexPath(dexPath),...);
final Element[] oldElements = dexElements;
dexElements = new Element[oldElements.length + newElements.length];
System.arraycopy(oldElements, 0, dexElements, 0, oldElements.length);
//新增的Elements只能加入到数组后面。我们需要把patch加到最前面
System.arraycopy(newElements, 0, dexElements, oldElements.length, newElements.length);
}
//上图的总结来自这里,遍历dexElements数组。
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
//最终调用在platform/art/rumtime/native/dalvik_system_DexFile.cc中
Class clazz = element.dexFile.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) return clazz;
}
return null;
}
/**
* Element of the dex/resource/native library path
*/
/*package*/ static class Element {
private final File dir;
private final boolean isDirectory;
private final File zip;
private final DexFile dexFile;
}
}
前置插入Patch.dex
我们需要把patch.dex插入到dexElements最前面。
由于没有对外暴露方法,需要反射执行。这就很简单了,方案就有很多,比如
- a.DexPathList.makeDexElements, 生成path element数组,合并新老数组
- b.DexPathList.addDexPath,然后把新插入patch的element调整到数组最前面
a方案,也是网上比较多的方案
b方案,理解这个东西,就可以整出来了。
过程实现
先来整一段用来显示的代码。完整版代码在 热修复之冷启动 module:hotfix_dexload
待修复功能
//待修复类
package com.a.fix;
public class M {
public static String a(){return "M aaa";}
}
//用来展示数据的类
package com.a.android_sample;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String str = M.a();
((TextView) findViewById(R.id.tv)).setText(str));
}
}
生成patch.dex
类修复代码
package com.a.fix;
public class M {
public static String a(){return "M aaa fix";}
}
java -> dex
这样,简单的dex文件就生成了。生成patch.dex后,我们把代码恢复成修复前的。
//来到java源码目录下
cd app/main/java
//.class文件
javac com/a/fix/M.java
//生成patch.dex
dx --dex --output com/a/fix/patch.dex com/a/fix/M.class
好奇点,我们可以名看看dex里面的是啥,可以使用smali相关技术 AndroidStudio安装插件java2smali或者smali.jar包执行
存放patch.dex
patch.dex复制到assets文件夹中
patch.dex 该放哪应用启动后能读取就行。放到sd卡当然可以。 我们选择assets文件夹中,程序启动时copy到我们想要的地方,就当模拟下载了。
插入patch.dex
从这里开始,都在自定义ApplicationApp的attachBaseContext()重载中完成。
为什么这个方法中合适, ApplicationApp是应用创建时,apk中最先实例化的类,实际上attachBaseContext在onCreate()之前调用。 这又是一个话题了,可以看看 CSDN上老罗的应用启动过程,有提到Applicaiton的创建。 参考源码LoadedApk.makeApplication()
public class ApplicationApp extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//1.从assets中复制出来
String dexFilePath = copyAssetsDex("patch.dex");
//2.装载补丁包,即插入到dexElements
installDex(this, dexFilePath);
}
}
//复制assets中的patch.dex到手机的存储系统,
private String copyAssetsDex(String dexFileName) {
//这个ExternalCacheDir应用沙盒存储,读写自由遍历
String hackPath = getExternalCacheDir().getAbsolutePath() + "/" + dexFileName;
File destFile = new File(hackPath);
if (destFile.exists()) destFile.delete();
InputStream is = getAssets().open(dexFileName);
FileOutputStream fos = new FileOutputStream(destFile);
byte[] buffer = new byte[1024];
int byteCount;
while ((byteCount = is.read(buffer)) != -1) {
fos.write(buffer, 0, byteCount);
}
...
return destFile.getAbsolutePath();
}
//插入patch.dex,因为是反射调用,不同系统版本的源码可能是不一致的,要做区分。省略部分代码
//这里,我找了一些不同版本,可以丰富起来。
private void installDex(Context context, String filePath) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
installDexh4_3_And_Below(context, filePath);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
installDexh4_4_TO_5_1(context, filePath);
} else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
installDexAbove_6_0_And_Above(context, filePath);
}
}
}
/**
* 执行插入,羞愧,都是别人的代码。也就是我们的a方案,下面还有简单的b方案。
*/
public static void installDexAbove_6_0_And_Above(Context context, String patch) {
//优化目录必须是私有目录,可自由访问。
File cacheDir = context.getCacheDir();
//PathClassLoader
ClassLoader classLoader = context.getClassLoader();
try {
//先获取pathList属性
Field pathList = getField(classLoader, "pathList");
//通过属性反射获取属性的对象 DexPathList
Object pathListObject = pathList.get(classLoader);
//通过 pathListObject 对象获取 pathList类中的dexElements 属性
//原本的dex element数组
Field dexElementsField = getField(pathListObject, "dexElements");
//通过dexElementsField 属性获取它存在的对象
Object[] dexElementsObject = (Object[]) dexElementsField.get(pathListObject);
List<File> files = new ArrayList<>();
File file = new File(patch);//补丁包
if (file.exists()) {
files.add(file);
}
//插桩所用到的类
// files.add(antiazyFile);
Method method = getMethod(pathListObject, "makeDexElements", List.class, File.class, List.class, ClassLoader.class);
final List<IOException> suppressedExceptionList = new ArrayList<IOException>();
//补丁的element数组
Object[] patchElement = (Object[]) method.invoke(null, files, cacheDir, suppressedExceptionList, classLoader);
//用于替换系统原本的element数组
Object[] newElement = (Object[]) Array.newInstance(dexElementsObject.getClass().getComponentType(),
dexElementsObject.length + patchElement.length);
//合并复制element
System.arraycopy(patchElement, 0, newElement, 0, patchElement.length);
System.arraycopy(dexElementsObject, 0, newElement, patchElement.length, dexElementsObject.length);
// 替换
dexElementsField.set(pathListObject, newElement);
} catch (Exception e) {
e.printStackTrace();
}
}
//b方案就简单点,也好理解点。
public static void installDexAbove_6_0_And_Above(Context context, String patch) {
try {
ClassLoader classLoader = context.getClassLoader();
Object pathListObject = getField(classLoader, "pathList").get(classLoader);
//1.先记录插入patch前 dexElements的长度
Field dexElementsField = getField(pathListObject, "dexElements");
int oldLength = ((Object[]) dexElementsField.get(pathListObject)).length;
//2.插入patch.dex
Method method = getMethod(classLoader, "addDexPath", String.class);
method.invoke(classLoader, patch);
//3.读取插入patch后 dexElements的长度
Object[] newDexElements = (Object[]) dexElementsField.get(pathListObject);
int newLength = newDexElements.length;
//4.前后交换生成新的dexElements,
Object[] resultElements = (Object[]) Array.newInstance(newDexElements.getClass().getComponentType(),
newLength);
System.arraycopy(newDexElements, 0, resultElements, newLength - oldLength, oldLength);
System.arraycopy(newDexElements, oldLength, resultElements, 0, newLength - oldLength);
//5.重新反射替换dexElements
dexElementsField.set(pathListObject, resultElements);
} catch (Exception e) {
e.printStackTrace();
}
}
}
验证
到此为止,我们的修复功能就实现了。我试了Android7都ok。
pre-verified问题
现象
上面的代码,我们试试跑在Android4.4及以下,结果报错了。
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
at com.a.android_sample.MainActivity.onCreate(MainActivity.java:16)
at android.app.Activity.perfromCreate(Activity.java:5266)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1313)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3733)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3939)
出错代码
String str = M.a();
原因分析
简单来说
1.假如类A及其引用类都在同一个dex中,则类A会被提前验证和优化,并被标记CLASS_ISPREVERIFIED
这里,MainActivity就会被标记上。
2.当我们调用M.a()时,需要加载类M,此时虚拟机会去校验M和MainActivity是否属于同一个dex。很明显不在,这就报错了。
具体代码抛错处
Android4.4 dalvik/vm/oo/Resolve.cpp cs.android.com/android/pla…
//省略了部分代码
ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx,
bool fromUnverifiedConstant){
DvmDex* pDvmDex = referrer->pDvmDex;
ClassObject* resClass;
const char* className;
//不用重复解析
resClass = dvmDexGetResolvedClass(pDvmDex, classIdx);
if (resClass != NULL) return resClass;
....
//这里的resClass是 com.a.fix.M,
//referrer是com.a.
resClass = dvmFindClassNoInit(className, referrer->classLoader);
//....
if (resClass != NULL) {
/*
* If the referrer was pre-verified, the resolved class must come
* from the same DEX or from a bootstrap class.
*/
if (!fromUnverifiedConstant &&
IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED)) {
ClassObject* resClassCheck = resClass;
if (referrer->pDvmDex != resClassCheck->pDvmDex &&
resClassCheck->classLoader != NULL){
dvmThrowIllegalAccessError(
"Class ref in pre-verified class resolved to unexpected "
"implementation");
return NULL;
}
}
//存一下,
dvmDexSetResolvedClass(pDvmDex, classIdx, resClass);
}
.....
return resClass;
}
调用链路
M.a()
AndroidStudio安装插件java2smali,看看MainActivity编译后的产物。
MainActivity.smali 部分代码
.class public Lcom/a/android_sample/MainActivity;
.source "MainActivity.java"
.method protected onCreate(Landroid/os/Bundle;)V
.registers 4
#执行到这一行出错了。
.line 16
invoke-static {}, Lcom/a/fix/M;->a()Ljava/lang/String;
.line 17
...
invoke-virtual {v1, v0}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V
...
.end method
invoke-static
代码在Android4.4源码 dalvik/vm/mterp/out/InterpC-portable.cpp
GOTO_TARGET(invokeStatic, bool methodCallRange)
methodToCall = dvmDexGetResolvedMethod(methodClassDex, ref);
if (methodToCall == NULL) {
//还没解析过,就去解析它
methodToCall = dvmResolveMethod(curMethod->clazz, ref, METHOD_STATIC);
}
GOTO_invokeMethod(methodCallRange, methodToCall, vsrc1, vdst);
GOTO_TARGET_END
dvmResolveMethod
Android4.4源码 dalvik/vm/oo/Resolve.cpp
解析Method前,先解析其所在的class
/*
* Find the method corresponding to "methodRef".
* If this is a static method, we ensure that the method's class is
* initialized.
*/
//省略了部分代码
Method* dvmResolveMethod(const ClassObject* referrer, u4 methodIdx,
MethodType methodType){
ClassObject* resClass;
const DexMethodId* pMethodId;
pMethodId = dexGetMethodId(pDvmDex->pDexFile, methodIdx);
//这里就开始调用到我们上一节提到的具体代码抛错处了。
resClass = dvmResolveClass(referrer, pMethodId->classIdx, false);
if (resClass == NULL) {
/* can't find the class that the method is a part of */
assert(dvmCheckException(dvmThreadSelf()));
return NULL;
}
....
}
dex文件验证优化
回头在来看dex文件优化,我们就放上调用
//libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
BaseDexClassLoader(dexPath,optimizedDirectory,libraryPath,parent)
//libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
DexPathList.loadDexFile(file, optimizedDirectory);
//libcore/dalvik/src/main/java/dalvik/system/DexFile.java
DexFile.loadDex(file.getPath(), optimizedPath, 0);
//dalvik/vm/native/dalvik_system_DexFile.cpp
Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args, JValue* pResult)
//dalvik/vm/RawDexFile.cpp
dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false)
//dalvik/vm/analysis/DexPrepare.cpp
dvmOptimizeDexFile(optFd, dexOffset, fileSize,fileName,....)
//创建进程 /system/bing/dexopt
//dalvik/dexopt/OptMain.cpp
int main(int argc, char* const argv[])
fromDex(int argc, char* const argv[])
dvmContinueOptimization(fd, offset, length...)
//dalvik/vm/analysis/DexPrepare.cpp
rewriteDex(addr, int len,doVerify,doOpt,..)
verifyAndOptimizeClasses(pDvmDex->pDexFile, doVerify, doOpt)
verifyAndOptimizeClass(pDexFile, clazz, pClassDef, doVerify, doOpt)
dvmVerifyClass(clazz)//Set the "is preverified" flag in the DexClassDef
dvmVerifyClass
//dalvik/vm/analysis/DexPrepare.cpp
if (dvmVerifyClass(clazz)) {
/* Set the "is preverified" flag in the DexClassDef. */
((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISPREVERIFIED;
verified = true;
}
//dalvik/vm/analysis/DexVerify.cpp
bool dvmVerifyClass(ClassObject* clazz)
bool verifyMethod(method)
bool dvmVerifyCodeFlow(VerifierData* vdata)
//dalvik/vm/analysis/CodeVerify.cpp
bool doCodeVerification()
...
参考
深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明.pdf
深入理解Dalvik虚拟机
系统源码(AOSP) github地址链接,下载你想要的。或者这个官网链接
安卓App热补丁动态修复技术介绍
android热修复的pre-verify问题详解及实践
05-DALVIK加载和解析DEX过程
pre-verified解决
方案分析
我们在把代码抄过来,发现有三个条件同时满足才会报错
//省略了部分代码
ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx,
bool fromUnverifiedConstant){
resClass = dvmDexGetResolvedClass(pDvmDex, classIdx);
if (resClass != NULL) return resClass;
if (!fromUnverifiedConstant &&
IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED)) {
ClassObject* resClassCheck = resClass;
if (referrer->pDvmDex != resClassCheck->pDvmDex &&
resClassCheck->classLoader != NULL){
dvmThrowIllegalAccessError(
"Class ref in pre-verified class resolved to unexpected "
"implementation");
return NULL;
}
}
}
return resClass;
}
根据上述代码,解决方案大致上有以下四种。
- 禁止dexopt过程打上CLASS_ISPREVERIFIED标记
Q-zone插桩方案突破了此限制,但是导致preverify失效,损失了性能。
- 修改fromUnverfiedConstant=true
需要通过 native hook 拦截系统方法,更改方法的入口参数,将 fromUnverifiedConstant 统一改为 true, 风险大,几乎无人采用。Cydia native hook
- 使dvmDexGetResolvedClass返回不为null,直接返回
QFix采用此方案,
- 补丁类与引用类放在同一个dex中
Tinker等全量合成方案突破了此限制。
Q-zone插桩方案
方案分析
通过字节码技术,在每个类的构造方法中插入一段引用 HackCode.class的代码,使得MainActivity引用到hack.dex中的Hack.class,导致verify不通过。 此时方案分成两步:
- 首先,单独打包HackCode.class
- 然后,MainActivity引用HackCode.class。
package com.a.hack;
public class HackCode {}
实际代码执行处。
//dalvik/vm/analysis/CodeVerify.cpp
case OP_CONST_CLASS:
//给它整失败了,会把错误值给failure,后面判断下失败,就返回失败了,就不标记了。
resClass = dvmOptResolveClass(meth->clazz, decInsn.vB, &failure);
////dalvik/vm/analysis/Optimize.cpp
/*
* Performs access checks on every resolve,
* and refuses to acknowledge the existence of classes
* defined in more than one DEX file.
* 不承认定义在多个dex中的类
*/
ClassObject* dvmOptResolveClass(ClassObject* referrer, u4 classIdx,
VerifyError* pFailure){
...
const char* className = dexStringByTypeIdx(pDvmDex->pDexFile, classIdx);
//referrer是所有引用类,包括:MainAcitivityClass,resClass的Hack.class
//referrer的dex中当然没有Hack.class
resClass = dvmFindClassNoInit(className, referrer->classLoader);
if (resClass == NULL) {
*pFailure = VERIFY_ERROR_NO_CLASS;
...
}
...
}
引用hackCode.class
apk源码不能包含HackCode.class,我们通过字节码插入引用。
编写自定义Gradle插件,使用javassist字节码技术
自定义Gradle插件参考 Gradle系列一 -- Groovy、Gradle和自定义Gradle插件
javassist参考 javassist使用全解析
关键代码,有点长
class HackTransform extends Transform {
def pool = ClassPool.default
def project
....
@Override
void transform(TransformInvocation transformInvocation) throws javax.xml.crypto.dsig.TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
project.android.bootClasspath.each {
pool.appendClassPath(it.absolutePath)
}
//这一行要注意,否则编译不通过哦
pool.makeClass("com.a.hack.HackCode")
transformInvocation.inputs.each {
it.jarInputs.each {
pool.insertClassPath(it.file.absolutePath)
// 重命名输出文件(同目录copyFile会冲突)
def jarName = it.name
def md5Name = DigestUtils.md5Hex(it.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
def dest = transformInvocation.outputProvider.getContentLocation(
jarName + md5Name, it.contentTypes, it.scopes, Format.JAR)
org.apache.commons.io.FileUtils.copyFile(it.file, dest)
}
it.directoryInputs.each {
def inputDir = it.file.absolutePath
pool.insertClassPath(inputDir)
findTarget(it.file, inputDir)
def dest = transformInvocation.outputProvider.getContentLocation(
it.name, it.contentTypes, it.scopes, Format.DIRECTORY)
org.apache.commons.io.FileUtils.copyDirectory(it.file, dest)
}
}
}
private void findTarget(File fileOrDir, String inputDir) {
if (fileOrDir.isDirectory()) {
fileOrDir.listFiles().each {
findTarget(it, inputDir)
}
} else {
modify(fileOrDir, inputDir)
}
}
private void modify(File file, String fileName) {
def filePath = file.absolutePath
if (!filePath.endsWith(SdkConstants.DOT_CLASS)
||filePath.contains('R$')
|| filePath.contains('R.class')
|| filePath.contains("BuildConfig.class")) {
return
}
def className = filePath.replace(fileName, "")
.replace("\\", ".").replace("/", ".")
def name = className.replace(SdkConstants.DOT_CLASS, "").substring(1)
CtClass ctClass = pool.get(name)
//我们的自定义的Application是初始类,加载完dex以后的类,才能插入Hakcode引用。
if (ctClass.getSuperclass() != null
&& ctClass.getSuperclass().name == "android.app.Application") {
return
}
//真正执行插入字节码的地方
ctClass.defrost()
CtConstructor[] constructors = ctClass.getDeclaredConstructors()
if (constructors != null && constructors.length > 0) {
CtConstructor constructor = constructors[0]
def body = "android.util.Log.e(\"alvin\",\"${constructor.name} constructor\" + com.a.hack.HackCode.class);"
constructor.insertBefore(body)
}
ctClass.writeFile(fileName)
ctClass.detach()
}
}
生成hack.dex
参考patch.dex的生成方式。 编写app/main/java/com/a/hack/HackCode.java,单独编译成dex,生成后,可以删掉此java文件。
package com.a.hack;
public class HackCode {}
//来到java源码目录下,
cd app/main/java
//.class文件
javac com/a/hack/HackCode.java
//生成hack.dex
dx --dex --output com/a/hack/hack.dex com/a/hack/HackCode.class
加载hack.dex
参考patch.dex的方式。
验证
android4.4上验证成功
Cydia NativeHook
需要通过 native hook 拦截系统方法,更改方法的入口参数,将 fromUnverifiedConstant 统一改为 true,
这里我们采用Cydia Substrate,hook dvmResolveClass方法,步骤如下 Demo代码:hook具体实现与动态库下载,注意方案只在Android4.4上验证可行。
实现步骤
cydia so库和头文件
这里可以下载。 so库放到一个自己的目录底下 比如
<moduleName>/src/main/jniLibs/armeabi-v7a/libsubstrate.so
<moduleName>/src/main/jniLibs/armeabi-v7a/libsubstrate-dvm.so
导入头文件
<moduleName>/src/main/cpp/include/substrate.h
hook代码实现
//<moduleName>/src/main/cpp/cydia-hook.cpp
#include "include/substrate.h"
#include <android/log.h>
#define TAG "alvin"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
//旧函数指针,指向旧函数
void *(*oldDvmResolveClass)(void *referrer, unsigned int classIdx, bool fromUnverifiedConstant);
//新函数实现
void *newDvmResolveClass(void *referrer, unsigned int classIdx, bool fromUnverifiedConstant) {
//这里,fromUnverifiedConstant 强制为true,就不会去check dex是否相等了。
return oldDvmResolveClass(referrer, classIdx, true);
}
//指明要hook的lib,涉及到dvmResolveClass的so
MSConfig(MSFilterLibrary, "/system/lib/libdvm.so")
//指明要hook的应用
MSConfig(MSFilterExecutable, "com.a.dexload.cydia")
MSInitialize {
MSImageRef image = MSGetImageByName("/system/lib/libdvm.so");
if (image == NULL) {
return;
}
void *resloveMethd = MSFindSymbol(image, "dvmResolveClass");
if (resloveMethd == NULL) {
return;
}
//具体的Hook实现
MSHookFunction(resloveMethd, (void *) newDvmResolveClass, (void **) &oldDvmResolveClass);
}
CMakeLists.txt
生成libcydiahook.so
cmake_minimum_required(VERSION 3.10.2)
add_library(cydiahook SHARED src/main/cpp/cydia-hook.cpp)
target_include_directories(cydiahook PRIVATE ${CMAKE_SOURCE_DIR}/src/main/cpp/include)
find_library(log-lib log)
file(GLOB libs ${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi-v7a/libsubstrate.so ${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi-v7a/libsubstrate-dvm.so)
target_link_libraries( cydiahook ${libs} ${log-lib})
libcydiahook.so加载
public class ApplicationApp extends Application {
static {
System.loadLibrary("cydiahook");
}
}
其他
ClassObject属性
如同Andfix,我们可以引入DexFile.h头文件,可以把参数和结果转成实际的class对象,查看class的一些属性
//新函数实现
void *newDvmResolveClass(void *referrer, unsigned int classIdx, bool fromUnverifiedConstant) {
void *res = oldDvmResolveClass(referrer, classIdx, true);
ClassObject *referrerClass = reinterpret_cast<ClassObject *>(referrer);
ClassObject *resClass = reinterpret_cast<ClassObject *>(res);
if (resClass == NULL) {
LOGE("newDvmResolveClass %s, %s", referrerClass->descriptor,
"resClass is NULL");
} else {
LOGE("newDvmResolveClass %s, %s", referrerClass->descriptor,
resClass->descriptor);
}
return res;
}
风险
和 Andfix 类似,native hook 方式存在各种兼容性和稳定性问题,甚至安全性问题。同时,拦截的是一个涉及 dalvik 基础功能同时调用很频繁的方法,无疑风险会大很多。
QFix方案实现
原理可参考这篇文章QFix探索之路—手Q热补丁轻量级方案
方案
回到这张图,从dvmResolveClass方法入手,提前解析patch类。
一开始想到的方案是使用"const-class" 或者 "instance-of"指令创建类,
来提前调用dvmResloveClass()
方法,此时参数fromUnverifiedConstant 为 true,绕过dex检测。
dvmResloveClass()
最后会将此class与referrer所在的Dex建立缓存映射。
实际也成功了。
public class ApplicationApp extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
DexInstaller.installDex(base, this.getExternalCacheDir().getAbsolutePath() + "/patch.dex");
//会执行 const-class 指令
Log.d("alvin", "bug class:" + com.a.fix.M.class);
}
但有个问题:怎么能提前知道哪些dex需要打补丁类? QFix放弃了此直接load patch class的方案。经过分析,
- 补丁包中的class数量是有限的。
- apk中dex文件的数量也是有限的。
得到如下方案:
- 构建apk时,每个dex预先埋入空白类HackXX,存储映射每个dexXX--空白类HackXX。
- 构建补丁包,存储映射空白类HackXX--补丁类--补丁类在原dex中的classIdx。方便后续通过classIdx反查className
- -----------运行app,加载补丁包-------------
- 使用java方法,调用classLoader.loadClass(空白类name)
- 使用jni方法,调用 dvmFindLoadedClass(空白类descriptor)
- 使用jni方法,调用dvmResolveClass(referrer:空白类,classIdx,fromUnverifiedConstant:true)
这样我们就在空白类HackXX所在dex与补丁class建立了关联,后续都会通过dvmDexGetResloved(pDvmDex,classIdx)直接获取
实操
全部实现代码都在github中
空白类注入到Dex
自定义gradle插件,使用smali操作dexfile,注入class。
1.buildSrc/build.gradle加入依赖
//buildSrc/build.gradle
dependencies {
...
compile group: 'org.smali', name: 'dexlib2', version: '2.2.4'
...
}
2.plugin代码
class QFixPlugin implements Plugin<Project> {
void apply(Project project1) {
project1.afterEvaluate { project ->
project.tasks.mergeDexDebug {
doLast {
println 'QFixPlugin inject Class after mergeDexDebug'
project.tasks.mergeDexDebug.getOutputs().getFiles().each { dir ->
println "outputs: " + dir
if (dir != null && dir.exists()) {
def files = dir.listFiles()
files.each { file ->
String dexfilepath = file.getAbsolutePath()
println "Outputs Dex file's path: " + dexfilepath
InjectClassHelper.injectHackClass(dexfilepath)
}
}
}
}
}
}
}
}
InjectClassHelper.java
public class InjectClassHelper {
public static void injectHackClass(String dexPath) {
try {
File file = new File(dexPath);
String fileName = file.getName();
String indexStr = fileName.split("\\.")[0].replace("classes", "");
System.out.println(" =============indexStr:"+indexStr);
String className = "com.a.Hack"+ indexStr;
String classType = "Lcom/a/Hack" + indexStr + ";";
DexBackedDexFile dexFile = DexFileFactory.loadDexFile(dexPath, Opcodes.getDefault());
ImmutableDexFile immutableDexFile = ImmutableDexFile.of(dexFile);
Set<ClassDef> classDefs = new HashSet<>();
for (ImmutableClassDef classDef : immutableDexFile.getClasses()) {
classDefs.add(classDef);
}
ImmutableClassDef immutableClassDef = new ImmutableClassDef(
classType,
AccessFlags.PUBLIC.getValue(),
"Ljava/lang/Object;",
null, null, null, null, null);
classDefs.add(immutableClassDef);
String resultPath = dexPath;
File resultFile = new File(resultPath);
if (resultFile != null && resultFile.exists()) resultFile.delete();
DexFileFactory.writeDexFile(resultPath, new DexFile() {
@Override
public Set<ClassDef> getClasses() {
return new HashSet<>(classDefs);
}
@Override
public Opcodes getOpcodes() {
return dexFile.getOpcodes();
}
});
System.out.println("Outputs injectHackClass: " + file.getName() + ":" + className);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Mapping
Outputs Dex file's path: /Users/mawenqiang/Documents/demo_project/hotfix_dexload/dexload_QFix/build/intermediates/dex/debug/out/classes2.dex
Outputs injectHackClass: classes2.dex:com.a.Hack2
Outputs Dex file's path: /Users/mawenqiang/Documents/demo_project/hotfix_dexload/dexload_QFix/build/intermediates/dex/debug/out/classes.dex
Outputs injectHackClass: classes.dex:com.a.Hack
执行指令 dexdump
#dexdump -h classes2.dex > classes2.dump
Class #1697 header:
class_idx : 2277 #class_idx
......
Class descriptor : 'Lcom/a/fix/M;'
......
我们可以得到mapping.txt
classes2.dex:com.a.Hack2:com.a.fix.M:2277
导入patch.dex和mapping.text
load patch.dex
patch.dex的生成和加载不变,参看本文上方。
resolve 补丁M.class
同样在ApplicationApp.attachBaseContext()中执行,在load patch之后执行。 代码文件 ApplicationApp.java
- 解析Mapping.txt,得到hackClassName,patchClassIdx
- classLoader.loadClass(com.a.Hack2)
- nativeResolveClass(hackClassDescriptor, patchClassIdx)
public static void resolvePatchClasses(Context context) {
try {
BufferedReader br = new BufferedReader(new FileReader(context.getExternalCacheDir().getAbsolutePath() + "/classIdx.txt"));
String line = "";
while (!TextUtils.isEmpty(line = br.readLine())) {
String[] ss = line.split(":");
//classes2.dex:com.a.Hack2:com.a.fix.M:2277
if (ss != null && ss.length == 4) {
String hackClassName = ss[1];
long patchClassIdx = Long.parseLong(ss[3]);
Log.d("alvin", "readLine:" + line);
String hackClassDescriptor = "L" + hackClassName.replace('.', '/') + ";";
Log.d("alvin", "classNameToDescriptor: " + hackClassName + " --> " + hackClassDescriptor);
ResolveTool.loadClass(context, hackClassName);
ResolveTool.nativeResolveClass(hackClassDescriptor, patchClassIdx);
}
}
br.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* * "descriptor" should have the form "Ljava/lang/Class;" or
* * "[Ljava/lang/Class;", i.e. a descriptor and not an internal-form
* * class name.
*
* @param referrerDescriptor
* @param classIdx
* @return
*/
public static native boolean nativeResolveClass(String referrerDescriptor, long classIdx);
public static void loadClass(Context context, String className) {
try {
Log.d("alvin", context.getClassLoader().loadClass(className).getSimpleName());
} catch (Exception e) {
e.printStackTrace();
Log.d("alvin", e.getMessage());
}
}
nativeResolveClass 就是正常的jni方法,代码实际也是简单的。
#include <jni.h>
#include <android/log.h>
#include <dlfcn.h>
#define LOG_TAG "alvin"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
//方法指针
void *(*dvmFindLoadedClass)(const char *);
//方法指针
void *(*dvmResolveClass)(const void *, unsigned int, bool);
extern "C" jboolean Java_com_a_dexload_qfix_ResolveTool_nativeResolveClass(JNIEnv *env, jclass thiz,
jstring referrerDescriptor,
jlong classIdx) {
LOGE("enter nativeResolveClass");
void *handle = 0;
handle = dlopen("/system/lib/libdvm.so", RTLD_LAZY);
if (!handle) LOGE("dlopen libdvm.so fail");
if (!handle) return false;
const char *loadClassSymbols[3] = {
"_Z18dvmFindLoadedClassPKc", "_Z18kvmFindLoadedClassPKc", "dvmFindLoadedClass"};
for (int i = 0; i < 3; i++) {
dvmFindLoadedClass = reinterpret_cast<void *(*)(const char *)>(
dlsym(handle, loadClassSymbols[i]));
if (dvmFindLoadedClass) {
LOGE("dlsym dvmFindLoadedClass success %s", loadClassSymbols[i]);
break;
}
}
const char *resolveClassSymbols[2] = {"dvmResolveClass", "vResolveClass"};
for (int i = 0; i < 2; i++) {
dvmResolveClass = reinterpret_cast<void *(*)(const void *, unsigned int, bool)>(
dlsym(handle, resolveClassSymbols[i]));
if (dvmResolveClass) {
LOGE("dlsym dvmResolveClass success %s", resolveClassSymbols[i]);
break;
}
}
if (!dvmFindLoadedClass) LOGE("dlsym dvmFindLoadedClass fail");
if (!dvmResolveClass) LOGE("dlsym dvmResolveClass fail");
if (!dvmFindLoadedClass || !dvmResolveClass) return false;
const char *descriptorChars = (*env).GetStringUTFChars(referrerDescriptor, 0);
//referrerClassObj 即为 com.a.Hack2
void *referrerClassObj = dvmFindLoadedClass(descriptorChars);
dvmResolveClass(referrerClassObj, classIdx, true);
return true;
}
到此,代码就全部实现了。