An overview of the

Using the principle of DexClassLoader class loading, APK contains multiple dex files and will search for classes in the DEX one by one. If found, apK will not continue to look for classes. We put the patch package.dex first, and we look for classes in the patch pack first.

The principle of analysis

Dex subcontract

Dex is a compilation binary of Java files, which can be understood as an Android optimized.class merged file. Originally, all Java files would be packaged into a single dex, but due to the 65536 problems of DEX, they would be subcontracted into multiple dex.

DexClassLoader mechanism

Android provides DexClassLoader for loading classes from Dex.

We generated patch.dex from the repaired com.a.fix.m.

Insert path.dex in front of dexElements.

When loader wants to find com.a.fix.m, it will traverse the dexElements array from front to back and terminate the traverse if it finds it.

DexClassLoader source





View the DexClassLoader source code, has 7.0 source code as an example, select some code

// Base class of DexClassLoader, code omitted
public class BaseDexClassLoader extends ClassLoader {
    
    // All the loading is left to The DexPathList, which is private and can be called by reflection
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, ...) {
        this.pathList = new DexPathList(this, dexPath, ...) ; }@Override
    protectedClass<? > findClass(String name){return pathList.findClass(name, suppressedExceptions);
    }
    / * * *@hideHidden methods can be called by reflection. Can be used to insert patch.dex */
    public void addDexPath(String dexPath) {
        pathList.addDexPath(dexPath, null /*optimizedDirectory*/);
    }
Copy the code
// Only reflection can use this class
/*package*/ final class DexPathList {

   	Element may be a dex file, an apK file containing dex, and a JAR file containing dex
    private Element[] dexElements;

    public DexPathList(ClassLoader definingContext, String dexPath,...) {
        this.dexElements = makeDexElements(splitDexPath(dexPath), ...) ; }// Can be used to insert patch.dex.
    public void addDexPath(String dexPath, File optimizedDirectory) {
       finalElement[] newElements = makeDexElements(splitDexPath(dexPath),...) ;final Element[] oldElements = dexElements;
       dexElements = new Element[oldElements.length + newElements.length];
       System.arraycopy(oldElements, 0, dexElements, 0, oldElements.length);
        // Elements can only be added to the end of the array. We need to add patch to the front
       System.arraycopy(newElements, 0, dexElements, oldElements.length, newElements.length);
    }
    
	// The summary of the figure above comes from here, traversing the dexElements array.
   	public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            / / final call on the 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 finalDexFile dexFile; }}Copy the code


Insert patch. dex in front

We need to insert patch.dex in front of dexElements. Since there is no external exposure method, reflection execution is required. That’s easy. There are a lot of options, like

  • A. d. exPathList makeDexElements, generate the path element array, merge the old and new array
  • B.D exPathList addDexPath, then inserted the new patch element to the front of the array

Plan A is also the most popular plan on the Internet



Plan B, if you understand this thing, you can figure it out.



Process implementation

Let’s start with a piece of code for display. Full version of the code in hot Fix cold startup module:hotfix_dexload

Unrepaired function

// Class to be repaired
package com.a.fix;
public class M {
    public static String a(a){return  "M aaa";}
}

// The class used to display data
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)); }}Copy the code

Generate patch. Dex

Class repair code

package com.a.fix;
public class M {
    public static String a(a){return  "M aaa fix";}
}
Copy the code

java -> dex

Thus, a simple dex file is generated. After patch.dex is generated, we restore the code to what it was before the fix.

// Go to the Java source directory
cd app/main/java
/ /. Class files
javac com/a/fix/M.java   
/ / generated patch. Dex
dx --dex --output com/a/fix/patch.dex com/a/fix/M.class 
Copy the code

Out of curiosity, we can take a look at the dex by name. You can install the smali plugin java2smali or smali.jar from AndroidStudio

Store patch. Dex

Copy patch.dex to the assets folder

Where to put patch.dex as long as it’s readable when the app starts. Of course you can put it on an SD card. We select assets, and when the program starts, we copy it to where we want it to go.

Insert patch. Dex

From here, it’s all done in the attachBaseContext() overload of the custom ApplicationApp.

Why is this method appropriate? ApplicationApp is the first class instantiated in apK when the application is created, and attachBaseContext is actually called before onCreate(). This is another topic, you can look at the CSDN on luo Luo’s application startup process, there is mention of the creation of application. Reference source LoadedApk. MakeApplication ()

public class ApplicationApp extends Application {
   
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        	//1. Copy from assets
            String dexFilePath = copyAssetsDex("patch.dex");
        	//2. Load the patch package, that is, insert it into dexElements
            installDex(this, dexFilePath); }}// Copy patch.dex from assets to the storage system of the mobile phone.
    private String copyAssetsDex(String dexFileName) {
        // This ExternalCacheDir applies sandbox storage, and reads and writes are freely traversed
        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();
    }

	// Insert patch.dex, because it is a reflection call, the source code of different system versions may be inconsistent, so make a difference. Omit some code
	// Here, I have found some different versions that can be enriched.
	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); }}}/** * Execute insert, shame, it's someone else's code. That's plan A, and then there's simple plan B. * /
    public static void installDexAbove_6_0_And_Above(Context context, String patch) {
        // The optimized directory must be private and freely accessible.
        File cacheDir = context.getCacheDir();
        //PathClassLoader
        ClassLoader classLoader = context.getClassLoader();
        try {
            // Get the pathList attribute first
            Field pathList = getField(classLoader, "pathList");
            // Get the attribute object DexPathList by attribute reflection
            Object pathListObject = pathList.get(classLoader);
            // Get the dexElements attribute of the pathList class from the pathListObject object
            // The original dex Element array
            Field dexElementsField = getField(pathListObject, "dexElements");

            // Get the object where it exists via the dexElementsField attribute
            Object[] dexElementsObject = (Object[]) dexElementsField.get(pathListObject);

            List<File> files = new ArrayList<>();

            File file = new File(patch);/ / patches
            if (file.exists()) {
                files.add(file);
            }
            // The class used to insert the pile
// files.add(antiazyFile);
            Method method = getMethod(pathListObject, "makeDexElements", List.class, File.class, List.class, ClassLoader.class);
            final List<IOException> suppressedExceptionList = new ArrayList<IOException>();
            // Patch the element array
            Object[] patchElement = (Object[]) method.invoke(null, files, cacheDir, suppressedExceptionList, classLoader);
            // Replace the system's original Element array
            Object[] newElement = (Object[]) Array.newInstance(dexElementsObject.getClass().getComponentType(),
                    dexElementsObject.length + patchElement.length);

            // merge copy element
            System.arraycopy(patchElement, 0, newElement, 0, patchElement.length);
            System.arraycopy(dexElementsObject, 0, newElement, patchElement.length, dexElementsObject.length);

            / / replace
            dexElementsField.set(pathListObject, newElement);
        } catch(Exception e) { e.printStackTrace(); }}// Plan B is simpler and easier to understand.
	 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. Record the length of dexElements before patch insertion
            Field dexElementsField = getField(pathListObject, "dexElements");
            int oldLength = ((Object[]) dexElementsField.get(pathListObject)).length;

            / / 2. Insert the patch. Dex
            Method method = getMethod(classLoader, "addDexPath", String.class);
            method.invoke(classLoader, patch);

            //3. Read the length of dexElements after patch insertion
            Object[] newDexElements = (Object[]) dexElementsField.get(pathListObject);
            int newLength = newDexElements.length;
            
            //4. Switch back and forth to generate new 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. Re-reflect to replace dexElements
            dexElementsField.set(pathListObject, resultElements);
        } catch(Exception e) { e.printStackTrace(); }}}Copy the code

validation

At this point, our repair function is implemented. I tried Android7 and everything is OK.

The pre – verified

The phenomenon of

The above code, we try to run on Android4.4 and below, the result is wrong.

java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
at com.a.android_sample.MainActivity.onCreate(MainActivity.java16) :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).Copy the code

Error code

String str = M.a();
Copy the code

Cause analysis,

In simple terms

1. If class A and its reference classes are in the same dex, class A is verified and optimized in advance and marked CLASS_ISPREVERIFIED. Here, MainActivity is marked. 2. When we call M.a(), we need to load class M, and the virtual machine checks whether M and MainActivity belong to the same dex. It’s clearly not there. That’s an error.

Do not understand, Dalvik class loading mechanism, this reason is not analyzed. We’re standing on the shoulders of giants, not ponies crossing the river.

Specific code throw errors

Android4.4 dalvik/vm/oo/Resolve. CPP

// Some code is omitted
ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx,
    bool fromUnverifiedConstant){
    DvmDex* pDvmDex = referrer->pDvmDex;
    ClassObject* resClass;
    const char* className;
    
    // Do not repeat parsing
    resClass = dvmDexGetResolvedClass(pDvmDex, classIdx);
    if(resClass ! = NULL)returnresClass; .//这里的resClass是 com.a.fix.M,
    / / the referrer is 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");
                returnNULL; }}// Save it.dvmDexSetResolvedClass(pDvmDex, classIdx, resClass); }...return resClass;
}
Copy the code

Call link

This part can be folded without looking.

M.a()

Install the plugin java2smali for AndroidStudio and see what MainActivity builds. Mainactivity. smali part of the code

.class public Lcom/a/android_sample/MainActivity;
.source "MainActivity.java"

.method protected onCreate(Landroid/os/Bundle;)Registers 4 # Error executing to this line. .line 16 invoke-static {}, Lcom/a/fix/M; ->a()Ljava/lang/String; .line17
    ...
    invoke-virtual {v1, v0}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V
    ...
.end method
Copy the code

invoke-static

Code in Android4.4 source dalvik/vm/mterp/out/InterpC – portable. CPP

GOTO_TARGET(invokeStatic, bool methodCallRange)

    methodToCall = dvmDexGetResolvedMethod(methodClassDex, ref);
    if (methodToCall == NULL) {
        // Parse it before you parse it
        methodToCall = dvmResolveMethod(curMethod->clazz, ref, METHOD_STATIC);
    }
    GOTO_invokeMethod(methodCallRange, methodToCall, vsrc1, vdst);
GOTO_TARGET_END
Copy the code

dvmResolveMethod

Dalvik /vm/oo/ resolve. CPP

/* * Find the method corresponding to "methodRef". * If this is a static method, we ensure that the method's class is * initialized. */
// Some code is omitted
Method* dvmResolveMethod(const ClassObject* referrer, u4 methodIdx,
    MethodType methodType){
    ClassObject* resClass;
    const DexMethodId* pMethodId;
    pMethodId = dexGetMethodId(pDvmDex->pDexFile, methodIdx);

    // This is where we start calling the specific code throws we mentioned in the previous section.
    resClass = dvmResolveClass(referrer, pMethodId->classIdx, false);
    if (resClass == NULL) {
        /* can't find the class that the method is a part of */
        assert(dvmCheckException(dvmThreadSelf()));
        returnNULL; }... }Copy the code

Dex file verification was optimized

Going back to the dex file optimization, let’s put the call

//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,....)
    
// Create process /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 
Copy the code

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(a).Copy the code

reference

An in-depth understanding of the Java Virtual Machine: Advanced features and Best Practices for the JVM (version 3). PDF An in-depth understanding of the Dalvik Virtual Machine System source code (AOSP) github address link to download what you want. Or this official website links to android App hot patch dynamic repair technology to introduce android hot patch pre-verify problems and practice 05-DALVIK load and parse DEX process

The pre – verified

Project analysis

We are copying the code, and we will only get an error if all three conditions are met

// Some code is omitted
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");
                returnNULL; }}}return resClass;
}
Copy the code

Based on the above code, there are roughly four solutions.

  • Do not mark the dexopt process with CLASS_ISPREVERIFIED

Q-zone piling scheme breaks this limitation, but results in preVerify failure and loss of performance.

  • Modify fromUnverfiedConstant = true

It is necessary to intercept the native hook system method, change the entry parameters of the method, and change fromUnverifiedConstant to true uniformly, which is risky and almost not adopted by anyone. Cydia native hook

  • DvmDexGetResolvedClass does not return null

QFix uses this solution,

  • The patch class is in the same dex as the reference class

Tinker and other total synthesis schemes break through this limitation.

Q-zone pile insertion scheme

Project analysis

Using bytecode technology, a reference to Hackcode. class is inserted into the constructor of each class, causing MainActivity to refer to hack. class in hack.dex, causing Verify to fail. At this point the scheme is divided into two parts

  • Package hackcode.class separately
  • MainActivity references hackcode.class.

package com.a.hack;
public class HackCode {}
Copy the code

Where the actual code is executed.

//dalvik/vm/analysis/CodeVerify.cpp
case OP_CONST_CLASS:
	 // If it fails, the error value is set to failure.
      resClass = dvmOptResolveClass(meth->clazz, decInsn.vB, &failure);

////dalvik/vm/analysis/Optimize.cpp
/* * Performs access checks on every resolve, Mother and to acknowledge the existence of classes * defined in more than one DEX file. * Classes defined in multiple DEX are not recognized */
ClassObject* dvmOptResolveClass(ClassObject* referrer, u4 classIdx, VerifyError* pFailure){...const char* className = dexStringByTypeIdx(pDvmDex->pDexFile, classIdx);
    // Referrer is a hack.class for all reference classes including MainAcitivityClass and resClass
    Hack.class is not found in the referrer's dex
	resClass = dvmFindClassNoInit(className, referrer->classLoader);
    if (resClass == NULL) { *pFailure = VERIFY_ERROR_NO_CLASS; . }... }Copy the code

Reference hackCode. Class

Apk source code cannot include hackcode.class, we insert references via bytecode. Write a custom Gradle plug-in, using Javassist bytecode technology custom Gradle plug-in referenceGradle – Groovy, Gradle, and custom Gradle pluginsJavassist referenceJavassist uses full parsing Key code, a little long

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)
        }
        // This line should be careful, otherwise the compilation will not pass
        pool.makeClass("com.a.hack.HackCode")

        transformInvocation.inputs.each {

            it.jarInputs.each {
                pool.insertClassPath(it.file.absolutePath)
                // Rename output file (conflict with directory 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)
        // Our custom Application is the initial class, and the Hakcode reference can only be inserted after loading the dex class.
        if(ctClass.getSuperclass() ! =null
                && ctClass.getSuperclass().name == "android.app.Application") {
            return
        }
       
        // Where the insertion of the bytecode is actually performed
        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()
    }
}
Copy the code

Generate hack. Dex

Refer to the generation method of patch.dex. Write the app/main/Java/com/a/hack/HackCode. Java, separate into dex, after generated, Java can delete this file.

package com.a.hack;
public class HackCode {}
Copy the code
// Go to the Java source directory,
cd app/main/java
/ /. Class files
javac com/a/hack/HackCode.java   
/ / generated hack. Dex
dx --dex --output com/a/hack/hack.dex com/a/hack/HackCode.class
Copy the code

Load the hack. Dex

Refer to patch.dex.

validation

Verified successfully on android4.4

Cydia NativeHook

You need to intercept the native hook system method by changing the entry parameter of the method to true.

Here, we adopt Cydia Substrate, hook dvmResolveClass method, the steps are as follows: hook implementation and dynamic library download, note that the scheme is only feasible on Android4.4.

Implementation steps

Cydia so library and header files

You can download it here. Put the so library in its own directory for example

<moduleName>/src/main/jniLibs/armeabi-v7a/libsubstrate.so
<moduleName>/src/main/jniLibs/armeabi-v7a/libsubstrate-dvm.so
Copy the code

Import header file

<moduleName>/src/main/cpp/include/substrate.h
Copy the code

Hook code implementation

//<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__)

// Old function pointer to old function
void *(*oldDvmResolveClass)(void *referrer, unsigned int classIdx, bool fromUnverifiedConstant);

// New function implementation
void *newDvmResolveClass(void *referrer, unsigned int classIdx, bool fromUnverifiedConstant) {
    // Here, fromUnverifiedConstant is mandatory to true, so you don't check whether the dex is equal.
    return oldDvmResolveClass(referrer, classIdx, true);
}

// Specify the lib to hook, involving the dvmResolveClass so
MSConfig(MSFilterLibrary, "/system/lib/libdvm.so")
// Specify the application to 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;
    }
    // Concrete Hook implementation
    MSHookFunction(resloveMethd, (void *) newDvmResolveClass, (void **) &oldDvmResolveClass);
}
Copy the code

CMakeLists.txt

Generate libcydiahook. So

cmake_minimum_required(VERSION 3.102.)

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})
Copy the code

Libcydiahook. So loading

public class ApplicationApp extends Application {
    static {
        System.loadLibrary("cydiahook"); }}Copy the code

other

ClassObject properties

As with Andfix, we can introduce a dexfile.h header file that can turn arguments and results into actual class objects and view some of the class properties

 // New function implementation
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;
}
Copy the code

risk

Similar to Andfix,native Hook has various compatibility and stability problems, and even security problems. At the same time, it intercepts a method that involves dalvik’s basic functions and is called frequently, which will undoubtedly be much riskier.

QFix scheme implementation

The principle can be found in this article: QFix Road to Discovery – Manual Q hot patch lightweight solution

plan

Go back to this figure and start with dvmResolveClass method to resolve the Patch class in advance.The initial solution was to create a class with the “const-class” or “instance-of” directives, fromUnverifiedConstant = true, to bypass the dex detection. And it worked. But there are two problems:

  • How do I know which patch classes in advance?
  • Or simply refer to all classes? Performance issues? How to do that?
public class ApplicationApp extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        DexInstaller.installDex(base, this.getExternalCacheDir().getAbsolutePath() + "/patch.dex");
		// The const-class directive is executed
        Log.d("alvin"."bug class:" + com.a.fix.M.class);
}    
Copy the code

QFix gives up the scheme of directly loading patch class. After analysis,

  • The number of classes in a patch pack is limited.
  • The number of DEX files in APK is also limited.

The following scheme is obtained:

  • When apK is constructed, dex is pre-embedded with blank classes, and the associated files of each dex and blank classes are obtained.
  • Build the patch package and map the association between the blank class of bug dex and the patch class in the original dex classIdx.
  • ———– Run the app and load the patch package ————-
  • Using the Java method, call classLoader.loadClass(blank class name)
  • Using the JNI method, call dvmFindLoadedClass(blank class Descriptor)
  • Using jni method, called dvmResolveClass (referrer: blank, classIdx, fromUnverifiedConstant: true)

As for how to find this method, of course, is the source inside the wandering.

In field

All the implementation code is in Github

Blank classes are injected into Dex

Custom Gradle plugin, use smali to manipulate dexfile and inject class.

  • Custom Gradle plugin reference
  • Smali technical reference
  • Class is injected into dex

BuildSrc /build.gradle adds dependencies

//buildSrc/build.gradle
dependencies {
 	...
    compile group: 'org.smali'.name: 'dexlib2'.version: '2.2.4'. }Copy the code

2. The plugin code

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)
                            }
                        }
                    }
                }
            }
        }
    }
}

Copy the code

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(a) {
                    return new HashSet<>(classDefs);
                }

                @Override
                public Opcodes getOpcodes(a) {
                    returndexFile.getOpcodes(); }}); System.out.println("Outputs injectHackClass: " + file.getName() + ":" + className);
            
        } catch(Exception e) { e.printStackTrace(); }}}Copy the code

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
Copy the code

Run the dexdump command

#dexdump -h classes2.dex > classes2.dump

Class #1697 header:
class_idx           : 2277 #class_idx
......
Class descriptor  : 'Lcom/a/fix/M; '.Copy the code

We can get mapping.txt

classes2.dex:com.a.Hack2:com.a.fix.M:2277
Copy the code

Import patch.dex and Mapping.text

load patch.dex

The generation and loading of patch.dex remain unchanged, as shown above.

Resolve patch M.c lass

Also in ApplicationApp. AttachBaseContext (), in the load patch after execution. The code file applicationApp.java

  • Analytical Mapping. TXT, get 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()); }}Copy the code

NativeResolveClass is a normal JNI method, and the code is actually simple.

#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__)

// Method pointer
void *(*dvmFindLoadedClass)(const char *);

// Method pointer
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 is com. A.H ack2
    void *referrerClassObj = dvmFindLoadedClass(descriptorChars);
    dvmResolveClass(referrerClassObj, classIdx, true);
    return true;
}
Copy the code

At this point, the code is fully implemented.