文章总结: 本文分析了AJM壳对抗FART脱壳技术的机制,通过检测FART在ActivityThread、DexFile和art_method.cc中新增的方法特征实现进程终止。文章详细列举了FART的特征代码位置,并提出了特征识别与进程防护的三步实施方案,为移动安全防护提供了具体的技术参考。 综合评分: 85 文章分类: 移动安全,逆向分析,恶意软件,渗透测试,代码审计
FART脱壳:实现AJM壳级别的对抗功能+绕过全解析
哆啦安全
2025年10月2日%2017:16 四川
在小说阅读器读本章
去阅读
以下文章来源于CYRUS%20STUDIO ,作者CYRUS%20STUDIO
CYRUS%20STUDIO .
专注%20Android%20逆向与移动安全,分享加壳脱壳、反调试、抓包、算法还原、%20Frida、Unidbg、IDA、Android%20ROM%20定制等方向的实战内容。
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/
实现类似%20AJM%20壳的%20FART%20对抗功能
某视频%20app%20的壳在启动的时候会检测%20FART%20特征,日志输出如下:
2025-05-29%2002:16:25.612%20 2557-2557%20 ActivityThread%20 %20 %20 %20 %20 cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 E%20 go%20into%20handleBindApplication
2025-05-29%2002:16:25.630%20 2557-2557%20 cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 I%20 The%20ClassLoaderContext%20is%20a%20special%20shared%20library.
2025-05-29%2002:16:25.807%20 1512-17245%20ActivityManager%20 %20 %20 %20 %20system_process%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20I%20 Process%20cn.cntv%20(pid%202557)%20has%20died:%20fore%20TOP
2025-05-29%2002:16:25.875%20 1512-1588%20 ActivityManager%20 %20 %20 %20 %20system_process%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20I%20 Start%20proc%202628:cn.cntv/u0a140%20for%20top-activity%20{cn.cntv/com.cctv.mcctv.ui.activity.SplashActivity}
2025-05-29%2002:16:25.932%20 2628-2628%20 ActivityThread%20 %20 %20 %20 %20 cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 E%20 go%20into%20handleBindApplication
2025-05-29%2002:16:25.945%20 2628-2628%20 cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 I%20 The%20ClassLoaderContext%20is%20a%20special%20shared%20library.
2025-05-29%2002:16:26.113%20 1512-4110%20 ActivityManager%20 %20 %20 %20 %20system_process%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20I%20 Process%20cn.cntv%20(pid%202628)%20has%20died:%20fore%20TOP
2025-05-29%2002:16:26.179%20 1512-1588%20 ActivityManager%20 %20 %20 %20 %20system_process%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20I%20 Start%20proc%202716:cn.cntv/u0a140%20for%20top-activity%20{cn.cntv/com.cctv.mcctv.ui.activity.SplashActivity}
2025-05-29%2002:16:26.233%20 2716-2716%20 ActivityThread%20 %20 %20 %20 %20 cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 E%20 go%20into%20handleBindApplication
2025-05-29%2002:16:26.245%20 2716-2716%20 cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 I%20 The%20ClassLoaderContext%20is%20a%20special%20shared%20library.
2025-05-29%2002:16:26.291%20 2716-2716%20 cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 W%20 type=1400%20audit(0.0:126069):%20avc:%20granted%20{%20execute%20}%20for%20path="/data/data/cn.cntv/files/libexec.so"%20dev="mmcblk0p64"%20ino=157243%20scontext=u:r:untrusted_app:s0:c140,c256,c512,c768%20tcontext=u:object_r:app_data_file:s0:c140,c256,c512,c768%20tclass=file%20app=cn.cntv
2025-05-29%2002:16:26.304%20 2716-2716%20 cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 W%20 type=1400%20audit(0.0:126070):%20avc:%20granted%20{%20execute%20}%20for%20path="/data/data/cn.cntv/files/libexecmain.so"%20dev="mmcblk0p64"%20ino=157244%20scontext=u:r:untrusted_app:s0:c140,c256,c512,c768%20tcontext=u:object_r:app_data_file:s0:c140,c256,c512,c768%20tclass=file%20app=cn.cntv
2025-05-29%2002:16:26.324%20 2716-2716%20 cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 W%20 type=1400%20audit(0.0:126071):%20avc:%20denied%20{%20execmod%20}%20for%20path="/apex/com.android.runtime/lib64/libart.so"%20dev="mmcblk0p61"%20ino=313%20scontext=u:r:untrusted_app:s0:c140,c256,c512,c768%20tcontext=u:object_r:system_lib_file:s0%20tclass=file%20permissive=0%20app=cn.cntv
2025-05-29%2002:16:26.334%20 2716-2716%20 cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 W%20 type=1400%20audit(0.0:126072):%20avc:%20denied%20{%20execmod%20}%20for%20path="/system/lib64/liblog.so"%20dev="mmcblk0p61"%20ino=3229%20scontext=u:r:untrusted_app:s0:c140,c256,c512,c768%20tcontext=u:object_r:system_lib_file:s0%20tclass=file%20permissive=0%20app=cn.cntv
2025-05-29%2002:16:26.385%20 1512-17245%20ActivityManager%20 %20 %20 %20 %20system_process%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20I%20 Process%20cn.cntv%20(pid%202716)%20has%20died:%20fore%20TOP
2025-05-29%2002:16:26.441%20 1512-1588%20 ActivityManager%20 %20 %20 %20 %20system_process%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20I%20 Start%20proc%202807:cn.cntv/u0a140%20for%20top-activity%20{cn.cntv/com.cctv.mcctv.ui.activity.SplashActivity}
2025-05-29%2002:16:26.491%20 2807-2807%20 ActivityThread%20 %20 %20 %20 %20 cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 E%20 go%20into%20handleBindApplication
2025-05-29%2002:16:26.506%20 2807-2807%20 cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 I%20 The%20ClassLoaderContext%20is%20a%20special%20shared%20library.
2025-05-29%2002:16:26.682%20 1512-17245%20ActivityManager%20 %20 %20 %20 %20system_process%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20I%20 Process%20cn.cntv%20(pid%202807)%20has%20died:%20fore%20TOP
2025-05-29%2002:16:26.731%20 1512-1588%20 ActivityManager%20 %20 %20 %20 %20system_process%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20I%20 Start%20proc%202872:cn.cntv/u0a140%20for%20top-activity%20{cn.cntv/com.cctv.mcctv.ui.activity.SplashActivity}
2025-05-29%2002:16:26.783%20 2872-2872%20 ActivityThread%20 %20 %20 %20 %20 cn.cntv%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 E%20 go%20into%20handleBindApplication
使用的是%20AJM%20的壳,App%20加载%20so%20文件,主动检测%20FART%20特征
avc:%20granted%20{%20execute%20}%20for%20path="/data/data/cn.cntv/files/libexec.so"
avc:%20granted%20{%20execute%20}%20for%20path="/data/data/cn.cntv/files/libexecmain.so"
一旦发现异常就触发崩溃(kill)
Process%20cn.cntv%20(pid%202628)%20has%20died:%20fore%20TOP
如何实现类似的功能?
- 1.%20首先找到%20FART%20的特征
- 2.%20FART%20特征检测识别
- 3.%20识别到%20FART%20特征%20kill%20进程,没有识别到正常进入%20app
FART特征
FART%20有什么特征?通过查看%20FART%20源码可以找到。
FART%20开源地址:https://github.com/CYRUS-STUDIO/FART
关于%20FART%20的详细介绍参考下面的文章:
- • 干掉抽取壳!FART%20自动化脱壳框架与%20Execute%20脱壳点解析[1]
- • FART%20主动调用组件深度解析:破解%20ART%20下函数抽取壳的终极武器[2]
- • 一步步带你移植%20FART%20到%20Android%2010,实现自动化脱壳[3]
- • FART%20自动化脱壳框架优化实战:Bug%20修复与代码改进记录[4]
- • Frida%20+%20FART%20联手:解锁更强大的%20Android%20脱壳新姿势[5]
- • FART%20脱壳不再全量!用一份配置文件精准控制节奏与范围[6]
ActivityThread
源码:https://github.com/CYRUS-STUDIO/FART/blob/master/fart10/frameworks/base/core/java/android/app/ActivityThread.java
FART%20在%20ActivityThread%20新增了以下方法,这些都可以作为%20FART%20的特征
public%20static%20Field%20getClassField(ClassLoader%20classloader,%20String%20class_name,%20String%20filedName)
public%20static%20Object%20getClassFieldObject(ClassLoader%20classloader,%20String%20class_name,%20Object%20obj,%20String%20filedName)
public%20static%20Object%20invokeStaticMethod(String%20class_name,%20String%20method_name,%20Class[]%20pareTyple,%20Object[]%20pareVaules)
public%20static%20Object%20getFieldOjbect(String%20class_name,%20Object%20obj,%20String%20filedName)
public%20static%20ClassLoader%20getClassloader()
public%20static%20void%20loadClassAndInvoke(ClassLoader%20appClassloader,%20String%20eachclassname,%20Method%20dumpMethodCode_method)
public%20static%20void%20fart()
public%20static%20void%20fartwithClassloader(ClassLoader%20appClassloader)
public%20static%20void%20fartthread()
DexFile
源码:https://github.com/CYRUS-STUDIO/FART/blob/master/fart10/libcore/dalvik/src/main/java/dalvik/system/DexFile.java
FART%20在%20DexFile%20新增了%20dumpMethodCode%20方法同样也可以作为%20FART%20的特征
private%20static%20native%20void%20dumpMethodCode(Object%20m);
art_method.cc
FART%20在%20art/runtime/art_method.cc%20中新增以下方法
uint8_t*%20codeitem_end(const%20uint8_t%20**pData)
extern%20"C"%20char%20*base64_encode(char%20*str,long%20str_len,long*%20outlen)
extern%20"C"%20void%20dumpDexFileByExecute(ArtMethod*%20artmethod)
extern%20"C"%20void%20dumpArtMethod(ArtMethod*%20artmethod)
extern%20"C"%20void%20myfartInvoke(ArtMethod*%20artmethod)
dalvik_system_DexFile.cc
FART%20在%20art/runtime/native/dalvik_system_DexFile.cc%20中新增了以下方法
static%20void%20DexFile_dumpMethodCode(JNIEnv*%20env,%20jclass,jobject%20method)
java_lang_reflect_Method.cc
FART%20在%20art/runtime/native/java_lang_reflect_Method.cc%20中新增了以下方法
extern%20"C"%20ArtMethod*%20jobject2ArtMethod(JNIEnv*%20env,%20jobject%20javaMethod)
上面这些都可以作为%20FART%20特征。
FART%20特征有的在%20native%20层,最终编译成%20so%20文件;有的在%20java%20层,最终编译成%20dex%20相关文件。
如何找到这些%20so%20和%20dex%20相关文件?
/proc/self/maps
/proc/self/maps%20是%20Linux(含%20Android)系统中一个非常重要的伪文件,它提供了当前进程内存映射(memory%20mapping)信息,是分析当前进程加载了哪些资源的重要窗口。
包括:
- •%20加载的%20.so%20动态库
- •%20加载的%20.dex%20文件(包含%20ODEX%20/%20VDEX)
- •%20映射的%20Java%20堆、native%20堆、stack%20等
- •%20匿名%20mmap%20内存区域
- •%20JIT%20编译生成的代码段
- •%20映射的%20/system/,%20/data/,%20/apex/,%20/dev/ashmem%20等文件
比如,进入%20adb%20shell%20,通过下面命令读取包名%20com.cyrus.example%20下的%20maps%20文件
cat%20/proc/$(pidof%20com.cyrus.example)/maps
输出结果如下:
12c00000-12c80000%20rw-p%2000000000%2000:00%200%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 [anon:dalvik-main%20space%20(region%20space)]
12c80000-132c0000%20---p%2000000000%2000:00%200%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 [anon:dalvik-main%20space%20(region%20space)]
132c0000-13580000%20rw-p%2000000000%2000:00%200%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 [anon:dalvik-main%20space%20(region%20space)]
13580000-26280000%20---p%2000000000%2000:00%200%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 [anon:dalvik-main%20space%20(region%20space)]
26280000-2a940000%20---p%2000000000%2000:00%200%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 [anon:dalvik-main%20space%20(region%20space)]
2a940000-2a980000%20---p%2000000000%2000:00%200%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 [anon:dalvik-main%20space%20(region%20space)]
2a980000-2a9c0000%20---p%2000000000%2000:00%200%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 [anon:dalvik-main%20space%20(region%20space)]
2a9c0000-2aac0000%20---p%2000000000%2000:00%200%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 [anon:dalvik-main%20space%20(region%20space)]
2aac0000-2ab80000%20---p%2000000000%2000:00%200%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 [anon:dalvik-main%20space%20(region%20space)]
2ab80000-2abc0000%20---p%2000000000%2000:00%200%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 [anon:dalvik-main%20space%20(region%20space)]
2abc0000-2ac00000%20rw-p%2000000000%2000:00%200%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 [anon:dalvik-main%20space%20(region%20space)]
708d5000-70b5a000%20rw-p%2000000000%20103:1d%201863%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot.art
70b5a000-70c4a000%20rw-p%2000000000%20103:1d%201833%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-core-libart.art
70c4a000-70c80000%20rw-p%2000000000%20103:1d%201848%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-okhttp.art
70c80000-70cc1000%20rw-p%2000000000%20103:1d%201830%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-bouncycastle.art
70cc1000-70cd1000%20rw-p%2000000000%20103:1d%201827%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-apache-xml.art
70cd1000-71595000%20rw-p%2000000000%20103:1d%201839%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-framework.art
71595000-715c9000%20rw-p%2000000000%20103:1d%201836%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-ext.art
715c9000-716c1000%20rw-p%2000000000%20103:1d%201854%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-telephony-common.art
716c1000-716cf000%20rw-p%2000000000%20103:1d%201860%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-voip-common.art
716cf000-716e4000%20rw-p%2000000000%20103:1d%201842%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-ims-common.art
716e4000-716e7000%20rw-p%2000000000%20103:1d%201824%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-android.test.base.art
716e7000-716e9000%20rw-p%2000000000%20103:1d%201851%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-org.ifaa.android.manager.art
716e9000-716f0000%20rw-p%2000000000%20103:1d%201845%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-ims-ext-common_system.art
716f0000-716f4000%20rw-p%2000000000%20103:1d%201857%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-telephony-ext.art
716f4000-716fc000%20rw-p%2000000000%20103:1d%201821%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-WfdCommon.art
716fc000-717b2000%20r--p%2000000000%20103:1d%201864%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot.oat
717b2000-71a4d000%20r-xp%20000b6000%20103:1d%201864%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot.oat
71a4d000-71a4e000%20rw-p%2000000000%2000:00%200%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 [anon:.bss]
71a4e000-71a50000%20r--s%2000000000%20103:1d%201882%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/boot.vdex
71a50000-71a51000%20r--p%2000351000%20103:1d%201864%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot.oat
71a51000-71a52000%20rw-p%2000352000%20103:1d%201864%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot.oat
71a52000-71a9b000%20r--p%2000000000%20103:1d%201834%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-core-libart.oat
71a9b000-71ba3000%20r-xp%2000049000%20103:1d%201834%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 /system/framework/arm64/boot-core-libart.oat
...
比如:
71a9b000-71ba3000%20r-xp%2000049000%20103:1d%201834%20 /system/framework/arm64/boot-core-libart.oat
字段解析如下:
|%20字段%20|%20示例值%20|%20含义%20| |%20—%20|%20—%20|%20—%20| |%2071a9b000-71ba3000%20|%20起始地址%20–%20结束地址%20|%20表示这段内存从%200x71a9b000%20映射到%200x71ba3000(大约%201MB)%20| |%20r-xp%20|%20权限%20|%20r%20=%20可读,x%20=%20可执行,p%20=%20私有%20| |%2000049000%20|%20文件偏移%20|%20映射文件时从%20offset=0x49000%20开始%20| |%20103:1d%20|%20设备编号%20|%20表示该文件所在设备的主/次设备号%20| |%201834%20|%20inode%20号%20|%20文件在设备上的%20inode%20编号%20| |%20/system/framework/arm64/boot-core-libart.oat%20|%20文件路径%20|%20表示映射的文件路径,来源是系统的%20OAT%20文件%20|
所有我们可以通过读取%20/proc/self/maps%20得到当前%20app%20加载的所有资源文件,实现如下:
extern%20"C"
JNIEXPORT%20jobjectArray%20JNICALL
Java_com_cyrus_example_fart_AntiFART_listLoadedFiles(JNIEnv%20*env,%20jclass)%20{
%20 %20std::ifstream%20maps("/proc/self/maps");
%20 %20std::string%20line;
%20 %20std::vector<std::string>%20paths;
%20 %20while%20(std::getline(maps,%20line))%20{
%20 %20 %20 %20std::size_t%20pathPos%20=%20line.find('/');
%20 %20 %20 %20if%20(pathPos%20!=%20std::string::npos)%20{
%20 %20 %20 %20 %20 %20std::string%20path%20=%20line.substr(pathPos);
%20 %20 %20 %20 %20 %20if%20(std::find(paths.begin(),%20paths.end(),%20path)%20==%20paths.end())%20{
%20 %20 %20 %20 %20 %20 %20 %20paths.push_back(path);
%20 %20 %20 %20 %20 %20}
%20 %20 %20 %20}
%20 %20}
%20 %20jclass%20stringClass%20=%20env->FindClass("java/lang/String");
%20 %20jobjectArray%20result%20=%20env->NewObjectArray(paths.size(),%20stringClass,%20nullptr);
%20 %20for%20(size_t%20i%20=%200;%20i%20<%20paths.size();%20++i)%20{
%20 %20 %20 %20env->SetObjectArrayElement(result,%20i,%20env->NewStringUTF(paths[i].c_str()));
%20 %20}
%20 %20return%20result;
}
效果如下:
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 特征。
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
替换完成。
重新编译系统
把修改后的 FART 代码替换到 Android 系统里面,重新编译。
# 初始化编译环境
source build/envsetup.sh
# 设置编译目标
breakfast wayne
# 回到 Android 源码树的根目录
croot
# 开始编译
brunch wayne
如何编译 FART ROM 参考这篇文章:移植 FART 到 Android 10 实现自动化脱壳[8]
生成 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
测试
可以看到 so 中已经检测不出 FART 特征
禁止加载 cdex
另外,dump 下来的 dex 文件头有可能是 cdex001,cdex 文件是不可以直接通过 dex 反编译工具反编译的
完整源码
开源地址:
- • https://github.com/CYRUS-STUDIO/AndroidExample
- • https://github.com/CYRUS-STUDIO/FART
相关文章:
- • Android Hook技术防范漫谈[10]
- • frida 检测[11]
引用链接
[1] 干掉抽取壳!FART 自动化脱壳框架与 Execute 脱壳点解析:https://cyrus-studio.github.io/blog/posts/%E5%B9%B2%E6%8E%89%E6%8A%BD%E5%8F%96%E5%A3%B3fart-%E8%87%AA%E5%8A%A8%E5%8C%96%E8%84%B1%E5%A3%B3%E6%A1%86%E6%9E%B6%E4%B8%8E-execute-%E8%84%B1%E5%A3%B3%E7%82%B9%E8%A7%A3%E6%9E%90/
[2]FART 主动调用组件深度解析:破解 ART 下函数抽取壳的终极武器:https://cyrus-studio.github.io/blog/posts/fart-%E4%B8%BB%E5%8A%A8%E8%B0%83%E7%94%A8%E7%BB%84%E4%BB%B6%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90%E7%A0%B4%E8%A7%A3-art-%E4%B8%8B%E5%87%BD%E6%95%B0%E6%8A%BD%E5%8F%96%E5%A3%B3%E7%9A%84%E7%BB%88%E6%9E%81%E6%AD%A6%E5%99%A8/
[3]一步步带你移植 FART 到 Android 10,实现自动化脱壳:https://cyrus-studio.github.io/blog/posts/%E4%B8%80%E6%AD%A5%E6%AD%A5%E5%B8%A6%E4%BD%A0%E7%A7%BB%E6%A4%8D-fart-%E5%88%B0-android-10%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%8A%A8%E5%8C%96%E8%84%B1%E5%A3%B3/
[4]FART 自动化脱壳框架优化实战:Bug 修复与代码改进记录:https://cyrus-studio.github.io/blog/posts/fart-%E8%87%AA%E5%8A%A8%E5%8C%96%E8%84%B1%E5%A3%B3%E6%A1%86%E6%9E%B6%E4%BC%98%E5%8C%96%E5%AE%9E%E6%88%98bug-%E4%BF%AE%E5%A4%8D%E4%B8%8E%E4%BB%A3%E7%A0%81%E6%94%B9%E8%BF%9B%E8%AE%B0%E5%BD%95/
[5]Frida + FART 联手:解锁更强大的 Android 脱壳新姿势:https://cyrus-studio.github.io/blog/posts/frida-+-fart-%E8%81%94%E6%89%8B%E8%A7%A3%E9%94%81%E6%9B%B4%E5%BC%BA%E5%A4%A7%E7%9A%84-android-%E8%84%B1%E5%A3%B3%E6%96%B0%E5%A7%BF%E5%8A%BF/
[6]FART 脱壳不再全量!用一份配置文件精准控制节奏与范围:https://cyrus-studio.github.io/blog/posts/fart-%E8%84%B1%E5%A3%B3%E4%B8%8D%E5%86%8D%E5%85%A8%E9%87%8F%E7%94%A8%E4%B8%80%E4%BB%BD%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E7%B2%BE%E5%87%86%E6%8E%A7%E5%88%B6%E8%8A%82%E5%A5%8F%E4%B8%8E%E8%8C%83%E5%9B%B4/
[7]使用 Frida 增强 FART:实现更强大的 Android 脱壳能力:https://cyrus-studio.github.io/blog/posts/%E4%BD%BF%E7%94%A8-frida-%E5%A2%9E%E5%BC%BA-fart%E5%AE%9E%E7%8E%B0%E6%9B%B4%E5%BC%BA%E5%A4%A7%E7%9A%84-android-%E8%84%B1%E5%A3%B3%E8%83%BD%E5%8A%9B/
[8]移植 FART 到 Android 10 实现自动化脱壳:https://cyrus-studio.github.io/blog/posts/%E7%A7%BB%E6%A4%8D-fart-%E5%88%B0-android-10-%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%8A%A8%E5%8C%96%E8%84%B1%E5%A3%B3/
[9]frida_fart:https://cyrus-studio.github.io/blog/posts/%E4%BD%BF%E7%94%A8-frida-%E5%A2%9E%E5%BC%BA-fart%E5%AE%9E%E7%8E%B0%E6%9B%B4%E5%BC%BA%E5%A4%A7%E7%9A%84-android-%E8%84%B1%E5%A3%B3%E8%83%BD%E5%8A%9B/
[10]Android Hook技术防范漫谈:https://tech.meituan.com/2018/02/02/android-anti-hooking.html
[11]frida 检测:https://www.52pojie.cn/thread-1783400-1-1.html
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:哆啦安全 《FART脱壳:实现AJM壳级别的对抗功能+绕过全解析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论