修改dex完成插装&实战之应用注入Xposed模块

2,850 阅读5分钟

前介

讲解前我想先问大家一个问题。 jardex 文件他们俩到底是啥关系呢?

  1. jar 是给 JVM 运行的字节码文件包。
  2. dexDalvik&ART 运行的字节码文件包。

其实 jardex 根本上就是2个东西。只是 Andorid 的第一个开发语言是 JavaJava 的字节码文件是 class(对应合集 Jar 包)。

因此 Google 爸爸,对 Java 生成的 Jar 包做了一次优化(好像是法律原因),变成了 Dalvik&ART 认识的字节码文件 Dex

其实有时候我想红色框框的过程其实没啥用,如果 Android 第一开发语言不是 Java,这个过程会不会就不存在了呢?或者新的编译器可以直接将 N个 Java 文件,直接编译成一个 dex 文件。

插装

前几年非常火的一项技术 面向切面编程,在 Apk 编译过程中,得到 class 字节码,然后通过 ASM 库对 class 字节码进行修改 。

前面说了 Andorid 的字节码产物,除了 class (jar) 还有 dex,哪我们能不能通过修改 dex完成插装过程呢?

Dex 结构

前面说了 Dex 是通过 Jar 优化过来的。相对于 Jar 里面是多个 class 单独文件(Zip包)。但是优化后的 Dex 并没有多个文件一说,他按照一种定义的结构,通过长度&偏移量计算出每个类或方法数据。(下图为Dex的文件结构)

举个简单的例子,我们例如 Dex 文件中有个 A 类,我们能在 class_defs 中找到这个类的数据总长度与在 data 去的数据偏移量,然后就能从 data 中读取出这个类的信息。

详细细节可以看 blog.csdn.net/sinat_18268… 这篇文章。

修改Dex

我们知道ASM是修改 class 字节码的库,那有没有修改 dex 的库呢?通过查看 Apktool 源码,我们发现了另外一个强大的库 smali 它提供了 dex 的解析与 smali 的相互转换.

解析 dex 主要用到 dexlib2

学习dexlib2

我们来简单的学习下解析 Dex 的方法(其实解析简单,主要难的是如何增删改查指令,因为会用到寄存器相关的知识)。

遍历dex的类

fun openDex(path: String): org.jf.dexlib2.iface.DexFile {
    // 1. 加载一个 dex
    val dexBackedDexFile =
        DexFileFactory.loadDexFile(File(path), Opcodes.getDefault())
    // 2. 创建一个 dex Rewriter 重写器
    val dexWriter = DexRewriter(object : RewriterModule() {})
    // 3. 读取 dex
    val newDexFile = dexWriter.dexFileRewriter.rewrite(dexBackedDexFile)

    // 4. 获取dex的所有class 遍历
    for (classDef in newDexFile.classes) {
        print("${classDef.type}  ${classDef.superclass}  ${classDef.accessFlags}")
    }

    return newDexFile
}

修改dex类

我们简单的将一个dex中所有类中的 exemptAll 方法名,变更成 testExemptAll 方法名。

private fun changeMethodName() {
    // 1. 读取dex
    val dexBackedDexFile =
        DexFileFactory.loadDexFile(File("/Users/wengege/Desktop/NewWx/classes-3.0.dex"), Opcodes.getDefault())
    // 2. 创建dex重写器
    val dexWriter = DexRewriter(object : RewriterModule() {
        override fun getMethodRewriter(rewriters: Rewriters): Rewriter<Method> {
            return Rewriter {
                // 3. 如果有一个方法的昵称是 exemptAll
                if (it.name == "exemptAll") {
                    object : MethodWrapper(it) {
                        // 将方法名变成 testExemptAll
                        override fun getName(): String {
                            return "testExemptAll"
                        }
                    }
                } else {
                    it
                }
            }
        }
    })
    // 4. 读写器开始工作
    val newDexFile = dexWriter.dexFileRewriter.rewrite(dexBackedDexFile)
    // 5. 将改后的dex写入文件
    DexFileFactory.writeDexFile("/Users/wengege/Desktop/NewWx/new.dex", newDexFile)
}

合并2个dex

private fun margeDex() {
    // 1. 创建 dex pool
    val dexPool = DexPool(Opcodes.getDefault())
    // 2. 打开dex1
    val d1 = openDex("/Users/wengege/Desktop/NewWx/classes-3.0.dex")
    // 3. 打开dex2
    val d2 = openDex("/Users/wengege/Desktop/NewWx/weixin807android1920/classes10.dex")
    // 4. 遍历dex1的所有类
    for (classDef in d1.classes) {
        // 5. 将类写入 dexPool
        dexPool.internClass(classDef);
    }
    // 6. 遍历dex2的所有类
    for (classDef in d2.classes) {
        // 7. 遍历dex2的所有类
        dexPool.internClass(classDef);
    }
    // 7. 写出合并的dex
    dexPool.writeTo(FileDataStore(File("/Users/wengege/Desktop/NewWx/marge.dex")));
}

开始实战开发XpRoot

前面我讲解了,通过 dexlib2 可以实现 dex 修改。思考下以前的 ASM 是操作的 class,所以我们一般操作的都是 apk 生成前的产物。
那如果是修改 dex 呢?是不是就能直接操作 apk 了呢?

换句话说我们改造的时机靠后了,可以改造的范围也变大了。也就是说任意 App 我们都可以改造了(包括第三方的App)。

Hook

whaleSandHook 都是基于应用的 Hook 框架,可以通过它来实现拦截应用中的方法。

那如果我们把这个框架注入到第三方的 apk 中,是不是就说我们有了篡改这些 App 的手段呢?

方案定制

其实最难的部分是红色框框部分。

开始敲代码

都准备好啦,接下来我们开始

解压Apk

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

override fun execute(): File {
    val apk = File(apkPath)
    if (!apk.exists()) {
        throw RuntimeException("宿主APP不存在")
    }
    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", "解压APP完成:${result.absolutePath}")
}
}

将加载核心Dex追到classesN

/**
 * 增加调用入口的dex方法
 */
class CopyAppendDexTask(val unZipDir: File) : Task<File, File>() {
    companion object {
        private const  val DEX_FILE = "xp_call_core.dex"
    }

    override fun execute(): File {
        val dexSize = unZipDir.listFiles().filter { it.name.endsWith(".dex") }.size
        Log.d("CopyAppendDexTask", "当前dex的个数为 $dexSize")
        val dexFileStream = Thread.currentThread().contextClassLoader.getResourceAsStream(DEX_FILE)
        // 文件写入
        val appendDexFile = File(unZipDir, "classes${dexSize + 1}.dex").apply {
            writeBytes(dexFileStream.readBytes())
        }
        return appendDexFile
    }

    override fun complete(result: File) {
    }
}

解析AndroidManifest获取Application

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

    override fun execute(): 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", "获取到 application $result")
    }

}

宿主Application增加静态代码块

这里就要分2种情况了。Application存在与不存在的情况。来个流程图吧!

/**
 * 构建静态代码块的 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);
}
/**
 * 原有的静态代码块插入
 * XpRoot.start() 方法
 */
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);
}

由于代码逻辑过长,请自行查看 ModifyApplicationDexTask 逻辑。

坑1

本以为这样的一个这样的思路是没有问题的。但是测试过程中遇到个大坑。

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

看上面的日志,应该是说 Dex 方法数超了 65535

原因由于宿主的 Application 存在的 Dex 方法数为 65535,我增加了个 XpRoot.start() 方法,超过了 Dex 的最大方法数导致 Dex 不对。

思考下,解决 65535 问题,只有多 Dex 好像别无它法。

因此我们的思路是创建 ProxyApplication (负责初始化Hook框架&加载插件) ,然后让 ProxyApplication 继承宿主 Application。然后修改 AndroidManifestapplicationProxyApplication

这里要注意个点,宿主的Application如果为 final 修饰要去掉,因为我要 ProxyApplication 继承宿主 Application

看看结果,完美!!

来是骡子是马 Run 一下。

看日志宿主的 Application 已经创建了,但是好像没有创建成功。构造方法错了???看下 smali 代码。

我去还真是!!由于我换了 ProxyApplication 的父类,因此默认的构造方法也要换父类的。
找到问题了继续改造代码,将原有的 super() 执行真正继承类的 super()

// 自己的application
val changeClassDef = ClassDefWrapper(it)
// 让我的 application 继承宿主的application
changeClassDef.setSupperClass(applicationDexFile.appClassDef.type)

// 因为改了父类 因此默认的构造方法 调用的 super 方法也要改
// 1. 查找默认无参构造方法
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) {
    // 构建新的构造方法
    val newInitMethod = changeInitSuperMethod(changeClassDef.type, defaultInitMethod, changeClassDef.superclass)
    // 把原来的从集合中删除掉
    changeClassDef.originDirectMethods.remove(defaultInitMethod)
    // 在增加新的
    changeClassDef.originDirectMethods.add(newInitMethod)
}

看看结果对吗?

擦屁股

代码改造完成了,该注入的也注入了,该趟的坑也趟玩了!! 只需要重新压缩 App,在签名 App 就行了。

// 重新压缩App
val unsignedApp = ZipTask(unApkFile).call()
// 重新签名
SignApkTask(unsignedApp).call()

验收成果

我曾经开源过一个通过 Xposed 拦截 App 一些关键方法,方便用户调试的应用 AppHook

今天就用它来测试,并注入到微信中测试下。

由于以前开源的 AppHook 存在控制,这次是将模块注入到了App中,因此需要改造下代码。

只需要改下 Core xposedHook 去除掉判断依据就可以啦!

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()
            .execute()
    }
}

源代码&使用

Github 素质三连

Thanks

Xpath