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...

相关推荐
柯南二号3 分钟前
【大前端】【Android】把 Activity 重构成 MVVM 的对比示例
android·状态模式
某空m16 分钟前
【Android】Glide的缓存机制
android·缓存·glide
清风66666622 分钟前
基于单片机的井盖安全监测与报警上位机监测系统设计
单片机·嵌入式硬件·安全·毕业设计·课程设计·期末大作业
米羊12125 分钟前
安全交付 (上)
安全
某空m26 分钟前
【Android】Glide的使用
android·glide
QING61826 分钟前
Jetpack Compose 中的 ViewModel 作用域管理 —— 新手指南
android·kotlin·android jetpack
鹏多多33 分钟前
flutter-使用EventBus实现组件间数据通信
android·前端·flutter
lubiii_1 小时前
网络安全需掌握的专业术语解析
安全·web安全·网络安全
计算机毕设指导61 小时前
基于微信小程序的网络安全知识科普平台系统【源码文末联系】
java·spring boot·安全·web安全·微信小程序·小程序·tomcat
Bruce_Liuxiaowei1 小时前
Windows系统sc命令:系统安全防护的实用工具
windows·安全·系统安全