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

FART 对抗

某视频 app 的壳在启动的时候会检测 FART 特征,日志输出如下:

2025-05-29 02:16:25.612  2557-2557  ActivityThread          cn.cntv                              E  go into handleBindApplication
2025-05-29 02:16:25.630  2557-2557  cn.cntv                 cn.cntv                              I  The ClassLoaderContext is a special shared library.
2025-05-29 02:16:25.807  1512-17245 ActivityManager         system_process                       I  Process cn.cntv (pid 2557) has died: fore TOP 
2025-05-29 02:16:25.875  1512-1588  ActivityManager         system_process                       I  Start proc 2628:cn.cntv/u0a140 for top-activity {cn.cntv/com.cctv.mcctv.ui.activity.SplashActivity}
2025-05-29 02:16:25.932  2628-2628  ActivityThread          cn.cntv                              E  go into handleBindApplication
2025-05-29 02:16:25.945  2628-2628  cn.cntv                 cn.cntv                              I  The ClassLoaderContext is a special shared library.
2025-05-29 02:16:26.113  1512-4110  ActivityManager         system_process                       I  Process cn.cntv (pid 2628) has died: fore TOP 
2025-05-29 02:16:26.179  1512-1588  ActivityManager         system_process                       I  Start proc 2716:cn.cntv/u0a140 for top-activity {cn.cntv/com.cctv.mcctv.ui.activity.SplashActivity}
2025-05-29 02:16:26.233  2716-2716  ActivityThread          cn.cntv                              E  go into handleBindApplication
2025-05-29 02:16:26.245  2716-2716  cn.cntv                 cn.cntv                              I  The ClassLoaderContext is a special shared library.
2025-05-29 02:16:26.291  2716-2716  cn.cntv                 cn.cntv                              W  type=1400 audit(0.0:126069): avc: granted { execute } for path="/data/data/cn.cntv/files/libexec.so" dev="mmcblk0p64" ino=157243 scontext=u:r:untrusted_app:s0:c140,c256,c512,c768 tcontext=u:object_r:app_data_file:s0:c140,c256,c512,c768 tclass=file app=cn.cntv
2025-05-29 02:16:26.304  2716-2716  cn.cntv                 cn.cntv                              W  type=1400 audit(0.0:126070): avc: granted { execute } for path="/data/data/cn.cntv/files/libexecmain.so" dev="mmcblk0p64" ino=157244 scontext=u:r:untrusted_app:s0:c140,c256,c512,c768 tcontext=u:object_r:app_data_file:s0:c140,c256,c512,c768 tclass=file app=cn.cntv
2025-05-29 02:16:26.324  2716-2716  cn.cntv                 cn.cntv                              W  type=1400 audit(0.0:126071): avc: denied { execmod } for path="/apex/com.android.runtime/lib64/libart.so" dev="mmcblk0p61" ino=313 scontext=u:r:untrusted_app:s0:c140,c256,c512,c768 tcontext=u:object_r:system_lib_file:s0 tclass=file permissive=0 app=cn.cntv
2025-05-29 02:16:26.334  2716-2716  cn.cntv                 cn.cntv                              W  type=1400 audit(0.0:126072): avc: denied { execmod } for path="/system/lib64/liblog.so" dev="mmcblk0p61" ino=3229 scontext=u:r:untrusted_app:s0:c140,c256,c512,c768 tcontext=u:object_r:system_lib_file:s0 tclass=file permissive=0 app=cn.cntv
2025-05-29 02:16:26.385  1512-17245 ActivityManager         system_process                       I  Process cn.cntv (pid 2716) has died: fore TOP 
2025-05-29 02:16:26.441  1512-1588  ActivityManager         system_process                       I  Start proc 2807:cn.cntv/u0a140 for top-activity {cn.cntv/com.cctv.mcctv.ui.activity.SplashActivity}
2025-05-29 02:16:26.491  2807-2807  ActivityThread          cn.cntv                              E  go into handleBindApplication
2025-05-29 02:16:26.506  2807-2807  cn.cntv                 cn.cntv                              I  The ClassLoaderContext is a special shared library.
2025-05-29 02:16:26.682  1512-17245 ActivityManager         system_process                       I  Process cn.cntv (pid 2807) has died: fore TOP 
2025-05-29 02:16:26.731  1512-1588  ActivityManager         system_process                       I  Start proc 2872:cn.cntv/u0a140 for top-activity {cn.cntv/com.cctv.mcctv.ui.activity.SplashActivity}
2025-05-29 02:16:26.783  2872-2872  ActivityThread          cn.cntv                              E  go into handleBindApplication

使用的是 ajm 的壳,App 加载 so 文件,主动检测 FART 特征

avc: granted { execute } for path="/data/data/cn.cntv/files/libexec.so"
avc: granted { execute } for path="/data/data/cn.cntv/files/libexecmain.so"

一旦发现异常就触发崩溃(kill)

Process cn.cntv (pid 2628) has died: fore TOP 

如何实现类似的功能?

  1. 首先找到 FART 的特征

  2. FART 特征检测识别

  3. 识别到 FART 特征 kill 进程,没有识别到正常进入 app

FART特征

FART 有什么特征?通过查看 FART 源码可以找到。

FART 开源地址:https://github.com/CYRUS-STUDIO/FART

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

ActivityThread

源码:https://github.com/CYRUS-STUDIO/FART/blob/master/fart10/frameworks/base/core/java/android/app/ActivityThread.java

FART 在 ActivityThread 新增了以下方法,这些都可以作为 FART 的特征

public static Field getClassField(ClassLoader classloader, String class_name, String filedName)
public static Object getClassFieldObject(ClassLoader classloader, String class_name, Object obj, String filedName)
public static Object invokeStaticMethod(String class_name, String method_name, Class[] pareTyple, Object[] pareVaules)
public static Object getFieldOjbect(String class_name, Object obj, String filedName)
public static ClassLoader getClassloader()
public static void loadClassAndInvoke(ClassLoader appClassloader, String eachclassname, Method dumpMethodCode_method) 
public static void fart() 
public static void fartwithClassloader(ClassLoader appClassloader)
public static void fartthread()

DexFile

源码:https://github.com/CYRUS-STUDIO/FART/blob/master/fart10/libcore/dalvik/src/main/java/dalvik/system/DexFile.java

FART 在 DexFile 新增了 dumpMethodCode 方法同样也可以作为 FART 的特征

private static native void dumpMethodCode(Object m);

art_method.cc

FART 在 art/runtime/art_method.cc 中新增以下方法

uint8_t* codeitem_end(const uint8_t **pData)
extern "C" char *base64_encode(char *str,long str_len,long* outlen)
extern "C" void dumpDexFileByExecute(ArtMethod* artmethod)
extern "C" void dumpArtMethod(ArtMethod* artmethod)
extern "C" void myfartInvoke(ArtMethod* artmethod)

dalvik_system_DexFile.cc

FART 在 art/runtime/native/dalvik_system_DexFile.cc 中新增了以下方法

static void DexFile_dumpMethodCode(JNIEnv* env, jclass,jobject method)

java_lang_reflect_Method.cc

FART 在 art/runtime/native/java_lang_reflect_Method.cc 中新增了以下方法

extern "C" ArtMethod* jobject2ArtMethod(JNIEnv* env, jobject javaMethod)

上面这些都可以作为 FART 特征。

FART 特征有的在 native 层,最终编译成 so 文件;有的在 java 层,最终编译成 dex 相关文件。

如何找到这些 so 和 dex 相关文件?

/proc/self/maps

/proc/self/maps 是 Linux(含 Android)系统中一个非常重要的伪文件,它提供了当前进程内存映射(memory mapping)信息,是分析当前进程加载了哪些资源的重要窗口。

包括:

  • 加载的 .so 动态库

  • 加载的 .dex 文件(包含 ODEX / VDEX)

  • 映射的 Java 堆、native 堆、stack 等

  • 匿名 mmap 内存区域

  • JIT 编译生成的代码段

  • 映射的 /system/, /data/, /apex/, /dev/ashmem 等文件

比如,进入 adb shell ,通过下面命令读取包名 com.cyrus.example 下的 maps 文件

cat /proc/$(pidof com.cyrus.example)/maps

输出结果如下:

12c00000-12c80000 rw-p 00000000 00:00 0                                  [anon:dalvik-main space (region space)]
12c80000-132c0000 ---p 00000000 00:00 0                                  [anon:dalvik-main space (region space)]
132c0000-13580000 rw-p 00000000 00:00 0                                  [anon:dalvik-main space (region space)]
13580000-26280000 ---p 00000000 00:00 0                                  [anon:dalvik-main space (region space)]
26280000-2a940000 ---p 00000000 00:00 0                                  [anon:dalvik-main space (region space)]
2a940000-2a980000 ---p 00000000 00:00 0                                  [anon:dalvik-main space (region space)]
2a980000-2a9c0000 ---p 00000000 00:00 0                                  [anon:dalvik-main space (region space)]
2a9c0000-2aac0000 ---p 00000000 00:00 0                                  [anon:dalvik-main space (region space)]
2aac0000-2ab80000 ---p 00000000 00:00 0                                  [anon:dalvik-main space (region space)]
2ab80000-2abc0000 ---p 00000000 00:00 0                                  [anon:dalvik-main space (region space)]
2abc0000-2ac00000 rw-p 00000000 00:00 0                                  [anon:dalvik-main space (region space)]
708d5000-70b5a000 rw-p 00000000 103:1d 1863                              /system/framework/arm64/boot.art
70b5a000-70c4a000 rw-p 00000000 103:1d 1833                              /system/framework/arm64/boot-core-libart.art
70c4a000-70c80000 rw-p 00000000 103:1d 1848                              /system/framework/arm64/boot-okhttp.art
70c80000-70cc1000 rw-p 00000000 103:1d 1830                              /system/framework/arm64/boot-bouncycastle.art
70cc1000-70cd1000 rw-p 00000000 103:1d 1827                              /system/framework/arm64/boot-apache-xml.art
70cd1000-71595000 rw-p 00000000 103:1d 1839                              /system/framework/arm64/boot-framework.art
71595000-715c9000 rw-p 00000000 103:1d 1836                              /system/framework/arm64/boot-ext.art
715c9000-716c1000 rw-p 00000000 103:1d 1854                              /system/framework/arm64/boot-telephony-common.art
716c1000-716cf000 rw-p 00000000 103:1d 1860                              /system/framework/arm64/boot-voip-common.art
716cf000-716e4000 rw-p 00000000 103:1d 1842                              /system/framework/arm64/boot-ims-common.art
716e4000-716e7000 rw-p 00000000 103:1d 1824                              /system/framework/arm64/boot-android.test.base.art
716e7000-716e9000 rw-p 00000000 103:1d 1851                              /system/framework/arm64/boot-org.ifaa.android.manager.art
716e9000-716f0000 rw-p 00000000 103:1d 1845                              /system/framework/arm64/boot-ims-ext-common_system.art
716f0000-716f4000 rw-p 00000000 103:1d 1857                              /system/framework/arm64/boot-telephony-ext.art
716f4000-716fc000 rw-p 00000000 103:1d 1821                              /system/framework/arm64/boot-WfdCommon.art
716fc000-717b2000 r--p 00000000 103:1d 1864                              /system/framework/arm64/boot.oat
717b2000-71a4d000 r-xp 000b6000 103:1d 1864                              /system/framework/arm64/boot.oat
71a4d000-71a4e000 rw-p 00000000 00:00 0                                  [anon:.bss]
71a4e000-71a50000 r--s 00000000 103:1d 1882                              /system/framework/boot.vdex
71a50000-71a51000 r--p 00351000 103:1d 1864                              /system/framework/arm64/boot.oat
71a51000-71a52000 rw-p 00352000 103:1d 1864                              /system/framework/arm64/boot.oat
71a52000-71a9b000 r--p 00000000 103:1d 1834                              /system/framework/arm64/boot-core-libart.oat
71a9b000-71ba3000 r-xp 00049000 103:1d 1834                              /system/framework/arm64/boot-core-libart.oat
...

比如:

71a9b000-71ba3000 r-xp 00049000 103:1d 1834  /system/framework/arm64/boot-core-libart.oat

字段解析如下:

字段示例值含义
71a9b000-71ba3000起始地址 - 结束地址表示这段内存从 0x71a9b000 映射到 0x71ba3000(大约 1MB)
r-xp权限r = 可读,x = 可执行,p = 私有
00049000文件偏移映射文件时从 offset=0x49000 开始
103:1d设备编号表示该文件所在设备的主/次设备号
1834inode 号文件在设备上的 inode 编号
/system/framework/arm64/boot-core-libart.oat文件路径表示映射的文件路径,来源是系统的 OAT 文件

所有我们可以通过读取 /proc/self/maps 得到当前 app 加载的所有资源文件,实现如下:

extern "C"
JNIEXPORT jobjectArray JNICALL
Java_com_cyrus_example_fart_AntiFART_listLoadedFiles(JNIEnv *env, jclass) {
    std::ifstream maps("/proc/self/maps");
    std::string line;
    std::vector<std::string> paths;

    while (std::getline(maps, line)) {
        std::size_t pathPos = line.find('/');
        if (pathPos != std::string::npos) {
            std::string path = line.substr(pathPos);
            if (std::find(paths.begin(), paths.end(), path) == paths.end()) {
                paths.push_back(path);
            }
        }
    }

    jclass stringClass = env->FindClass("java/lang/String");
    jobjectArray result = env->NewObjectArray(paths.size(), stringClass, nullptr);
    for (size_t i = 0; i < paths.size(); ++i) {
        env->SetObjectArrayElement(result, i, env->NewStringUTF(paths[i].c_str()));
    }

    return result;
}

效果如下:

word/media/image1.png

so 文件 FART 特征检测

对于 FART 在 C/C++ 层添加的函数特征码检测。

通过检测 /proc/self/maps下的加载 so库列表得到各个库文件绝对路径

// 读取 /proc/self/maps 获取加载的 .so 路径
std::set<std::string> get_loaded_so_paths() {
    std::set<std::string> so_paths;
    std::ifstream maps("/proc/self/maps");
    std::string line;
    std::regex so_regex(".+\\.so(\\s|$)");

    while (std::getline(maps, line)) {
        std::size_t path_pos = line.find('/');
        if (path_pos != std::string::npos) {
            std::string path = line.substr(path_pos);
            if (std::regex_search(path, so_regex)) {
                so_paths.insert(path);
            }
        }
    }
    return so_paths;
}

再通过 fopen 函数将 so 库的内容以16进制读进来放在内存,采用字符串模糊查找来检测是否命中黑名单中的方法特征码。

// so 黑名单函数特征
std::vector<std::string> so_symbols_blacklist = {
        "dumpDexFileByExecute",
        "dumpArtMethod",
        "myfartInvoke",
        "DexFile_dumpMethodCode"
};

// 读取文件内容为字符串
std::string read_file_content(const std::string &path) {
    FILE *file = fopen(path.c_str(), "rb");
    if (!file) {
        LOGI("Failed to open: %s", path.c_str());
        return "";
    }

    fseek(file, 0, SEEK_END);
    long size = ftell(file);
    rewind(file);

    std::string buffer(size, 0);
    fread(&buffer[0], 1, size, file);
    fclose(file);

    return buffer;
}

// 单词边界检查
bool is_word_boundary(char ch) {
    return !std::isalnum(static_cast<unsigned char>(ch)) && ch != '_';
}

// 返回匹配到的特征列表
std::vector<std::string> get_matched_signatures(const std::string &content, const std::vector<std::string> &patterns) {
    std::vector<std::string> matched;
    for (const auto &pattern : patterns) {
        size_t pos = content.find(pattern);
        if (pos != std::string::npos) {
            // 类似 DexFile_dumpMethodCode 这种,带 _ 的不需要做单词边界检查
            if (pattern.find('_') != std::string::npos) {
                matched.push_back(pattern);
            }else{
                // 单词边界检查
                // 这样就不会匹配 farther、himmelfart,但可以匹配像 void fart()、"fart"、 call fart 等形式。
                char prev = (pos == 0) ? '\0' : content[pos - 1];
                char next = (pos + pattern.length() < content.size()) ? content[pos + pattern.length()] : '\0';

                if (is_word_boundary(prev) && is_word_boundary(next)) {
                    matched.push_back(pattern);
                }
            }
        }
    }
    return matched;
}

// JNI 方法:检测已加载 .so 中是否包含黑名单符号
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_com_cyrus_example_fart_AntiFART_detectFartInLoadedSO(JNIEnv *env, jclass clazz) {
    std::vector<std::string> detected_logs;
    auto so_paths = get_loaded_so_paths();

    for (const auto &path: so_paths) {
        std::string content = read_file_content(path);
        if (!content.empty()) {
            std::vector<std::string> matched = get_matched_signatures(content, so_symbols_blacklist);
            if (!matched.empty()) {
                std::ostringstream oss;
                oss << "[FART DETECTED] " << path << " => ";
                for (size_t i = 0; i < matched.size(); ++i) {
                    oss << matched[i];
                    if (i != matched.size() - 1) oss << ", ";
                }
                LOGI("%s", oss.str().c_str());
                detected_logs.push_back(oss.str());
            }
        }
    }

    jclass stringClass = env->FindClass("java/lang/String");
    jobjectArray result = env->NewObjectArray(detected_logs.size(), stringClass, nullptr);
    for (int i = 0; i < detected_logs.size(); ++i) {
        env->SetObjectArrayElement(result, i, env->NewStringUTF(detected_logs[i].c_str()));
    }

    return result;
}

可以看到在 libart.so 中命中了多个 FART 特征。

word/media/image2.png

dex 文件 FART 特征检测

对于 FART 在 Java 层添加的方法特征码检测也是类似。

但是 dex 相关文件格式有多种,包括:

  • .dex 文件(原始 dex)

  • .odex(优化过的 dex)

  • .vdex(Verified DEX)

  • .art(预编译的 ART 文件)

  • 以及 .jar、.apk 中可能包含 dex 文件的路径

读取 /proc/self/maps 获取加载的 dex 或 dex 相关文件路径

// 读取 /proc/self/maps 获取加载的 dex 或 dex 相关文件路径
std::set<std::string> get_loaded_dex_paths() {
    std::set<std::string> dex_paths;
    std::ifstream maps("/proc/self/maps");
    std::string line;

    // 匹配 dex、odex、vdex、art、apk、jar 文件
    std::regex dex_regex(R"((\.dex|\.odex|\.vdex|\.art|\.apk|\.jar)(\s|$))");

    while (std::getline(maps, line)) {
        std::size_t path_pos = line.find('/');
        if (path_pos != std::string::npos) {
            std::string path = line.substr(path_pos);
            if (std::regex_search(path, dex_regex)) {
                dex_paths.insert(path);
            }
        }
    }
    return dex_paths;
}

再通过 fopen 函数将 dex 相关文件的内容以16进制读进来放在内存,采用字符串模糊查找来检测是否命中黑名单中的方法特征码。

// dex 黑名单函数特征
const std::vector<std::string> dex_method_blacklist = {
        "loadClassAndInvoke",
        "fart",
        "fartwithClassloader",
        "fartthread"
};

// JNI 方法:检测已加载 dex 中是否包含黑名单符号
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_com_cyrus_example_fart_AntiFART_detectFartInLoadedDex(JNIEnv *env, jclass clazz) {
    std::vector<std::string> detected_logs;
    auto dex_paths = get_loaded_dex_paths();

    for (const auto &path: dex_paths) {
        std::string content = read_file_content(path);
        if (!content.empty()) {
            std::vector<std::string> matched = get_matched_signatures(content, dex_method_blacklist);
            if (!matched.empty()) {
                std::ostringstream oss;
                oss << "[FART DETECTED] " << path << " => ";
                for (size_t i = 0; i < matched.size(); ++i) {
                    oss << matched[i];
                    if (i != matched.size() - 1) oss << ", ";
                }
                LOGI("%s", oss.str().c_str());
                detected_logs.push_back(oss.str());
            }
        }
    }

    jclass stringClass = env->FindClass("java/lang/String");
    jobjectArray result = env->NewObjectArray(detected_logs.size(), stringClass, nullptr);
    for (int i = 0; i < detected_logs.size(); ++i) {
        env->SetObjectArrayElement(result, i, env->NewStringUTF(detected_logs[i].c_str()));
    }

    return result;
}

可以看到在 framework.jar 中检测到了多个 FART 特征。

word/media/image3.png

反 FART 对抗

绕过 FART 对抗只需要定制个性化的 ROM,抹除这些 FART 特征就好了。

抹除 FART 特征

比如把这些 FART 中默认的方法名重命名一下就好了。

public static Field getClassField(ClassLoader classloader, String class_name, String filedName)
public static Object getClassFieldObject(ClassLoader classloader, String class_name, Object obj, String filedName)
public static Object invokeStaticMethod(String class_name, String method_name, Class[] pareTyple, Object[] pareVaules)
public static Object getFieldOjbect(String class_name, Object obj, String filedName)
public static ClassLoader getClassloader()
public static void loadClassAndInvoke(ClassLoader appClassloader, String eachclassname, Method dumpMethodCode_method) 
public static void fart() 
public static void fartwithClassloader(ClassLoader appClassloader)
public static void fartthread()
private static native void dumpMethodCode(Object m);

uint8_t* codeitem_end(const uint8_t **pData)
extern "C" char *base64_encode(char *str,long str_len,long* outlen)
extern "C" void dumpDexFileByExecute(ArtMethod* artmethod)
extern "C" void dumpArtMethod(ArtMethod* artmethod)
extern "C" void myfartInvoke(ArtMethod* artmethod)

static void DexFile_dumpMethodCode(JNIEnv* env, jclass,jobject method)

extern "C" ArtMethod* jobject2ArtMethod(JNIEnv* env, jobject javaMethod)

假设把函数重命名如下:

Java 层重命名:

原方法名替代方法名
getClassFieldresolveDeclaredField
getClassFieldObjectextractFieldValue
invokeStaticMethodinvokeStaticByName
getFieldOjbectgetInstanceFieldValue
getClassloaderobtainAppClassLoader
loadClassAndInvokedispatchClassTask
fartstartCodeInspection
fartwithClassloaderstartCodeInspectionWithCL
fartthreadlaunchInspectorThread
dumpMethodCodenativeDumpCode

Native 层函数重命名:

原函数名替代函数名
codeitem_endgetDexCodeItemEnd
base64_encodeencodeBase64Buffer
dumpDexFileByExecutetraceDexExecution
dumpArtMethodtraceMethodCode
myfartInvokecallNativeMethodInspector
DexFile_dumpMethodCodeDexFile_nativeDumpCode
jobject2ArtMethodconvertToArtMethodPtr

记得相关函数调用也要做修改。

自动化脚本

但是一个个修改太麻烦了,写个脚本自动修改(可以自定义 RENAME_MAP 中的值去定制一个只属于自己的 FART ROM,这样就不容易被检测):

import os
import re

# 敏感方法名及其替代名映射表
RENAME_MAP = {
    "getClassField": "resolveDeclaredField",
    "getClassFieldObject": "extractFieldValue",
    "invokeStaticMethod": "invokeStaticByName",
    "getFieldOjbect": "getInstanceFieldValue",
    "getClassloader": "obtainAppClassLoader",
    "loadClassAndInvoke": "dispatchClassTask",
    "fart\\b": "startCodeInspection",
    "fartwithClassloader": "startCodeInspectionWithCL",
    "fartthread": "launchInspectorThread",
    "dumpMethodCode": "nativeDumpCode",
    "codeitem_end": "getDexCodeItemEnd",
    "base64_encode": "encodeBase64Buffer",
    "dumpDexFileByExecute": "traceDexExecution",
    "dumpArtMethod": "traceMethodCode",
    "myfartInvoke": "callNativeMethodInspector",
    "DexFile_dumpMethodCode": "DexFile_nativeDumpCode",
    "jobject2ArtMethod": "convertToArtMethodPtr"
}

SOURCE_SUFFIX = (".java", ".kt", ".cc", ".c", ".cpp", ".h", ".js")

def replace_in_file(file_path):
    try:
        with open(file_path, "r", encoding="utf-8") as f:
            content = f.read()

        original_content = content

        for old, new in RENAME_MAP.items():
            content = re.sub(r'\b' + old + r'\b', new, content)

        if content != original_content:
            with open(file_path, "w", encoding="utf-8") as f:
                f.write(content)
            print(f"[UPDATED] {file_path}")
        else:
            print(f"[SKIPPED] {file_path}")

    except Exception as e:
        print(f"[ERROR] {file_path}: {e}")

def scan_directory(root_dir):
    for dirpath, _, filenames in os.walk(root_dir):
        for file in filenames:
            if file.endswith(SOURCE_SUFFIX):
                replace_in_file(os.path.join(dirpath, file))

if __name__ == "__main__":
    import sys
    if len(sys.argv) < 2:
        print("Usage: python rename_fart_symbols.py <source_directory>")
        sys.exit(1)

    scan_directory(sys.argv[1])

    input("Press Enter to exit...")

执行脚本:

D:\Projects\FART\rename_fart_symbols.py D:\Projects\FART\fart10

替换完成。

word/media/image4.png

同时也可以用来修改 frida_fart 的 js 源码

D:\Projects\FART\rename_fart_symbols.py  D:\Python\anti-app\frida_fart

参考:使用 Frida 增强 FART:实现更强大的 Android 脱壳能力

word/media/image5.png

重新编译系统

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

# 初始化编译环境
source build/envsetup.sh

# 设置编译目标
breakfast wayne

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

# 开始编译
brunch wayne

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

生成 OTA 包

./sign_ota_wayne.sh

编译完成

word/media/image6.png

刷机

由于我这里是在 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

word/media/image7.png

开始刷机

adb sideload E:\lineageos\xiaomi6x_wayne_lineageos-17.1_signed-ota_update_fart_cyrus.zip

成功刷入后重启手机。

测试

可以看到 so 中已经检测不出 FART 特征

word/media/image8.png

dex 相关文件也没有检测出 FART 特征

word/media/image9.png

使用 frida_fart 发起主动调用

word/media/image10.png

成功脱壳

word/media/image11.png

测试某视频 app 的 ajm 壳成功脱壳,能正常进入 app 没有被 kill 掉

word/media/image12.png

禁止加载 cdex

另外,dump 下来的 dex 文件头有可能是 cdex001,cdex 文件是不可以直接通过 dex 反编译工具反编译的

word/media/image13.png

Android 9 引入 CompactDex(.cdex,magic 为 cdex001),是 DEX 的压缩优化版本,导致 dump 后无法直接反编译。

优化后的 dex/cdex 通常存放在:

/data/app/package_name/oat/arm64/base.odex
/data/app/package_name/oat/arm64/base.vdex

在 Android 9(Pie)中,APP 的 .cdex 文件 是由 dex2oat 优化生成的,通常以 odex, vdex 或直接优化后的 .art 文件形式存在。

进入 adb shell 找到 目标app 存放 oat 文件的路径并删除所有 oat 文件

wayne:/sdcard/Android/data/com.cyrus.example/dump_dex # cd /data/app
wayne:/data/app # ls
com.android.chrome-b1d3YEy1eVrwwjPOa1oq5A==       com.iflytek.inputmethod-s1r9JFv0-eKNskzHyrh_vQ==
com.cyrus.example-uIsySv7lFm21qMVPnPJ-pw==        com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==
com.cyrus.example.plugin-YsXrxPvfWYdsWHxFKjcusw== com.tencent.mm-ql7ajyK9JqKXli5pgu88nw==
com.cyrus.example.test-R06ZNyf5doqJFOcZ6EaYHQ==   com.xingin.xhs-HeYr1dfB-rU7NjxJiLiDeg==
wayne:/data/app # cd com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==
wayne:/data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ== # ls
base.apk lib oat
wayne:/data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ== # cd oat
wayne:/data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/oat # ls
arm64
wayne:/data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/oat # cd arm64/
wayne:/data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/oat/arm64 # ls
base.art base.odex base.vdex
wayne:/data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/oat/arm64 # rm -rf *

再重新 dump dex,这时可以看到 dump 下来的 dex 魔数都是 dex 039 / dex 035 (标准 Dex 文件的魔数)不是 cdex001,可以直接用 jadx 去反编译了。

word/media/image14.png

完整源码

开源地址:

相关文章: