版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/

FART 和 Frida 结合会发生什么?

对 FART 进一步增强:

  1. 增强 FART 的脱壳能力:解决对抗 FART 的壳、动态加载的 dex 的 dump 和修复;

  2. 控制 FART 主动调用的范围,让 FART 更精细化,比如按需进行类甚至是函数的修复。

非双亲委派关系下动态加载的 dex 脱壳问题

由于动态加载的 dex 没有取改变 android 中 ClassLoader 双亲委派关系,所以动态加载的 dex 没有自动脱壳。

相关文章:

在 android studio 中创建一个 plugin module 其中包含一个 FartTest 类源码如下:

package com.cyrus.example.plugin

import android.util.Log

class FartTest {

    fun test(): String {
        Log.d("FartTest", "call FartTest test().")
        return "String from FartTest."
    }

}

把 plugin-debug.apk push 到 files 目录下

adb push "D:\Projects\AndroidExample\plugin\build\intermediates\apk\debug\plugin-debug.apk" /sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk

ls 一下 files 目录是否存在 plugin-debug.apk

adb shell ls /sdcard/Android/data/com.cyrus.example/files

在 app 动态加载 files 目录下的 plugin-debug.apk 并调用 FartTest 的 test 方法

val apkPath = "/sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk"

// 创建 DexClassLoader 加载 sdcard 上的 apk
val classLoader = DexClassLoader(
    apkPath,
    null,
    this@FartActivity.packageResourcePath,
    classLoader // parent 设为当前 context 的类加载器
)

// classLoader 加载 com.cyrus.example.plugin.FartTest 类并通过反射调用 test 方法
val pluginClass = classLoader.loadClass("com.cyrus.example.plugin.FartTest")
val constructor = pluginClass.getDeclaredConstructor()
constructor.isAccessible = true
val instance = constructor.newInstance()
val method = pluginClass.getDeclaredMethod("test")
method.isAccessible = true
val result = method.invoke(instance) as? String

log("动态加载:${apkPath}\n\ncall ${method}\n\nreuslt=${result}")

mClassLoader = classLoader

脱壳完成,但是没有对 plugin-debug.apk 中的目标类 FartTest 发起主动调用

word/media/image1.png

这时候 frida 就派上用场了,因为 frida 本身具有枚举所有 ClassLoader 的能力。

Frida + FART 脱壳动态加载的 dex

枚举出所有 ClassLoader 后,再结合 FART 的 api 就可以实现动态加载 dex 的脱壳。

function invokeAllClassloaders() {
    Java.perform(function () {
        try {
            // 获取 ActivityThread 类
            var ActivityThread = Java.use("android.app.ActivityThread");

            Java.enumerateClassLoaders({
                onMatch: function (loader) {
                    try {
                        // 过滤掉 BootClassLoader
                        if (loader.toString().includes("BootClassLoader")) {
                            console.log("[-] 跳过 BootClassLoader");
                            return;
                        }

                        // 调用 fartWithClassLoader
                        console.log("[*] 调用 fartwithClassloader -> " + loader);
                        ActivityThread.fartwithClassloader(loader);
                    } catch (e) {
                        console.error("[-] 调用失败: " + e);
                    }
                },
                onComplete: function () {
                    console.log("[*] 枚举并调用完毕");
                }
            });
        } catch (err) {
            console.error("[-] 脚本执行异常: " + err);
        }
    });
}


setImmediate(invokeAllClassloaders)

把 log 导出到 txt

adb logcat -v time > logcat.txt

打开 app 后执行脚本

frida -H 127.0.0.1:1234 -F -l fart_invoke_all_classloaders.js

从输出日志可以看到已经成功对 FartTest 类中方法发起主动调用

word/media/image2.png

局部变量的 ClassLoader 枚举不出来

但还有一个问题呢:局部变量的 ClassLoader 枚举不出来。

因为:

  • enumerateClassLoaders() 只枚举当前 VM 中可访问的、被 GC Root 持有的 ClassLoader;

  • 如果 DexClassLoader 作为临时变量创建后,没有被保存,就会被 GC 回收或无法遍历到。

比如,下面的 Kotlin 代码中,当 DexClassLoader 为局部变量时就没有枚举出这个 DexClassLoader 。

/**
 * 局部变量的 ClassLoader
 */
fun onLocalClassLoaderClicked(log: (String) -> Unit) {

    val apkPath = "/sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk"

    // 创建 DexClassLoader 加载 sdcard 上的 apk
    val classLoader = DexClassLoader(
        apkPath,
        null,
        this@FartActivity.packageResourcePath,
        classLoader // parent 设为当前 context 的类加载器
    )

    // classLoader 加载 com.cyrus.example.plugin.FartTest 类并通过反射调用 test 方法
    val pluginClass = classLoader.loadClass("com.cyrus.example.plugin.FartTest")
    val constructor = pluginClass.getDeclaredConstructor()
    constructor.isAccessible = true
    val instance = constructor.newInstance()
    val method = pluginClass.getDeclaredMethod("test")
    method.isAccessible = true
    val result = method.invoke(instance) as? String

    log("局部变量的 ClassLoader 动态加载:${apkPath}\n\ncall ${method}\n\nreuslt=${result}\n\n")
}

在构造 ClassLoader 时脱壳

所以,为了解决这种情况,我们 hook DexClassLoader 构造函数去调用 FART 脱壳 就可以解决了。

function fartOnDexclassloader() {
    Java.perform(function () {
        var DexClassLoader = Java.use("dalvik.system.DexClassLoader");
        var ActivityThread = Java.use("android.app.ActivityThread");

        DexClassLoader.$init.overload(
            'java.lang.String',     // dexPath
            'java.lang.String',     // optimizedDirectory
            'java.lang.String',     // librarySearchPath
            'java.lang.ClassLoader' // parent
        ).implementation = function (dexPath, optimizedDirectory, libPath, parent) {
            console.log("[+] DexClassLoader created:");
            console.log("    |- dexPath: " + dexPath);
            console.log("    |- optimizedDirectory: " + optimizedDirectory);
            console.log("    |- libPath: " + libPath);

            var cl = this.$init(dexPath, optimizedDirectory, libPath, parent);

            // 调用 fart 方法
            try {
                console.log("[*] Calling fartWithClassLoader...");
                ActivityThread.fartwithClassloader(this);
                console.log("[+] fartWithClassLoader finished.");
            } catch (e) {
                console.error("[-] Error calling fartWithClassLoader:", e);
            }

            return cl;
        };
    });
}

setImmediate(fartOnDexclassloader)

启动 app 并执行脚本

frida -H 127.0.0.1:1234 -l fart_on_dexclassloader.js -f com.cyrus.example

frida 日志如下:

Spawned `com.cyrus.example`. Use %resume to let the main thread start executing!
[Remote::com.cyrus.example]-> %resume
[Remote::com.cyrus.example]-> [+] DexClassLoader created:
    |- dexPath: /sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk
    |- optimizedDirectory: null
    |- libPath: /data/app/com.cyrus.example-DjrDTvMGrC1TBVLehVPmHQ==/base.apk
[*] Calling fartWithClassLoader...
[+] fartWithClassLoader finished.

可以看到成功 hook 到 局部变量的 DexClassLoader 构造函数

从 logcat 可以看到正在对 ClassLoader 中的类方法发起主动调用

word/media/image3.png

等调用完成,进入 fart 目录下可以看到脱壳下来的文件

wayne:/sdcard/Android/data/com.cyrus.example/fart # ls
12968_class_list.txt            17104392_ins_7079.bin        400440_class_list_execute.txt 54120_dex_file.dex
12968_class_list_execute.txt    17268924_class_list.txt      400440_dex_file_execute.dex   54120_ins_7079.bin
12968_dex_file.dex              17268924_dex_file.dex        4461704_class_list.txt        66552_class_list_execute.txt
12968_dex_file_execute.dex      17268924_ins_7079.bin        4461704_dex_file.dex          66552_dex_file_execute.dex
12968_ins_7079.bin              20996_class_list_execute.txt 4461704_ins_7079.bin          9085048_class_list_execute.txt
16800_class_list_execute.txt    20996_dex_file_execute.dex   536008_class_list.txt         9085048_dex_file_execute.dex
16800_dex_file_execute.dex      21024_class_list_execute.txt 536008_class_list_execute.txt 9248236_class_list.txt
17104392_class_list.txt         21024_dex_file_execute.dex   536008_dex_file.dex           9248236_class_list_execute.txt
17104392_class_list_execute.txt 33196_class_list.txt         536008_dex_file_execute.dex   9248236_dex_file.dex
17104392_dex_file.dex           33196_dex_file.dex           536008_ins_7079.bin           9248236_dex_file_execute.dex
17104392_dex_file_execute.dex   33196_ins_7079.bin           54120_class_list.txt          9248236_ins_7079.bin

控制 FART 主动调用的范围

FART 中添加的 api 天生为脱壳而生,比如 fartwithClassLoader,loadClassAndInvoke,dumpArtMethod 等等这些接口都可以由 Frida 进行主动调用来控制脱壳精细度。

1. 过滤某些主动调用

hook loadClassAndInvoke 过滤掉某些 class 的主动调用,加快脱壳进程。

比如:过滤掉 androidx.* 、org.jetbrains.* 、kotlinx.* 、org.intellij.* 相关的主动调用

// 前缀过滤逻辑
function shouldSkipClass(name) {
    return name.startsWith("androidx.") ||
        name.startsWith("android.") ||
        name.startsWith("com.google.android.") ||
        name.startsWith("org.jetbrains.") ||
        name.startsWith("kotlinx.") ||
        name.startsWith("kotlin.") ||
        name.startsWith("org.intellij.");
}

function hookLoadClassAndInvoke() {
    const ActivityThread = Java.use('android.app.ActivityThread');

    if (ActivityThread.loadClassAndInvoke) {
        ActivityThread.loadClassAndInvoke.implementation = function (classloader, className, method) {
            if (shouldSkipClass(className)) {
                console.log('[skip] loadClassAndInvoke: ' + className);
                return; // 不调用原函数
            }

            console.log('[load] loadClassAndInvoke: ' + className);
            return this.loadClassAndInvoke(classloader, className, method); // 正常调用
        };
    } else {
        console.log('[-] ActivityThread.loadClassAndInvoke not found');
    }
}

function fartOnDexclassloader() {
    var DexClassLoader = Java.use("dalvik.system.DexClassLoader");
    var ActivityThread = Java.use("android.app.ActivityThread");

    DexClassLoader.$init.overload(
        'java.lang.String',     // dexPath
        'java.lang.String',     // optimizedDirectory
        'java.lang.String',     // librarySearchPath
        'java.lang.ClassLoader' // parent
    ).implementation = function (dexPath, optimizedDirectory, libPath, parent) {
        console.log("[+] DexClassLoader created:");
        console.log("    |- dexPath: " + dexPath);
        console.log("    |- optimizedDirectory: " + optimizedDirectory);
        console.log("    |- libPath: " + libPath);

        var cl = this.$init(dexPath, optimizedDirectory, libPath, parent);

        // 调用 fart 方法
        try {
            console.log("[*] Calling fartWithClassLoader...");
            ActivityThread.fartwithClassloader(this);
            console.log("[+] fartWithClassLoader finished.");
        } catch (e) {
            console.error("[-] Error calling fartWithClassLoader:", e);
        }

        return cl;
    };
}


setImmediate(function () {
    Java.perform(function () {
        hookLoadClassAndInvoke()
        fartOnDexclassloader()
    })
})

执行脚本并输出日志到 log.txt

frida -H 127.0.0.1:1234 -l fart_loadClassAndInvoke_filter.js -f com.cyrus.example -o log.txt

输出日志如下:

word/media/image4.png

2. fart thread 调用

由于每个 app 启动都会自动调用 fartthread,有点影响手机性能。

先去掉 ActivityThread.java 中 fartthread 调用

word/media/image5.png 路径:frameworks/base/core/java/android/app/ActivityThread.java

通过 frida 调用 fartthread:

function fartThread() {
    Java.perform(function () {
        const ActivityThread = Java.use('android.app.ActivityThread')
        ActivityThread.fartthread()
    })
}

setImmediate(fartThread)

执行脚本针对当前前台应用启动 fart thread 开始脱壳

frida -H 127.0.0.1:1234 -F -l fart_thread.js

执行效果如下:

word/media/image6.png

3. 对某个类发起主动调用

如果我们只想单独对某个类发起主动调用。

通过反射拿到 dumpMethodCode

function findDumpMethodCodeMethod(){

    let dumpMethodCodeMethod = null;

    // 反射获取 dumpMethodCode 方法
    try {
        const DexFile = Java.use("dalvik.system.DexFile");
        const dexFileClazz = DexFile.class;
        const declaredMethods = dexFileClazz.getDeclaredMethods();

        for (let i = 0; i < declaredMethods.length; i++) {
            const m = declaredMethods[i];
            if (m.getName().toString() === "dumpMethodCode") {
                m.setAccessible(true);
                dumpMethodCodeMethod = m;
                break;
            }
        }

        if (!dumpMethodCodeMethod) {
            console.log("[-] dumpMethodCode not found in DexFile");
            return;
        }

        console.log("[+] dumpMethodCode Method: " + dumpMethodCodeMethod.toString());

    } catch (err) {
        console.log("[-] Exception: " + err);
    }

    return dumpMethodCodeMethod
}

调用 LoadClassAndInvoke 对指定类发起主动调用

function invokeClass(targetClassName, dumpMethodCodeMethod) {

    let foundLoader = findClassLoader(targetClassName)

    const ActivityThread = Java.use("android.app.ActivityThread");

    // 调用 ActivityThread.loadClassAndInvoke(loader, className, dumpMethodCodeMethod)
    if (ActivityThread.loadClassAndInvoke) {
        console.log('[load] loadClassAndInvoke: ' + targetClassName);
        ActivityThread.loadClassAndInvoke(foundLoader, targetClassName, dumpMethodCodeMethod);
    } else {
        console.log("[-] ActivityThread.loadClassAndInvoke not found");
    }
}

完整源码如下:

function findClassLoader(targetClassName) {
    let foundLoader = null;

    try {
        Java.enumerateClassLoaders({
            onMatch: function (loader) {
                try {
                    const clazz = loader.loadClass(targetClassName);
                    if (clazz) {
                        console.log("[+] Found class in loader: " + loader.toString());
                        foundLoader = loader;
                        throw "found"; // 快速退出枚举
                    }
                } catch (e) {
                    // Ignore: class not found in this loader
                }
            },
            onComplete: function () {
            }
        });
    } catch (e) {
        if (e !== "found") {
            console.log("[-] ClassLoader enumeration error: " + e);
        }
    }

    if (!foundLoader) {
        console.log("[-] Could not find class: " + targetClassName);
    }

    return foundLoader
}

function findDumpMethodCodeMethod(){

    let dumpMethodCodeMethod = null;

    // 反射获取 dumpMethodCode 方法
    try {
        const DexFile = Java.use("dalvik.system.DexFile");
        const dexFileClazz = DexFile.class;
        const declaredMethods = dexFileClazz.getDeclaredMethods();

        for (let i = 0; i < declaredMethods.length; i++) {
            const m = declaredMethods[i];
            if (m.getName().toString() === "dumpMethodCode") {
                m.setAccessible(true);
                dumpMethodCodeMethod = m;
                break;
            }
        }

        if (!dumpMethodCodeMethod) {
            console.log("[-] dumpMethodCode not found in DexFile");
            return;
        }

        console.log("[+] dumpMethodCode Method: " + dumpMethodCodeMethod.toString());

    } catch (err) {
        console.log("[-] Exception: " + err);
    }

    return dumpMethodCodeMethod
}

function invokeClass(targetClassName, dumpMethodCodeMethod) {

    let foundLoader = findClassLoader(targetClassName)

    const ActivityThread = Java.use("android.app.ActivityThread");

    // 调用 ActivityThread.loadClassAndInvoke(loader, className, dumpMethodCodeMethod)
    if (ActivityThread.loadClassAndInvoke) {
        console.log('[load] loadClassAndInvoke: ' + targetClassName);
        ActivityThread.loadClassAndInvoke(foundLoader, targetClassName, dumpMethodCodeMethod);
    } else {
        console.log("[-] ActivityThread.loadClassAndInvoke not found");
    }
}


setImmediate(function () {
    Java.perform(function () {

        let dumpMethodCodeMethod = findDumpMethodCodeMethod()

        // TODO: 替换为你的目标类
        invokeClass("com.cyrus.example.plugin.FartTest", dumpMethodCodeMethod)
    })
})

执行脚本,附近到当前前台应用

frida -H 127.0.0.1:1234 -F -l fart_invoke_class.js

输入如下:

[+] dumpMethodCode Method: private static native void dalvik.system.DexFile.dumpMethodCode(java.lang.Object)
[+] Found class in loader: dalvik.system.DexClassLoader[DexPathList[[zip file "/sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.cyrus.example-DjrDTvMGrC1TBVLehVPmHQ==/base.apk, /system/lib64, /system/product/lib64]]]
[load] loadClassAndInvoke: com.cyrus.example.plugin.FartTest

在 Logcat 中可以看到只对指定的类进行了主动加载和调用

word/media/image7.png

代码与功能整合

整合代码实现如下功能:

  • 过滤不需要主动调用的类

  • 解决局部变量的 ClassLoader 枚举不出来问题

  • 解决非双亲委派关系下动态加载的 dex 脱壳问题

完整代码如下:

// 前缀过滤逻辑
function shouldSkipClass(name) {
    return name.startsWith("androidx.") ||
        name.startsWith("android.") ||
        name.startsWith("com.google.android.") ||
        name.startsWith("org.jetbrains.") ||
        name.startsWith("kotlinx.") ||
        name.startsWith("kotlin.") ||
        name.startsWith("org.intellij.");
}

function hookLoadClassAndInvoke() {
    const ActivityThread = Java.use('android.app.ActivityThread');

    if (ActivityThread.loadClassAndInvoke) {
        ActivityThread.loadClassAndInvoke.implementation = function (classloader, className, method) {
            if (shouldSkipClass(className)) {
                console.log('[skip] loadClassAndInvoke: ' + className);
                return; // 不调用原函数
            }

            console.log('[load] loadClassAndInvoke: ' + className);
            return this.loadClassAndInvoke(classloader, className, method); // 正常调用
        };
    } else {
        console.log('[-] ActivityThread.loadClassAndInvoke not found');
    }
}

function fartOnDexclassloader() {
    var DexClassLoader = Java.use("dalvik.system.DexClassLoader");
    var ActivityThread = Java.use("android.app.ActivityThread");

    DexClassLoader.$init.overload(
        'java.lang.String',     // dexPath
        'java.lang.String',     // optimizedDirectory
        'java.lang.String',     // librarySearchPath
        'java.lang.ClassLoader' // parent
    ).implementation = function (dexPath, optimizedDirectory, libPath, parent) {
        console.log("[+] DexClassLoader created:");
        console.log("    |- dexPath: " + dexPath);
        console.log("    |- optimizedDirectory: " + optimizedDirectory);
        console.log("    |- libPath: " + libPath);

        var cl = this.$init(dexPath, optimizedDirectory, libPath, parent);

        // 调用 fart 方法
        try {
            console.log("[*] Calling fartWithClassLoader...");
            ActivityThread.fartwithClassloader(this);
            console.log("[+] fartWithClassLoader finished.");
        } catch (e) {
            console.error("[-] Error calling fartWithClassLoader:", e);
        }

        return cl;
    };
}

function invokeAllClassloaders() {
    try {
        // 获取 ActivityThread 类
        var ActivityThread = Java.use("android.app.ActivityThread");

        Java.enumerateClassLoaders({
            onMatch: function (loader) {
                try {
                    // 过滤掉 BootClassLoader
                    if (loader.toString().includes("BootClassLoader")) {
                        console.log("[-] 跳过 BootClassLoader");
                        return;
                    }

                    // 调用 fartWithClassLoader
                    console.log("[*] 调用 fartwithClassloader -> " + loader);
                    ActivityThread.fartwithClassloader(loader);
                } catch (e) {
                    console.error("[-] 调用失败: " + e);
                }
            },
            onComplete: function () {
                console.log("[*] 枚举并调用完毕");
            }
        });
    } catch (err) {
        console.error("[-] 脚本执行异常: " + err);
    }
}


setImmediate(function () {
    Java.perform(function () {
        // 过滤不需要主动调用的类
        hookLoadClassAndInvoke()
        // 解决局部变量的 ClassLoader 枚举不出来问题
        fartOnDexclassloader()
        // 解决非双亲委派关系下动态加载的 dex 脱壳问题
        invokeAllClassloaders()
    })
})

启动 app 执行脚本,并输出日志到 log.txt

frida -H 127.0.0.1:1234 -l fart.js -f com.cyrus.example -o log.txt

或者 hook 当前前台 app ,并输出日志到 log.txt

frida -H 127.0.0.1:1234 -F -l fart.js -o log.txt

输出日志如下:

word/media/image8.png

在 /sdcard/Android/data/com.cyrus.example/fart 下可以找到脱壳文件

word/media/image9.png

FART 脱壳结束得到的文件列表(分 Execute 与 主动调用两类):

  1. Execute 脱壳点得到的 dex (*_dex_file_execute.dex)和 dex 中的所有类列表( txt 文件)

  2. 主动调用时 dump 得到的 dex (*_dex_file.dex)和此时 dex 中的所有类列表,以及该 dex 中所有函数的 CodeItem( bin 文件)

完整源码

开源地址:

相关文章: