Before the dielectric

I want to start by asking you a question. What is the relationship between jar and dex files?

  1. jarIs to giveJVMThe bytecode file package to run.
  2. dexDalvik&ARTThe bytecode file package to run.

Jar and dex are essentially two things. It’s just that the first development language for Andorid was Java and Java’s bytecode files were class (corresponding to the collection Jar package).

So Google dad, for legal reasons, optimised the java-generated Jar package and turned it into Dex, a bytecode file known to Dalvik&ART.

In fact, sometimes I wonder if the red box process is useless. If The first Android development language was not Java, would this process not exist? Or the new compiler can compile N Java files directly into a dex file.

instrumentation

One technique that has been very popular in the last few years is faceted programming, where you get the class bytecode during Apk compilation and modify the class bytecode through the ASM library.

The bytecode products of Andorid include the class (jar) and dex. Can we modify the dex to complete the insertion process?

Dex structure

Dex is optimized by Jar. Inside a Jar are multiple class individual files (Zip packages). However, the optimized Dex does not have multiple files. It calculates each class or method data by length and offset according to a defined structure. (The following figure shows the file structure of Dex)

For A simple example, if we have A class A in the Dex file, we can find the total data length of the class in class_defs and the offset of the data in data, and then we can read the information of the class from data.

Full details can be found at blog.csdn.net/sinat_18268… This article.

Modify the Dex

We know that ASM is a library for modifying class bytecodes, but is there a library for modifying dex? Through looking at Apktool source code, we found another powerful library smali which provides dex parsing and smALI conversion.

Dexlib2 is used to parse the dex

Learning dexlib2

Let’s briefly learn how to parse Dex (in fact, it is simple to parse, but the main difficulty is how to add, delete, change and look up instructions, because registers will be used).

Traverse the dex class

fun openDex(path: String): org.jf.dexlib2.iface.DexFile {
    // 1. Load a dex
    val dexBackedDexFile =
        DexFileFactory.loadDexFile(File(path), Opcodes.getDefault())
    // 2. Create a dex Rewriter
    val dexWriter = DexRewriter(object : RewriterModule() {})
    // 3. Read the dex
    val newDexFile = dexWriter.dexFileRewriter.rewrite(dexBackedDexFile)

    // 4. Obtain all class traversals of the dex
    for (classDef in newDexFile.classes) {
        print("${classDef.type} ${classDef.superclass} ${classDef.accessFlags}")}return newDexFile
}
Copy the code

Modify the dex class

You will simply change the exemptAll method name of all the classes in a DEX to testExemptAll.

private fun changeMethodName(a) {
    // 1. Read the dex
    val dexBackedDexFile =
        DexFileFactory.loadDexFile(File("/ Users/wengege/Desktop/NewWx/classes - 3.0 dex." "), Opcodes.getDefault())
    // 2. Create a dex rewriter
    val dexWriter = DexRewriter(object : RewriterModule() {
        override fun getMethodRewriter(rewriters: Rewriters): Rewriter<Method> {
            return Rewriter {
                // 3. If you have a method nickname of exemptAll
                if (it.name == "exemptAll") {
                    object : MethodWrapper(it) {
                        // Change the method name to testExemptAll
                        override fun getName(a): String {
                            return "testExemptAll"}}}else {
                    it
                }
            }
        }
    })
    // 4. The reader starts working
    val newDexFile = dexWriter.dexFileRewriter.rewrite(dexBackedDexFile)
    // 5. Write the modified dex to the file
    DexFileFactory.writeDexFile("/Users/wengege/Desktop/NewWx/new.dex", newDexFile)
}
Copy the code

Merge the two dex

private fun margeDex(a) {
    Create a Dex Pool
    val dexPool = DexPool(Opcodes.getDefault())
    // 2. Open dex1
    val d1 = openDex("/ Users/wengege/Desktop/NewWx/classes - 3.0 dex." ")
    // 3. Open dex2
    val d2 = openDex("/Users/wengege/Desktop/NewWx/weixin807android1920/classes10.dex")
    // 4. Walk through all classes of dex1
    for (classDef in d1.classes) {
        // 5. Write the class to the dexPool
        dexPool.internClass(classDef);
    }
    // 6. Walk through all classes in dex2
    for (classDef in d2.classes) {
        // 7. Traverse all classes in dex2
        dexPool.internClass(classDef);
    }
    // 7. Write the combined dex
    dexPool.writeTo(FileDataStore(File("/Users/wengege/Desktop/NewWx/marge.dex")));
}
Copy the code

XpRoot development started

As I explained earlier, dex can be modified using dexlib2. Consider that ASM was a class of operations, so we generally operate on the product before THE generation of APK. What about changing dex? Can you operate APK directly?

In other words, the opportunity of our transformation is later, and the scope of transformation is also larger. This means that we can modify any App (including third-party apps).

Hook

Whale and SandHook are application-based Hook frameworks that can be used to intercept methods in applications.

So if we inject this framework into a third party APK, does that mean we have the means to tamper with these apps?

Plan to customize

The hardest part is the red box.

Start typing code

All set, let’s get started

Unpack the Apk

companion object{
    private const val DATA_PATTERN = "yyyy-MM-dd HH:mm:ss";
    private val sdf = SimpleDateFormat(DATA_PATTERN);
}

override fun execute(a): File {
    val apk = File(apkPath)
    if(! apk.exists()) {throw RuntimeException("Host APP does not exist")
    }
    val dirName = if (IS_DEBUG) {
        "debug"
    } else {
        sdf.format(Date())
    }
    val parent = File(apk.parent, "${File(apkPath).name.getBaseName()}-${dirName}")
    if (parent.exists()) {
        FileUtils.delete(parent)
    }
    val dir = File(parent, "app")
    dir.mkdirs()
    ZipUtils.unzipFile(apk, dir)
    return dir
}


override fun complete(result: File) {
    Log.d("wyz".${result.absolutePath}")}}Copy the code

Trace the loaded core Dex to classesN

/** * add the dex method */ to the call entry
class CopyAppendDexTask(val unZipDir: File) : Task<File.File> (){
    companion object {
        private const  val DEX_FILE = "xp_call_core.dex"
    }

    override fun execute(a): File {
        val dexSize = unZipDir.listFiles().filter { it.name.endsWith(".dex") }.size
        Log.d("CopyAppendDexTask"."The current number of dex is $dexSize")
        val dexFileStream = Thread.currentThread().contextClassLoader.getResourceAsStream(DEX_FILE)
        // File write
        val appendDexFile = File(unZipDir, "classes${dexSize + 1}.dex").apply {
            writeBytes(dexFileStream.readBytes())
        }
        return appendDexFile
    }

    override fun complete(result: File) {}}Copy the code

Parse the AndroidManifest to get the Application

class GetApplicationTask(val apkFile: File) : Task<File.String> (){
    companion object {
        const val ANDROID_MANIFEST = "AndroidManifest.xml"
    }

    override fun execute(a): String {
        val manifestInput = File(apkFile, ANDROID_MANIFEST).inputStream()
        val value = ManifestParser.parseManifestFile(manifestInput)
        val applicationName = value.applicationName
        val packageName = value.packageName
        return applicationName
    }

    override fun complete(result: String) {
        Log.d("GetApplicationTask".$result = application $result)}}Copy the code

The host Application adds static code blocks

So we’re going to have two cases here. Application exists and does not exist. Let’s have a flow chart.

/** * Mehtod * static {* xproot.start (); *} * /
public static Method buildStaticContextMethod(String className) {
    ArrayList<ImmutableInstruction> instructions = Lists.newArrayList(
            ImmutableInstructionFactory.INSTANCE.makeInstruction35c(Opcode.INVOKE_STATIC, 0.0.0.0.0.0, getStaticContextMethodRef()),
            ImmutableInstructionFactory.INSTANCE.makeInstruction10x(Opcode.RETURN_VOID)
    );
    ImmutableMethodImplementation methodImpl = new ImmutableMethodImplementation(0, instructions, null.null);
    return new ImmutableMethod(className, "<clinit>".new ArrayList<>(), "V", AccessFlags.STATIC.getValue() | AccessFlags.CONSTRUCTOR.getValue(), null.null, methodImpl);
}
Copy the code
/** * The original static code block inserts the xproot.start () method */
public static Method buildStaticContextMethod(String className, Method mehtod) {
    ArrayList<Instruction> instructions = Lists.newArrayList(
            ImmutableInstructionFactory.INSTANCE.makeInstruction35c(Opcode.INVOKE_STATIC, 0.0.0.0.0.0, getStaticContextMethodRef())
    );
    MethodImplementation implementation = mehtod.getImplementation();
    MethodImplementation newImplementation = null;
    if(implementation ! =null) {
        int registerCount = implementation.getRegisterCount();
        for (Instruction instruction : mehtod.getImplementation().getInstructions()) {
            instructions.add(instruction);
        }
        newImplementation = new ImmutableMethodImplementation(registerCount, instructions, implementation.getTryBlocks(), implementation.getDebugItems());
    }

    return new ImmutableMethod(className, mehtod.getName(), mehtod.getParameters(), mehtod.getReturnType(), mehtod.getAccessFlags(), mehtod.getAnnotations(),
            mehtod.getHiddenApiRestrictions(), newImplementation);
}
Copy the code

Because the code logic is too long, see the ModifyApplicationDexTask logic for yourself.

1 the pit

I thought there was no problem with such a way of thinking. But there was a big hole in the test.

Caused by: org.jf.util.ExceptionWithContext: Error while writing instruction at code offset 0x2
	at org.jf.dexlib2.writer.DexWriter.writeCodeItem(DexWriter.java:1320)
	at org.jf.dexlib2.writer.DexWriter.writeDebugAndCodeItems(DexWriter.java:1043)...27 more
Caused by: org.jf.util.ExceptionWithContext: Unsigned short value out of range: 65536
	at org.jf.dexlib2.writer.DexDataWriter.writeUshort(DexDataWriter.java:116)
	at org.jf.dexlib2.writer.InstructionWriter.write(InstructionWriter.java:356)
	at org.jf.dexlib2.writer.DexWriter.writeCodeItem(DexWriter.java:1280)...28 more
Copy the code

The number of Dex methods exceeded 65535.

Because the number of Dex methods in the host Application was 65535, I added a xproot.start () method, which exceeded the maximum number of Dex methods, resulting in incorrect Dex.

Think about it, to solve the 65535 problem, only multiple Dex seems to have no other way.

So the idea is to create the ProxyApplication (responsible for initializing the Hook framework & loading the plug-in) and then have the ProxyApplication inherit from the host Application. Then change the AndroidManifest application to ProxyApplication.

One thing to note here is that the host Application should be removed if it is final, because I want the ProxyApplication to inherit from the host Application.

Look at the result, perfect!!

Come mule or horse Run.

The Application of the logging host has been created, but it does not seem to have been successfully created. Wrong construction method?? Take a look at the SMALI code.

I did!! Since I changed the parent class of the ProxyApplication, the default constructor will also change the parent class. Find the problem continue to modify the code, the original super() to implement the real inheritance of the super() class.

// Own application
val changeClassDef = ClassDefWrapper(it)
// Let my application inherit from the host's application
changeClassDef.setSupperClass(applicationDexFile.appClassDef.type)

// The default constructor calls the super method because the superclass has been changed
// 1. Find the default no-parameter constructor
var defaultInitMethod: Method? = null
for (method in changeClassDef.originDirectMethods) {
    if (method.parameters.size == 0 && method.name == "<init>" && method.returnType == "V") {
        defaultInitMethod = method
    }
}
if(defaultInitMethod ! =null) {
    // Build a new constructor
    val newInitMethod = changeInitSuperMethod(changeClassDef.type, defaultInitMethod, changeClassDef.superclass)
    // Delete the original set from the collection
    changeClassDef.originDirectMethods.remove(defaultInitMethod)
    // Add new ones
    changeClassDef.originDirectMethods.add(newInitMethod)
}
Copy the code

Let’s see, is that right?

wipe

Code transformation is complete, the injection is also injected, the pit is also a trip to play!! You just need to recompress the App and sign the App.

// Recompress the App
val unsignedApp = ZipTask(unApkFile).call()
// Re-sign
SignApkTask(unsignedApp).call()
Copy the code

Acceptance of the results

I have an open source through Xposed interception App some key methods, convenient user debugging application AppHook

Use it to test today and inject it into wechat to test it.

Due to the control existing in open source AppHook before, this time module is injected into App, so the code needs to be modified.

Just change Core xposedHook to get rid of the judgment.

object Core  {
    fun xposedHook(classLoader: ClassLoader) {
        if (isHook) return
        isHook = true
        MethodHook
            .Builder()
            .setClass(Application::class.java)
            .methodName("onCreate")
            .afterHookedMethod {
                val app = it.thisObject as Application
                appHook(app)}.build(a)
            .execute(a)}}Copy the code

Source code & Use

Github Quality three

Thanks

Xpath