攻防 FART 脱壳:特征检测识别 + 对抗绕过全解析
版权归作者所有,如有转发,请注明文章出处: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
如何实现类似的功能?
首先找到 FART 的特征
FART 特征检测识别
识别到 FART 特征 kill 进程,没有识别到正常进入 app
FART特征
FART 有什么特征?通过查看 FART 源码可以找到。
FART 开源地址:https://github.com/CYRUS-STUDIO/FART
关于 FART 的详细介绍参考下面的文章:
ActivityThread
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
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 | 设备编号 | 表示该文件所在设备的主/次设备号 |
1834 | inode 号 | 文件在设备上的 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;
}
效果如下:
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 特征。
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 特征。
反 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 层重命名:
原方法名 | 替代方法名 |
---|---|
getClassField | resolveDeclaredField |
getClassFieldObject | extractFieldValue |
invokeStaticMethod | invokeStaticByName |
getFieldOjbect | getInstanceFieldValue |
getClassloader | obtainAppClassLoader |
loadClassAndInvoke | dispatchClassTask |
fart | startCodeInspection |
fartwithClassloader | startCodeInspectionWithCL |
fartthread | launchInspectorThread |
dumpMethodCode | nativeDumpCode |
Native 层函数重命名:
原函数名 | 替代函数名 |
---|---|
codeitem_end | getDexCodeItemEnd |
base64_encode | encodeBase64Buffer |
dumpDexFileByExecute | traceDexExecution |
dumpArtMethod | traceMethodCode |
myfartInvoke | callNativeMethodInspector |
DexFile_dumpMethodCode | DexFile_nativeDumpCode |
jobject2ArtMethod | convertToArtMethodPtr |
记得相关函数调用也要做修改。
自动化脚本
但是一个个修改太麻烦了,写个脚本自动修改(可以自定义 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
替换完成。
同时也可以用来修改 frida_fart 的 js 源码
D:\Projects\FART\rename_fart_symbols.py D:\Python\anti-app\frida_fart
参考:使用 Frida 增强 FART:实现更强大的 Android 脱壳能力
重新编译系统
把修改后的 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
成功刷入后重启手机。
测试
可以看到 so 中已经检测不出 FART 特征
dex 相关文件也没有检测出 FART 特征
使用 frida_fart 发起主动调用
成功脱壳
测试某视频 app 的 ajm 壳成功脱壳,能正常进入 app 没有被 kill 掉
禁止加载 cdex
另外,dump 下来的 dex 文件头有可能是 cdex001,cdex 文件是不可以直接通过 dex 反编译工具反编译的
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 去反编译了。
完整源码
开源地址:
相关文章: