参考
将FART和Youpk结合来做一次针对函数抽取壳的全面提升
看雪高研班课程
寒冰大佬的FART带动了不少新的主动调用思想的抽取壳方案。看了上面这篇文章,感觉意犹未尽,当然是要动手实践一翻来优化一波。于是我重新翻阅FART和Youpk的源码,我准备和那位大佬一样,参考Youpk在FART的基础上进行升级改造。开工前先明确出我的需求。
需求
对指定进程脱壳,非目标进程不要执行脱壳线程。
对指定类列表进行脱壳
将FART升级到aosp10实现。
去FART指纹
FART保存出的函数修复合并为dex。
实现FART更深的主动调用
更快的主动调用(暂未优化)
一、什么是FART 还未了解过的请看原作者对于fart的介绍
FART:ART环境下基于主动调用的自动化脱壳方案
FART正餐前甜点:ART下几个通用简单高效的dump内存中dex方法
拨云见日:安卓APP脱壳的本质以及如何快速发现ART下的脱壳点
关于fart源码的调用流程可以看看我以前整理的一篇文章
fart的理解和分析过程
简单总结:
这是一个基于主动调用来脱抽取壳的方案。
简述实现原理:
在进程启动的时候通过双亲委派机制遍历所有classloader,然后遍历里面的所有class,取出所有函数,直接调用。然后在ArtMethod的Invoke函数这里根据参数判断出这是主动调用触发的,然后就取消函数的正常执行,并执行脱壳操作。
二、对指定进程脱壳 首先看看FART源码中,脱壳线程的入口点ActivityThread.java的performLaunchActivity这个函数中开始的FART处理。
1 2 3 4 5 private Activity performLaunchActivity (ActivityClientRecord r, Intent customIntent) { ... fartthread(); ... }
也就是说,所有的进程都会执行脱壳的流程。所以这个地方我觉得还是有必要优化的。应该是对指定的进程脱壳会更加好一些。Youpk中已经有了这个优化。所以我直接拿Youpk的处理来使用。下面是修改后的入口启动部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public static boolean shouldUnpack () { boolean should_unpack = false ; String processName = ActivityThread.currentProcessName(); BufferedReader br = null ; String configPath="/data/local/tmp/fext.config" ; try { br = new BufferedReader(new FileReader(configPath)); String line; while ((line = br.readLine()) != null ) { if (processName.equals(line))) { should_unpack = true ; break ; } } br.close(); } catch (Exception ignored) { } return should_unpack; } public static void fartthread () { if (!shouldUnpack()) { return ; } new Thread(new Runnable() { @Override public void run () { try { Log.e("ActivityThread" , "start sleep......" ); Thread.sleep(1 * 60 * 1000 ); } catch (InterruptedException e) { e.printStackTrace(); } Log.e("ActivityThread" , "sleep over and start fart" ); fart(); Log.e("ActivityThread" , "fart run over" ); } }).start(); } private void handleBindApplication (AppBindData data) { ... app = data.info.makeApplication(data.restrictedBackupMode, null ); app.setAutofillOptions(data.autofillOptions); app.setContentCaptureOptions(data.contentCaptureOptions); mInitialApplication = app; fartthread(); ... }
这里主要是做了两点修改:
1、把启动FART线程的处理放到了handleBindApplication函数,这是因为performLaunchActivity这个函数调用有的时候可能会触发两次,而handleBindApplication确定只会触发一次。
2、FART线程启动前加了个判断,在配置文件中的进程才需要脱壳。这样基本第一个优化就完成了。
三、对指定类列表进行脱壳 由于应用的有些类可能是被特殊处理了,主动调用的情况会导致程序崩溃或者退出。所以最好是可以单独对某些类进行主动调用。我调整了FART线程启动的逻辑,先是上面的判断是不是要脱壳的目标进程,然后判断有没有设定类列表,如果有类列表就只脱壳类列表,否则就完整主动调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 public static String getClassList () { String processName = ActivityThread.currentProcessName(); BufferedReader br = null ; String configPath="/data/local/tmp/" +processName; Log.e("ActivityThread" , "getClassList processName:" +processName); StringBuilder sb=new StringBuilder(); try { br = new BufferedReader(new FileReader(configPath)); String line; while ((line = br.readLine()) != null ) { if (line.length()>=2 ){ sb.append(line+"\n" ); } } br.close(); } catch (Exception ex) { Log.e("ActivityThread" , "getClassList err:" +ex.getMessage()); return "" ; } return sb.toString(); } public static void fartWithClassList (String classlist) { ClassLoader appClassloader = getClassloader(); if (appClassloader==null ){ Log.e("ActivityThread" , "appClassloader is null" ); return ; } Class DexFileClazz = null ; try { DexFileClazz = appClassloader.loadClass("dalvik.system.DexFile" ); } catch (Exception e) { e.printStackTrace(); } catch (Error e) { e.printStackTrace(); } Method dumpMethodCode_method = null ; for (Method field : DexFileClazz.getDeclaredMethods()) { if (field.getName().equals("fartextMethodCode" )) { dumpMethodCode_method = field; dumpMethodCode_method.setAccessible(true ); } } String[] classes=classlist.split("\n" ); for (String clsname : classes){ String line=clsname; if (line.startsWith("L" )&&line.endsWith(";" )&&line.contains("/" )){ line=line.substring(1 ,line.length()-1 ); line=line.replace("/" ,"." ); } loadClassAndInvoke(appClassloader, line, dumpMethodCode_method); } } public static void fartthread () { if (!shouldUnpack()) { return ; } String classlist=getClassList(); if (!classlist.equals("" )){ fartWithClassList(classlist); return ; } ... }
四、FART升级AOSP10 直接将FART修改的代码部分直接替换到AOSP10中。毫不意外的出现了一堆错误。不过问题比较集中。主要是对于CodeItem的成员访问方式发生了变化。这里可以参考下面的文章
Android ART 虚拟机 - dex 文件格式要旨
根据这篇文章中对CodeItem对象新的访问方式。对FART的源码部分做出修改
修改文件是art_method.cc。我这里只贴上部分关键修改的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 extern "C" void dumpArtMethod (ArtMethod* artmethod) REQUIRES_SHARED (Locks::mutator_lock_) { ... const dex::CodeItem* code_item = artmethod->GetCodeItem(); const DexFile* dex_=artmethod->GetDexFile(); CodeItemDataAccessor accessor (*dex_, dex_->GetCodeItem(artmethod->GetCodeItemOffset())) ; if (LIKELY(code_item != nullptr )) { int code_item_len = 0 ; uint8_t *item=(uint8_t *) code_item; if (accessor.TriesSize()>0 ) { const uint8_t *handler_data = accessor.GetCatchHandlerData(); uint8_t * tail = codeitem_end(&handler_data); code_item_len = (int )(tail - item); }else { code_item_len = 16 +accessor.InsnsSizeInCodeUnits()*2 ; } ... } ... }
需要修改的不止上面这几个地方。但是主要都是针对CodeItem的使用以及命名空间的修改。这里我就不全部贴出了。最后我会贴出改完后的版本。
五、去FART指纹 由于FART的知名度还是挺高的,所以最好还是把FART中特有的一些函数名和文件保存路径给修改一下。下面整理下我参考Youpk做的一些修改。
1、将ActivityThread中的FART相关函数全部单独放到一个类cn.mik.Fartext中。这样如果别人对ActivityThread的函数检测就找不到FART相关的了。
2、将DexFile中的dumpMethodCode函数名修改为fartextMethodCode
3、将myfartInvoke函数名改成fartextInvoke
4、将所有使用/sdcard/fart的这个路径全部修改成/sdcard/fext
把这些常见的可能识别的方式都修改之后。一般就识别不出来了。我这种完全没知名度的,想必不会被人检测到了。暗自欣喜。
六、FART的函数修复 抽取壳的应用在脱壳后,有两种文件,一个是在当前时机dump出来的dex文件。另一个是保存codeitem出来的bin文件。
FART的修复组件是使用开源项目FART 中那个py的脚本来解析dex文件,将bin的codeitem修复打印。对于里面的代码解析部分我之前也写了文章。感兴趣可以看看
dex起步探索
但是我仔细研究后,发现修复组件只进行了打印。并没有修复成dex,而是直接解析打印。最理想的还是修复到dex,方便使用静态分析工具查看。有大佬也已经写了这个工具。那就是前面参考的Youpk。他的内部有个dexfixer的目录,就是实现了对导出的codeitem数据修复到dex中。不过他的codeitem的保存结构和FART的并不大一样。不过没关系,修改一下codeitem文件的解析部分就好了。下面贴上Youpk和我修改后专门处理fart结果的dexfixer。
Youpk
dexfixer
七、更深的主动调用 FART的主动调用深度 首先当然是看看FART的主动调用的深度是在哪里,这里的深度其实就是在函数的主动执行过程中,FART是在执行到哪个流程时,进行的脱壳处理。下面贴上FART主动调用的脱壳位置
1 2 3 4 5 6 7 8 void ArtMethod::Invoke (Thread* self, uint32_t * args, uint32_t args_size, JValue* result, const char * shorty) { if (self== nullptr ) { dumpArtMethod(this ); return ; } ... }
上面可以看到。当函数执行流程到达ArtMethod::Invoke时,就根据参数判断是主动调用的情况,就脱壳并结束了。
为什么需要更深的主动调用 一般的函数抽取壳,在执行到ArtMethod::Invoke前就已经对抽取函数还原了。但是也有一些抽取壳,执行到Invoke时依然还没有还原函数。譬如下面这种抽取壳。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 .method public constructor <init>()V .registers 2 goto :goto_c :goto_1 nop nop nop nop nop nop nop nop nop nop return -void :goto_c const v0, 0x1669 invoke-static {v0}, Ls/h/e/l/l/H;->i(I)V goto :goto_1 .end method
可以看到这个抽取的函数,进来之后就goto,然后执行invoke-static。接着在goto到函数的开始位置。
也就是说。这个抽取壳,必须在函数执行了之后,才会还原出真实的函数。回想一下前面说的FART的主动调用深度。发现函数真正执行前就已经被我们直接结束掉了。所以我们需要更深的主动调用才能够解决这个抽取壳。
Youpk更深的主动调用 我们回头看看上面的抽取壳,我们的目标是要判断如果这个函数的第一个指令是goto,就正常执行,然后执行到invoke-static的指令。这个指令完成之后就直接结束掉函数调用。避免真实函数调用会出现异常。
先参考Youpk的看看他是如何实现更深的主动调用来解决这个问题的。下面是第一步,先修改默认的解释器为Switch的解释器。这是因为Switch解释器的可读性更加高,方便我们直接修改源码来达到目的。
1 static constexpr InterpreterImplKind kInterpreterImplKind = kSwitchImplKind;
然后我们看看主动调用时Youpk是怎么模拟参数的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void Unpacker::invokeAllMethods () { ... auto methods = klass->GetDeclaredMethods(pointer_size); Unpacker::enableFakeInvoke(); for (auto & m : methods) { ArtMethod* method = &m; if (!method->IsProxyMethod() && method->IsInvokable()) { uint32_t args_size = (uint32_t )ArtMethod::NumArgRegisters(method->GetShorty()); if (!method->IsStatic()) { args_size += 1 ; } JValue result; std ::vector <uint32_t > args (args_size, 0 ) ; if (!method->IsStatic()) { mirror::Object* thiz = klass->AllocObject(self); args[0 ] = StackReference<mirror::Object>::FromMirrorPtr(thiz).AsVRegValue(); } method->Invoke(self, args.data(), args_size, &result, method->GetShorty()); } } Unpacker::disableFakeInvoke(); cJSON_ReplaceItemInObject(current, "status" , cJSON_CreateString("Dumped" )); writeJson(); } } }
这里可以看到Youpk的参数是模拟赋值进去的。而寒冰大佬的做法不大一样。看看FART的函数调用模拟。
1 2 3 4 5 6 7 8 extern "C" void myfartInvoke (ArtMethod* artmethod) REQUIRES_SHARED (Locks::mutator_lock_) { JValue *result=nullptr ; Thread *self=nullptr ; uint32_t temp=6 ; uint32_t * args=&temp; uint32_t args_size=6 ; artmethod->Invoke(self, args, args_size, result, "fart" ); }
这样肯定没法顺利往后执行。我们先继续参考Youpk的后续。
然后看看Youpk的ArtMethod::Invoke的处理,如果是主动调用并且非Native函数就正常执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void ArtMethod::Invoke (Thread* self, uint32_t * args, uint32_t args_size, JValue* result, const char * shorty) { ... if (UNLIKELY(!runtime->IsStarted() || Dbg::IsForcedInterpreterNeededForCalling(self, this ) || (Unpacker::isFakeInvoke(self, this ) && !this ->IsNative()))) { if (IsStatic()) { art::interpreter::EnterInterpreterFromInvoke( self, this , nullptr , args, result, true ); } else { mirror::Object* receiver = reinterpret_cast <StackReference<mirror::Object>*>(&args[0 ])->AsMirrorPtr(); art::interpreter::EnterInterpreterFromInvoke( self, this , receiver, args + 1 , result, true ); } } else { if (Unpacker::isFakeInvoke(self, this ) && this ->IsNative()) { self->PopManagedStackFragment(fragment); return ; } ... } ... }
接下来看解释器的EnterInterpreterFromInvoke函数处理。这里Youpk没有什么处理。
1 2 3 4 5 6 7 8 void EnterInterpreterFromInvoke (Thread* self, ArtMethod* method, Object* receiver, uint32_t * args, JValue* result, bool stay_in_interpreter) { ... JValue r = Execute(self, code_item, *shadow_frame, JValue(), stay_in_interpreter); ... }
继续看看函数Execute。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static inline JValue Execute ( Thread* self, const DexFile::CodeItem* code_item, ShadowFrame& shadow_frame, JValue result_register, bool stay_in_interpreter = false ) SHARED_REQUIRES (Locks::mutator_lock_) { ... } else if (kInterpreterImplKind == kSwitchImplKind) { if (transaction_active) { return ExecuteSwitchImpl<false , true >(self, code_item, shadow_frame, result_register, false ); } else { return ExecuteSwitchImpl<false , false >(self, code_item, shadow_frame, result_register, false ); } } ... }
然后这ExecuteSwitchImpl就是关键的解释指令的函数了。到这里终于有Youpk修改的部分了。先看看修改的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #define PREAMBLE() \ do { \ inst_count++; \ bool dumped = Unpacker::beforeInstructionExecute(self, shadow_frame.GetMethod(), \ dex_pc, inst_count); \ if (dumped) { \ return JValue(); \ } \ if (UNLIKELY(instrumentation->HasDexPcListeners())) { \ instrumentation->DexPcMovedEvent(self, shadow_frame.GetThisObject(code_item->ins_size_), \ shadow_frame.GetMethod(), dex_pc); \ } \ } while (false )
PREAMBLE这个函数基本每个指令执行前都会调用beforeInstructionExecute来判断下。如果这里dump脱壳了,就直接结束掉,这个函数不再往下执行了。如果是上面那种特殊壳,这里就可以暂时先不要dump。让他正常执行先。下面看看里面的逻辑处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 bool Unpacker::beforeInstructionExecute (Thread *self, ArtMethod *method, uint32_t dex_pc, int inst_count) { if (Unpacker::isFakeInvoke(self, method)) { const uint16_t * const insns = method->GetCodeItem()->insns_; const Instruction* inst = Instruction::At(insns + dex_pc); uint16_t inst_data = inst->Fetch16(0 ); Instruction::Code opcode = inst->Opcode(inst_data); if (inst_count == 0 && opcode != Instruction::GOTO && opcode != Instruction::GOTO_16 && opcode != Instruction::GOTO_32) { Unpacker::dumpMethod(method); return true ; } else if (inst_count == 0 && opcode >= Instruction::GOTO && opcode <= Instruction::GOTO_32) { return false ; } else if (inst_count == 1 && opcode >= Instruction::CONST_4 && opcode <= Instruction::CONST_WIDE_HIGH16) { return false ; } else if (inst_count == 2 && (opcode == Instruction::INVOKE_STATIC || opcode == Instruction::INVOKE_STATIC_RANGE)) { Unpacker::disableFakeInvoke(); Unpacker::enableRealInvoke(); return false ; } else if (inst_count == 3 ) { if (opcode >= Instruction::GOTO && opcode <= Instruction::GOTO_32) { const Instruction* inst_first = Instruction::At(insns); Instruction::Code first_opcode = inst_first->Opcode(inst->Fetch16(0 )); CHECK(first_opcode >= Instruction::GOTO && first_opcode <= Instruction::GOTO_32); ULOGD("found najia/ijiami %s" , PrettyMethod(method).c_str()); switch (first_opcode) { case Instruction::GOTO: Unpacker::dumpMethod(method, 2 ); break ; case Instruction::GOTO_16: Unpacker::dumpMethod(method, 4 ); break ; case Instruction::GOTO_32: Unpacker::dumpMethod(method, 8 ); break ; default : break ; } } else { Unpacker::dumpMethod(method); } return true ; } Unpacker::dumpMethod(method); return true ; } return false ; }
这里可以看到。如果是INVOKE_STATIC就让指令正常执行。其他正常的抽取壳的深度就是在这里。这相当于就是指令执行前进行dump了。但是这里依然没解决特殊壳的深度问题。必须执行完INVOKE_STATIC之后。再进行脱壳并结束掉函数。继续看Youpk下面的处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 template <bool do_access_check, bool transaction_active>JValue ExecuteSwitchImpl (Thread* self, const DexFile::CodeItem* code_item, ShadowFrame& shadow_frame, JValue result_register, bool interpret_one_instruction) { ... int inst_count = -1 ; do { dex_pc = inst->GetDexPc(insns); shadow_frame.SetDexPC(dex_pc); TraceExecution(shadow_frame, inst, dex_pc); inst_data = inst->Fetch16(0 ); switch (inst->Opcode(inst_data)) { ... case Instruction::GOTO: { PREAMBLE(); int8_t offset = inst->VRegA_10t(inst_data); BRANCH_INSTRUMENTATION(offset); if (IsBackwardBranch(offset)) { HOTNESS_UPDATE(); self->AllowThreadSuspension(); } inst = inst->RelativeAt(offset); break ; } ... case Instruction::INVOKE_STATIC: { PREAMBLE(); bool success = DoInvoke<kStatic, false , do_access_check>( self, shadow_frame, inst, inst_data, &result_register); POSSIBLY_HANDLE_PENDING_EXCEPTION(!success, Next_3xx); break ; } case Instruction::INVOKE_STATIC_RANGE: { PREAMBLE(); bool success = DoInvoke<kStatic, true , do_access_check>( self, shadow_frame, inst, inst_data, &result_register); POSSIBLY_HANDLE_PENDING_EXCEPTION(!success, Next_3xx); break ; } ... } bool dumped = Unpacker::afterInstructionExecute(self, shadow_frame.GetMethod(), dex_pc, inst_count); if (dumped) { return JValue(); } } while (!interpret_one_instruction); shadow_frame.SetDexPC(inst->GetDexPc(insns)); return result_register; }
这里就看到每个指令都执行了PREAMBLE函数。然后每个指令执行完都执行了afterInstructionExecute这个函数。在这里就可以判断,如果执行完的指令是INVOKE_STATIC。就可以直接return结束掉函数执行了。看看Youpk的处理
1 2 3 4 5 6 7 8 9 10 11 12 bool Unpacker::afterInstructionExecute (Thread *self, ArtMethod *method, uint32_t dex_pc, int inst_count) { const uint16_t * const insns = method->GetCodeItem()->insns_; const Instruction* inst = Instruction::At(insns + dex_pc); uint16_t inst_data = inst->Fetch16(0 ); Instruction::Code opcode = inst->Opcode(inst_data); if (inst_count == 2 && (opcode == Instruction::INVOKE_STATIC || opcode == Instruction::INVOKE_STATIC_RANGE) && Unpacker::isRealInvoke(self, method)) { Unpacker::enableFakeInvoke(); Unpacker::disableRealInvoke(); } return false ; }
这里留意了一下。这个函数固定返回的false。但是通过设置enableFakeInvoke和disableRealInvoke来控制下一个指令执行的时候来进行退出函数。我感觉这里退出应该也没啥问题。
到这里基本就走完大致的流程了。那么欣赏完别人的代码。可以开始我们的改造工作了。
FartExt更深的主动调用 和Youpk一样。第一步就是先把解释器给改成使用Switch解释器。但是由于我使用的是AOSP10。所以发现修改部分果然不大一样了。
1 2 3 4 5 #if ART_USE_CXX_INTERPRETER static constexpr InterpreterImplKind kInterpreterImplKind = kSwitchImplKind;#else static constexpr InterpreterImplKind kInterpreterImplKind = kMterpImplKind;#endif
发现这里变成可以通过编译参数来控制的了。搜索一下ART_USE_CXX_INTERPRETER的使用
1 2 3 if envTrue (ctx, "ART_USE_CXX_INTERPRETER" ) { cflags = append(cflags, "-DART_USE_CXX_INTERPRETER=1" ) }
发现这个好像可以通过cflags来配置了。所以我修改了下runtime下的Android.pb。如果不想改全局的。也可以在源码里面直接判断是主动调用就强制走switch解释器。
1 2 3 4 5 6 7 cflags: [ // ART is allowed to link to libicuuc directly // since they are in the same module "-DANDROID_LINK_SHARED_ICU4C", "-Wno-error", "-DART_USE_CXX_INTERPRETER=1" ],
接着就是ArtMethod::Invoke的时候不要直接结束了。但是这里我们需要留意的是。第一个参数的Thread是fart用来判断是否为主动调用的。为了让后面能正常执行,我就直接把第一个参数给赋值了。而后面的调用流程也是需要判断当前执行函数是否为主动调用。Youpk是用线程和一个变量来控制判断是否为主动调用的。这里使用result=111111在后续判断是否为主动调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 extern "C" void fartextInvoke (ArtMethod* artmethod) REQUIRES_SHARED (Locks::mutator_lock_) { if (artmethod->IsNative()||artmethod->IsAbstract()){ return ; } JValue result; Thread *self=Thread::Current(); uint32_t temp[100 ]={0 }; uint32_t * args=temp; uint32_t args_size = (uint32_t )ArtMethod::NumArgRegisters(artmethod->GetShorty()); if (!artmethod->IsStatic()) { args_size += 1 ; } result.SetI(111111 ); LOG(ERROR) << "fartext fartextInvoke" ; Unpacker_self_=self; artmethod->Invoke(self, args, args_size, &result,artmethod->GetShorty()); } void ArtMethod::Invoke (Thread* self, uint32_t * args, uint32_t args_size, JValue* result, const char * shorty) { ... if (result!=nullptr && result->GetI()==111111 ){ LOG(ERROR) << "fartext artMethod::Invoke Method " <<this ->PrettyMethod().c_str(); if (IsStatic()) { art::interpreter::EnterInterpreterFromInvoke( self, this , nullptr , args, result, true ); }else { art::interpreter::EnterInterpreterFromInvoke( self, this , nullptr , args + 1 , result, true ); } LOG(ERROR) << "fartext artMethod::Invoke Method Over " <<this ->PrettyMethod().c_str(); self->PopManagedStackFragment(fragment); return ; } ... }
这里有个问题是上面这种模拟参数的方式,碰到引用类型的参数会报错。所以在处理参数入栈的时候,也要进行判断处理一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 void EnterInterpreterFromInvoke (Thread* self, ArtMethod* method, ObjPtr<mirror::Object> receiver, uint32_t * args, JValue* result, bool stay_in_interpreter) { ... if (!method->IsStatic()) { if (result!=nullptr &&result->GetI()==111111 ){ shadow_frame->SetVReg(cur_reg, args[0 ]); }else { CHECK(receiver != nullptr ); shadow_frame->SetVRegReference(cur_reg, receiver); } ++cur_reg; } uint32_t shorty_len = 0 ; const char * shorty = method->GetShorty(&shorty_len); for (size_t shorty_pos = 0 , arg_pos = 0 ; cur_reg < num_regs; ++shorty_pos, ++arg_pos, cur_reg++) { DCHECK_LT(shorty_pos + 1 , shorty_len); switch (shorty[shorty_pos + 1 ]) { case 'L' : { if (result!=nullptr &&result->GetI()==111111 ){ shadow_frame->SetVReg(cur_reg, args[0 ]); break ; } ObjPtr<mirror::Object> o = reinterpret_cast <StackReference<mirror::Object>*>(&args[arg_pos])->AsMirrorPtr(); shadow_frame->SetVRegReference(cur_reg, o); break ; } case 'J' : case 'D' : { uint64_t wide_value = (static_cast <uint64_t >(args[arg_pos + 1 ]) << 32 ) | args[arg_pos]; shadow_frame->SetVRegLong(cur_reg, wide_value); cur_reg++; arg_pos++; break ; } default : shadow_frame->SetVReg(cur_reg, args[arg_pos]); break ; } } ... if (LIKELY(!method->IsNative())) { if (result!=nullptr &&result->GetI()==111111 ){ JValue r = Execute(self, accessor, *shadow_frame, *result, stay_in_interpreter); if (result != nullptr ) { *result = r; } return ; }else { JValue r = Execute(self, accessor, *shadow_frame, JValue(), stay_in_interpreter); if (result != nullptr ) { *result = r; } } } ... }
接下来就开始修改解释器部分的逻辑了。我们只要做到几点处理。就可以搞定这种壳了。
1、如果是主动调用并且第一个指令如果不是GOTO的。就直接脱壳并结束
2、如果是主动调用并且第一个指令是GOTO的。让他继续执行
3、如果第三个指令是INVOKE-STATIC的执行完后直接结束掉
接下来准备改代码。然后碰到一个问题。同样也是AOSP10的版本导致的。Switch解释器的逻辑发生了较大的变动。先看看变成了啥样子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 template <bool do_access_check, bool transaction_active>ATTRIBUTE_NO_SANITIZE_ADDRESS void ExecuteSwitchImplCpp (SwitchImplContext* ctx) { ... bool const interpret_one_instruction = ctx->interpret_one_instruction; while (true ) { dex_pc = inst->GetDexPc(insns); shadow_frame.SetDexPC(dex_pc); TraceExecution(shadow_frame, inst, dex_pc); inst_data = inst->Fetch16(0 ); { bool exit_loop = false ; InstructionHandler<do_access_check, transaction_active> handler( ctx, instrumentation, self, shadow_frame, dex_pc, inst, inst_data, exit_loop); if (!handler.Preamble()) { if (UNLIKELY(exit_loop)) { return ; } if (UNLIKELY(interpret_one_instruction)) { break ; } continue ; } } switch (inst->Opcode(inst_data)) { #define OPCODE_CASE(OPCODE, OPCODE_NAME, pname, f, i, a, e, v) \ case OPCODE: { \ bool exit_loop = false ; \ InstructionHandler<do_access_check, transaction_active> handler( \ ctx, instrumentation, self, shadow_frame, dex_pc, inst, inst_data, exit_loop); \ handler.OPCODE_NAME(); \ \ if (UNLIKELY(exit_loop)) { \ return ; \ } \ break ; \ } DEX_INSTRUCTION_LIST(OPCODE_CASE) #undef OPCODE_CASE } if (UNLIKELY(interpret_one_instruction)) { break ; } } shadow_frame.SetDexPC(inst->GetDexPc(insns)); ctx->result = ctx->result_register; return ; }
看到了这两个部分都发生了较大的变化。那个超大的case都不见了。不过也只是处理的方式发生变化。我们跟着调整下就行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 template <bool do_access_check, bool transaction_active>ATTRIBUTE_NO_SANITIZE_ADDRESS void ExecuteSwitchImplCpp (SwitchImplContext* ctx) { ... int32_t regvalue=ctx->result_register.GetI(); ctx->result_register=JValue(); int inst_count = -1 ; bool flag=false ; bool const interpret_one_instruction = ctx->interpret_one_instruction; while (true ) { ... inst_count++; uint8_t opcode = inst->Opcode(inst_data) if (regvalue==111111 ){ if (inst_count == 0 ){ if (opcode == Instruction::GOTO || opcode == Instruction::GOTO_16 || opcode == Instruction::GOTO_32){ LOG(ERROR) << "fartext ExecuteSwitchImplCpp Switch inst_count=0 opcode==GOTO " <<shadow_frame.GetMethod()->PrettyMethod().c_str(); flag=true ; }else { LOG(ERROR) << "fartext ExecuteSwitchImplCpp Switch inst_count=0 opcode!=GOTO " <<shadow_frame.GetMethod()->PrettyMethod().c_str(); dumpArtMethod(shadow_frame.GetMethod()); break ; } } if (inst_count == 1 ){ if (opcode >= Instruction::CONST_4 && opcode <= Instruction::CONST_WIDE_HIGH16){ LOG(ERROR) << "fartext ExecuteSwitchImplCpp Switch inst_count=1 opcode==CONST " <<shadow_frame.GetMethod()->PrettyMethod().c_str(); flag=true ; }else { LOG(ERROR) << "fartext ExecuteSwitchImplCpp Switch inst_count=1 opcode!=CONST " <<shadow_frame.GetMethod()->PrettyMethod().c_str(); dumpArtMethod(shadow_frame.GetMethod()); break ; } } } switch (opcode) { #define OPCODE_CASE(OPCODE, OPCODE_NAME, pname, f, i, a, e, v) \ case OPCODE: { \ bool exit_loop = false ; \ InstructionHandler<do_access_check, transaction_active> handler( \ ctx, instrumentation, self, shadow_frame, dex_pc, inst, inst_data, exit_loop); \ handler.OPCODE_NAME(); \ \ if (UNLIKELY(exit_loop)) { \ return ; \ } \ break ; \ } DEX_INSTRUCTION_LIST(OPCODE_CASE) #undef OPCODE_CASE } if (regvalue==111111 ){ if (inst_count==2 &&flag){ if (opcode == Instruction::INVOKE_STATIC || opcode == Instruction::INVOKE_STATIC_RANGE){ dumpArtMethod(shadow_frame.GetMethod()); ArtMethod::disableFartextInvoke(); break ; } } if (inst_count>2 ){ dumpArtMethod(shadow_frame.GetMethod()); ArtMethod::disableFartextInvoke(); break ; } } if (UNLIKELY(interpret_one_instruction)) { break ; } } shadow_frame.SetDexPC(inst->GetDexPc(insns)); ctx->result = ctx->result_register; return ; }
八、流程图
九、更快的主动调用(暂未优化) 在测试FART的主动调用中发现,主动调用的耗时较长,根据上面的流程图。我们可以看到调用最耗时最核心的函数dumpArtMethod。就是在这里进行脱壳的。先看看FART里面做了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 extern "C" void dumpArtMethod (ArtMethod* artmethod) REQUIRES_SHARED (Locks::mutator_lock_) { char *dexfilepath=(char *)malloc (sizeof (char )*1000 ); if (dexfilepath==nullptr ) { LOG(ERROR) << "ArtMethod::dumpArtMethodinvoked,methodname:" <<artmethod->PrettyMethod().c_str()<<"malloc 1000 byte failed" ; return ; } int result=0 ; int fcmdline =-1 ; char szCmdline[64 ]= {0 }; char szProcName[256 ] = {0 }; int procid = getpid(); sprintf (szCmdline,"/proc/%d/cmdline" , procid); fcmdline = open (szCmdline, O_RDONLY,0644 ); if (fcmdline >0 ) { result=read (fcmdline, szProcName,256 ); if (result<0 ) { LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,open cmdline file file error" ; } close (fcmdline); } if (szProcName[0 ]) { const DexFile* dex_file = artmethod->GetDexFile(); const uint8_t * begin_=dex_file->Begin(); size_t size_=dex_file->Size(); memset (dexfilepath,0 ,1000 ); int size_int_=(int )size_; memset (dexfilepath,0 ,1000 ); sprintf (dexfilepath,"%s" ,"/sdcard/fart" ); mkdir (dexfilepath,0777 ); memset (dexfilepath,0 ,1000 ); sprintf (dexfilepath,"/sdcard/fart/%s" ,szProcName); mkdir (dexfilepath,0777 ); memset (dexfilepath,0 ,1000 ); sprintf (dexfilepath,"/sdcard/fart/%s/%d_dexfile.dex" ,szProcName,size_int_); int dexfilefp=open (dexfilepath,O_RDONLY,0666 ); if (dexfilefp>0 ){ close (dexfilefp); dexfilefp=0 ; }else { int fp=open (dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666 ); if (fp>0 ) { result=write (fp,(void *)begin_,size_); if (result<0 ) { LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,open dexfilepath file error" ; } fsync(fp); close (fp); memset (dexfilepath,0 ,1000 ); sprintf (dexfilepath,"/sdcard/fart/%s/%d_classlist.txt" ,szProcName,size_int_); int classlistfile=open (dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666 ); if (classlistfile>0 ) { for (size_t ii= 0 ; ii< dex_file->NumClassDefs(); ++ii) { const DexFile::ClassDef& class_def = dex_file->GetClassDef(ii); const char * descriptor = dex_file->GetClassDescriptor(class_def); result=write (classlistfile,(void *)descriptor,strlen (descriptor)); if (result<0 ) { LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write classlistfile file error" ; } const char * temp="\n" ; result=write (classlistfile,(void *)temp,1 ); if (result<0 ) { LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write classlistfile file error" ; } } fsync(classlistfile); close (classlistfile); } } } const DexFile::CodeItem* code_item = artmethod->GetCodeItem(); if (LIKELY(code_item != nullptr )) { int code_item_len = 0 ; uint8_t *item=(uint8_t *) code_item; if (code_item->tries_size_>0 ) { const uint8_t *handler_data = (const uint8_t *)(DexFile::GetTryItems(*code_item, code_item->tries_size_)); uint8_t * tail = codeitem_end(&handler_data); code_item_len = (int )(tail - item); }else { code_item_len = 16 +code_item->insns_size_in_code_units_*2 ; } memset (dexfilepath,0 ,1000 ); int size_int=(int )dex_file->Size(); uint32_t method_idx=artmethod->GetDexMethodIndexUnchecked(); sprintf (dexfilepath,"/sdcard/fart/%s/%d_ins_%d.bin" ,szProcName,size_int,(int )gettidv1()); int fp2=open (dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666 ); if (fp2>0 ){ lseek(fp2,0 ,SEEK_END); memset (dexfilepath,0 ,1000 ); int offset=(int )(item - begin_); sprintf (dexfilepath,"{name:%s,method_idx:%d,offset:%d,code_item_len:%d,ins:" ,artmethod->PrettyMethod().c_str(),method_idx,offset,code_item_len); int contentlength=0 ; while (dexfilepath[contentlength]!=0 ) contentlength++; result=write (fp2,(void *)dexfilepath,contentlength); if (result<0 ) { LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write ins file error" ; } long outlen=0 ; char * base64result=base64_encode((char *)item,(long )code_item_len,&outlen); result=write (fp2,base64result,outlen); if (result<0 ) { LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write ins file error" ; } result=write (fp2,"};" ,2 ); if (result<0 ) { LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write ins file error" ; } fsync(fp2); close (fp2); if (base64result!=nullptr ){ free (base64result); base64result=nullptr ; } } } } if (dexfilepath!=nullptr ) { free (dexfilepath); dexfilepath=nullptr ; } }
这里可以看到dump的规则是相同大小的dex就跳过。非相同的就写入文件保存。相当于算是一个整体脱壳非常晚的时机。不过这个时机的调用频率较多。相对会影响性能,好处是这个整体脱壳点的时机够晚,绝对能脱掉除抽取函数外的整体壳。而Youpk中的优化是不使用这个整体脱壳点,只单纯的把codeitem写入bin文件,这样就能提高一定的效率。这里我就暂时不修改了。毕竟在这里脱整体壳也有一定的优势。如果想要更快的速度。也可以选择过滤主动调用的范围,来降低调用频率。
十、实战测试 编译完成之后,我们就可以来试试深度主动调用+dex修复的效果。为了防止风险,就不放测试的apk样本了。
安装好apk后,先去/data/local/tmp/fext.config中填入我们的目标进程。如果主动调用出现崩溃的情况。可以将class.txt的文件复制到/data/local/tmp/进程名称
来对指定的类进行主动调用。然后打开应用静静的等待脱壳结果。
或者是使用整合怪来对指定类进行处理
Ps1:如果第二次打开应用发现没有触发主动调用,请清理应用:adb shell pm clear packageName
Ps2:如果不想等待60秒,想自己触发fart的主动调用。可以使用frida扩展
Ps3:如果想看logcat日志。搜索fartext即可,日志统一都添加了这个头部。方便查日志。
修复前的数据
使用前面我修改的修复工具,用下面的命令来修复
java -jar dexfixer.jar dexpath binpath outpath
或者是使用我整合怪的工具来修复
修复后的函数结果如下
十一、联合frida扩展 可以结合frida来直接调用FART中准备的函数来对单个类或者类列表进行脱壳。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function romClassesInvoke (classes ) { Java.perform(function ( ) { klog("romClassesInvoke start load" ); var fartExt=Java.use("cn.mik.Fartext" ); if (!fartExt.fartWithClassList){ klog("fartExt中未找到fartWithClassList函数,可能是未使用Fartext的rom" ) return ; } fartExt.fartWithClassList(classes); }) } function romFartAllClassLoader ( ) { Java.perform(function ( ) { var fartExt=Java.use("cn.mik.Fartext" ); if (!fartExt.fartWithClassLoader){ klog("fartExt中未找到fartWithClassLoader函数,可能是未使用Fartext的rom" ); return ; } Java.enumerateClassLoadersSync().forEach(function (loader ) { klog("romFartAllClassLoader to loader:" +loader); if (loader.toString().indexOf("BootClassLoader" )==-1 ){ klog("fart start loader:" +loader); fartExt.fartWithClassLoader(loader); } }) }); }
同时我的整合怪里面也添加了对我这个rom的主动调用和类列表主动调用支持
十二、思考 整个流程梳理完成后,我们可以由此来借鉴来思考延伸一下。
比如,包装一些属于自己的系统层api调用。便于我们使用xposed或者是frida来调用一些功能。
再比如,加载应用时,读取配置文件作为开关,我们来对网络流量进行拦截写入保存,或者对所有的jni函数调用,或者是java函数调用进行trace。这种就属于是rom级别的打桩。
再比如,可以做一个应用来读写作为开关的配置文件,而rom读取配置文件后,对一些流程进行调整。例如控制FART是否使用更深调用。控制是否开启rom级别的打桩。
以上纯属个人瞎想。刚刚入门,想的有点多,以后了解更深了,我再看看如何定制一个专属的rom逆向集合
十三、补充 整理一下前面所有的资料。
文章
FART:ART环境下基于主动调用的自动化脱壳方案
FART正餐前甜点:ART下几个通用简单高效的dump内存中dex方法
拨云见日:安卓APP脱壳的本质以及如何快速发现ART下的脱壳点
将FART和Youpk结合来做一次针对函数抽取壳的全面提升
fart的理解和分析过程
Android ART 虚拟机 - dex 文件格式要旨
dex起步探索
github
FART
Youpk
dexfixer
fridaUiTools
因特殊原因,暂时取消开源。