FART 精准脱壳:通过配置文件控制脱壳节奏与范围

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

前言

由于 FART 默认会对所有 app 进行脱壳,每次 app 启动都会自动脱壳,而且会对 app 中所有类发起主动调用,这样效率比较慢,遇到 FART 对抗类也不能选择性跳过。

如何通过一份简单的配置文件,实现对 FART 脱壳过程的精准控制:包括是否启用脱壳、延迟时间、需要主动调用的类列表、排除类规则等。提高脱壳效率,也可以避开一些垃圾类(FART 对抗类)的调用。

关于 FART 的详细介绍参考下面的文章:

通过配置文件控制脱壳节奏与范围

例如,配置项如下:

ini 复制代码
# 是否开启脱壳功能(true 开启,false 关闭)
dump=true

# 启动后延迟多少毫秒再进行脱壳(单位:毫秒),避免应用初始化未完成
sleep=60000

# 明确指定哪些类名或包路径需要主动调用以触发加载(支持通配符 *)
# 示例:ff.l0.* 表示 ff.l0 包下所有类
force=ff.l0.*

# 忽略哪些类或包路径(支持通配符 *)
# 通常用于排除系统类、常见库类、FART对抗类等
ignore=androidx.*,android.*,com.google.android.*,org.jetbrains.*,kotlinx.*,kotlin.*,com.alibaba.android.arouter.*,org.intellij.*

效果说明:

  • force=:指定你想确保加载的类

  • ignore=:忽略系统包或你不想触发加载的类

  • 二者同时存在时,force 优先生效

  • 支持使用 * 匹配多个类名

1. 配置解析类实现

增加一个 Cyrus 类 用于读取和解析脱壳配置文件,并提供按类名判断是否应被主动调用的能力,配合 FART 脱壳框架实现精细化控制脱壳流程。

arduino 复制代码
package android.app;

import android.util.Log;
import java.io.*;
import java.util.*;
import java.util.regex.Pattern;

public class Cyrus {

    private static final String TAG = "Cyrus";
    private static boolean initialized = false;

    private static boolean dumpEnabled = false;
    private static int sleepTimeMs = 0;
    private static List<Pattern> forceCallClassPatterns = new ArrayList<>();
    private static List<Pattern> ignoredClassPatterns = new ArrayList<>();

    /**
     * 初始化 Cyrus 配置
     * 从 /data/data/{packageName}/cyrus.config 读取配置项:
     * dump, sleep, force, ignore
     *
     * @param packageName 应用包名
     */
    public static void init(String packageName) {
        if (initialized) return;

        File configFile = new File("/data/data/" + packageName + "/cyrus.config");
        if (!configFile.exists()) {
            Log.w(TAG, "Config file not found: " + configFile.getPath());
            initialized = true;
            return;
        }

        try (BufferedReader reader = new BufferedReader(new FileReader(configFile))) {
            String line;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (line.startsWith("dump=")) {
                    dumpEnabled = line.substring(5).equalsIgnoreCase("true");
                } else if (line.startsWith("sleep=")) {
                    sleepTimeMs = Integer.parseInt(line.substring(6));
                } else if (line.startsWith("force=")) {
                    String[] parts = line.substring(6).split(",");
                    for (String part : parts) {
                        forceCallClassPatterns.add(Pattern.compile(convertToRegex(part)));
                    }
                } else if (line.startsWith("ignore=")) {
                    String[] parts = line.substring(7).split(",");
                    for (String part : parts) {
                        ignoredClassPatterns.add(Pattern.compile(convertToRegex(part)));
                    }
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "Failed to read config: " + e.getMessage(), e);
        }

        initialized = true;
    }

    /**
     * 是否启用脱壳功能
     * @return true 表示启用
     */
    public static boolean isDumpEnabled() {
        return dumpEnabled;
    }

    /**
     * 获取脱壳前的延迟休眠时间(毫秒)
     * @return 休眠时间(单位:毫秒)
     */
    public static int getSleepTimeMs() {
        return sleepTimeMs;
    }

    /**
     * 获取匹配主动调用类的正则规则列表
     * @return 正则 Pattern 列表
     */
    public static List<Pattern> getForceCallClassPatterns() {
        return forceCallClassPatterns;
    }

    /**
     * 获取忽略主动调用类的正则规则列表
     * @return 正则 Pattern 列表
     */
    public static List<Pattern> getIgnoredClassPatterns() {
        return ignoredClassPatterns;
    }

    /**
     * 判断一个类是否需要在脱壳线程启动时被主动调用。
     * <p>
     * 判断逻辑如下:
     * 1. 如果配置中设置了 force 规则(forceCallClassPatterns 非空):
     *    - 只有匹配 force 列表中的类会返回 true,其余类返回 false。
     * 2. 如果未设置 force,但配置了 ignore 规则(ignoredClassPatterns 非空):
     *    - 匹配 ignore 列表的类返回 false,其余返回 true。
     * 3. 如果 force 和 ignore 都为空:
     *    - 默认所有类都返回 true。
     * 4. 如果同时配置了 force 和 ignore,则优先判断 force
    */
    public static boolean shouldForceCall(String className) {
        if (!forceCallClassPatterns.isEmpty()) {
            for (Pattern force : forceCallClassPatterns) {
                if (force.matcher(className).matches()) {
                    return true;
                }
            }
            return false;
        }

        if (!ignoredClassPatterns.isEmpty()) {
            for (Pattern ignored : ignoredClassPatterns) {
                if (ignored.matcher(className).matches()) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * 将配置文件中的通配符路径转为正则表达式
     * 例如 ff.l0.* → ff\.l0\..*
     * @param pattern 原始配置字符串
     * @return 正则表达式字符串
     */
    private static String convertToRegex(String pattern) {
        // exact match or wildcard * support
        if (!pattern.contains("*")) {
            return Pattern.quote(pattern);
        }
        return pattern.replace(".", "\\.").replace("*", ".*");
    }
}

2. 脱壳线程实现修改

在 launchInspectorThread 方法里:

  • 调用 init 初始化配置

  • 通过 Cyrus.isDumpEnabled() 判断当前 app 是否需要脱壳

  • 通过 Cyrus.getSleepTimeMs() 方法获取配置的休眠时间

typescript 复制代码
public static void launchInspectorThread(Context context) {
    new Thread(new Runnable() {

        @Override
        public void run() {
            // 初始化配置
            Cyrus.init(context.getPackageName());

            // 判断是否需要脱壳
            if (Cyrus.isDumpEnabled()) {

                // 休眠
                try {
                    Log.e("ActivityThread", "start sleep......" + Cyrus.getSleepTimeMs());
                    Thread.sleep(Cyrus.getSleepTimeMs());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // 开始脱壳
                Log.e("ActivityThread", "sleep over and start startCodeInspection");
                startCodeInspection();
                Log.e("ActivityThread", "startCodeInspection run over");
            }
        }
    }).start();
}

另外把 launchInspectorThread 的调用放到 handleBindApplication 里,因为 performLaunchActivity 中有可能发生多次调用。

scss 复制代码
private void handleBindApplication(AppBindData data) {
    ...
    
    //add
    launchInspectorThread(appContext);
}

3. 主动调用范围过滤

在 dispatchClassTask 中通过 Cyrus.shouldForceCall(eachclassname) 判断是否需要加载并调用当前类

typescript 复制代码
public static void dispatchClassTask(ClassLoader appClassloader, String eachclassname, Method dumpMethodCode_method) {
    boolean shouldForceCall = Cyrus.shouldForceCall(eachclassname);
    Log.i("ActivityThread", (shouldForceCall ? "[load]" : "[skip]") + " dispatchClassTask: " + eachclassname);

    if (!shouldForceCall) {
        return;
    }

    ...
}

重新编译系统

把修改后的 FART 代码替换到 Android 系统里面,重新编译。

bash 复制代码
# 初始化编译环境
source build/envsetup.sh

# 设置编译目标
breakfast wayne

# 回到 Android 源码树的根目录
croot

# 开始编译
brunch wayne

如何编译 FART ROM 参考这篇文章:移植 FART 到 Android 10 实现自动化脱壳

生成 OTA 包

bash 复制代码
./sign_ota_wayne.sh

编译完成

刷机

由于我这里是在 WSL 中编译,先把 ota 文件 copy 到 windwos 目录下

bash 复制代码
cp ./signed-ota_update.zip /mnt/e/lineageos/xiaomi6x_wayne_lineageos-17.1_signed-ota_update_fart_cyrus.zip

设备进入 recovery 模式(或者同时按住【音量+】和【开机键】)

复制代码
adb reboot recovery

【Apply update】【Apply from adb】开启 adb sideload

开始刷机

objectivec 复制代码
adb sideload E:\lineageos\xiaomi6x_wayne_lineageos-17.1_signed-ota_update_fart_cyrus.zip

成功刷入后重启手机。

脱壳配置

1. 获取 app 包名

你可以使用下面的 adb 命令来获取当前前台 app 的包名

Mac/Linux:

javascript 复制代码
adb shell dumpsys window | grep -E 'mCurrentFocus|mFocusedApp'

Windows:

javascript 复制代码
 adb shell dumpsys window | Select-String 'mCurrentFocus|mFocusedApp'

示例输出:

ini 复制代码
  mCurrentFocus=Window{b3fdf6e u0 com.shizhuang.duapp/com.shizhuang.duapp.du_login.optimize.LoginContainerActivityV2}
  mFocusedApp=AppWindowToken{c3cf4d4 token=Token{a76da27 ActivityRecord{fcc84e6 u0 com.shizhuang.duapp/.du_login.optimize.LoginContainerActivit
yV2 t55}}}
    mFocusedApp=Token{a76da27 ActivityRecord{fcc84e6 u0 com.shizhuang.duapp/.du_login.optimize.LoginContainerActivityV2 t55}}

提取其中的包名部分(如 com.shizhuang.duapp)。

2. 配置文件

通过下面命令把配置文件推送到 /data/data/<packageName>/cyrus.config 路径下:

假设只脱壳 ff 包下的类

ini 复制代码
adb shell 'cat > /data/data/com.shizhuang.duapp/cyrus.config <<EOF
dump=true
sleep=60000
force=ff.*
EOF'
  • cat > 表示覆盖写入

  • cat >> 表示追加写入

假设忽略 androidx.,android.,com.google.android.*... 中的类

ini 复制代码
adb shell 'cat > /data/data/com.shizhuang.duapp/cyrus.config <<EOF
dump=true
sleep=60000
ignore=androidx.*,android.*,com.google.android.*,org.jetbrains.*,kotlinx.*,kotlin.*,com.alibaba.android.arouter.*,org.intellij.*
EOF'

注意:如果 force 和 ignore 参数同时存在优先 force。

开始脱壳

清空日志缓存

r 复制代码
adb logcat -c

输出日志到文件

css 复制代码
adb logcat -v time > logcat.txt

打开 app 等待 60 秒开始自动脱壳(比如:只脱壳 ff 包下的类)。

等输出 run over 就是脱壳完成。

脱壳完成

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

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

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

完整源码

开源地址:github.com/CYRUS-STUDI...

相关推荐
烈焰晴天5 分钟前
使用ReactNative加载Svga动画支持三端【Android/IOS/Harmony】
android·react native·ios
阿幸软件杂货间25 分钟前
PPT转图片拼贴工具 v2.0
android·python·powerpoint
tonydf35 分钟前
还在用旧的认证授权方案?快来试试现代化的OpenIddict!
后端·安全
sg_knight41 分钟前
Flutter嵌入式开发实战 ——从树莓派到智能家居控制面板,打造工业级交互终端
android·前端·flutter·ios·智能家居·跨平台
Digitally1 小时前
如何轻松将视频从安卓设备传输到电脑?
android·电脑·音视频
Dola_Pan1 小时前
Android四大组件通讯指南:Kotlin版组件茶话会
android·开发语言·kotlin
hopetomorrow2 小时前
学习路之PHP--webman安装及使用
android·学习·php
aningxiaoxixi2 小时前
android 之 Tombstone
android
☞无能盖世♛逞何英雄☜2 小时前
SSRF漏洞
安全·web安全
移动开发者1号3 小时前
应用启动性能优化与黑白屏处理方案
android·kotlin