FART 精准脱壳:通过配置文件控制脱壳节奏与范围
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/
前言
由于 FART 默认会对所有 app 进行脱壳,每次 app 启动都会自动脱壳,而且会对 app 中所有类发起主动调用,这样效率比较慢,遇到 FART 对抗类也不能选择性跳过。
如何通过一份简单的配置文件,实现对 FART 脱壳过程的精准控制:包括是否启用脱壳、延迟时间、需要主动调用的类列表、排除类规则等。提高脱壳效率,也可以避开一些垃圾类(FART 对抗类)的调用。
关于 FART 的详细介绍参考下面的文章:
-
FART 自动化脱壳框架简介与脱壳点的选择
-
FART 主动调用组件设计和源码分析
-
移植 FART 到 Android 10 实现自动化脱壳
-
FART 自动化脱壳框架一些 bug 修复记录
-
使用 Frida 增强 FART:实现更强大的 Android 脱壳能力
-
攻防 FART 脱壳:特征检测识别 + 对抗绕过全解析
通过配置文件控制脱壳节奏与范围
例如,配置项如下:
# 是否开启脱壳功能(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 脱壳框架实现精细化控制脱壳流程。
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 * supportif (!pattern.contains("*")) {return Pattern.quote(pattern);}return pattern.replace(".", "\\.").replace("*", ".*");}
}
2. 脱壳线程实现修改
在 launchInspectorThread 方法里:
-
调用 init 初始化配置
-
通过 Cyrus.isDumpEnabled() 判断当前 app 是否需要脱壳
-
通过 Cyrus.getSleepTimeMs() 方法获取配置的休眠时间
public static void launchInspectorThread(Context context) {new Thread(new Runnable() {@Overridepublic 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 中有可能发生多次调用。
private void handleBindApplication(AppBindData data) {...//addlaunchInspectorThread(appContext);
}
3. 主动调用范围过滤
在 dispatchClassTask 中通过 Cyrus.shouldForceCall(eachclassname) 判断是否需要加载并调用当前类
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 系统里面,重新编译。
# 初始化编译环境
source build/envsetup.sh# 设置编译目标
breakfast wayne# 回到 Android 源码树的根目录
croot# 开始编译
brunch wayne
如何编译 FART ROM 参考这篇文章:移植 FART 到 Android 10 实现自动化脱壳
生成 OTA 包
./sign_ota_wayne.sh
编译完成
刷机
由于我这里是在 WSL 中编译,先把 ota 文件 copy 到 windwos 目录下
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
开始刷机
adb sideload E:\lineageos\xiaomi6x_wayne_lineageos-17.1_signed-ota_update_fart_cyrus.zip
成功刷入后重启手机。
脱壳配置
1. 获取 app 包名
你可以使用下面的 adb 命令来获取当前前台 app 的包名
Mac/Linux:
adb shell dumpsys window | grep -E 'mCurrentFocus|mFocusedApp'
Windows:
adb shell dumpsys window | Select-String 'mCurrentFocus|mFocusedApp'
示例输出:
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 包下的类
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.*… 中的类
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。
开始脱壳
清空日志缓存
adb logcat -c
输出日志到文件
adb logcat -v time > logcat.txt
打开 app 等待 60 秒开始自动脱壳(比如:只脱壳 ff 包下的类)。
等输出 run over 就是脱壳完成。
脱壳完成
FART 脱壳结束得到的文件列表(分 Execute 与 主动调用两类):
-
Execute 脱壳点得到的 dex (*_dex_file_execute.dex)和 dex 中的所有类列表( txt 文件)
-
主动调用时 dump 得到的 dex (*_dex_file.dex)和此时 dex 中的所有类列表,以及该 dex 中所有函数的 CodeItem( bin 文件)
完整源码
开源地址:https://github.com/CYRUS-STUDIO/FART