Frida + FART 联手:解锁更强大的 Android 脱壳新姿势

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

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 发起主动调用

这时候 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 类中方法发起主动调用

局部变量的 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 中的类方法发起主动调用

等调用完成,进入 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

输出日志如下:

2. fart thread 调用

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

先去掉 ActivityThread.java 中 fartthread 调用

路径: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

执行效果如下:

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 中可以看到只对指定的类进行了主动加载和调用

代码与功能整合

整合代码实现如下功能:

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

  • 解决局部变量的 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

输出日志如下:

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

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

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

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

完整源码

开源地址:

相关文章:

相关推荐
三少爷的鞋3 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
Dovis(誓平步青云)7 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码8 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程9 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程12 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程13 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程13 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓14 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer