版权归作者所有,如有转发,请注明文章出处: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 与 主动调用两类):
-
Execute 脱壳点得到的 dex (*_dex_file_execute.dex)和 dex 中的所有类列表( txt 文件)
-
主动调用时 dump 得到的 dex (*_dex_file.dex)和此时 dex 中的所有类列表,以及该 dex 中所有函数的 CodeItem( bin 文件)
