查看原文
其他

目录重大更新!《安卓高级研修班》2022年春季班开始招生!

看雪高研 看雪学苑 2022-07-01

优秀学员作品:《FartExt之优化更深主动调用的FART10》

参考:

  • 将FART和Youpk结合来做一次针对函数抽取壳的全面提升https://bbs.pediy.com/thread-260052.htm

  • 看雪高研班课程

 

寒冰大佬的FART带动了不少新的主动调用思想的抽取壳方案,看了上面这篇文章感觉意犹未尽,当然是要动手实践一翻来优化一波。于是我重新翻阅FART和Youpk的源码,我准备和那位大佬一样,参考Youpk在FART的基础上进行升级改造。

开工前先明确出我的需求:

  • 对指定进程脱壳,非目标进程不要执行脱壳线程

  • 对指定类列表进行脱壳

  • 将FART升级到aosp10实现

  • 去FART指纹

  • FART保存出的函数修复合并为dex

  • 实现FART更深的主动调用

  • 更快的主动调用(暂未优化)



1


什么是FART

 
还未了解过的请看原作者对于fart的介绍:
1、FART:ART环境下基于主动调用的自动化脱壳方案https://bbs.pediy.com/thread-252630.htm

2、FART正餐前甜点:ART下几个通用简单高效的dump内存中dex方法
https://bbs.pediy.com/thread-254028.htm

3、拨云见日:安卓APP脱壳的本质以及如何快速发现ART下的脱壳点
 https://bbs.pediy.com/thread-254555.htm

关于fart源码的调用流程可以看看我以前整理的一篇文章:
fart的理解和分析过程
https://bbs.pediy.com/thread-263401.htm
 
简单总结:这是一个基于主动调用来脱抽取壳的方案。
 
简述实现原理:在进程启动的时候通过双亲委派机制遍历所有classloader,然后遍历里面的所有class,取出所有函数,直接调用。然后在ArtMethod的Invoke函数这里根据参数判断出这是主动调用触发的,然后就取消函数的正常执行,并执行脱壳操作。


2


对指定进程脱壳

 
首先看看FART源码中,脱壳线程的入口点ActivityThread.java的performLaunchActivity这个函数中开始的FART处理。
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ... fartthread(); ... }

也就是说,所有的进程都会执行脱壳的流程。所以这个地方我觉得还是有必要优化的。应该是对指定的进程脱壳会更加好一些。Youpk中已经有了这个优化,所以我直接拿Youpk的处理来使用。下面是修改后的入口启动部分:
//判断这个进程是否应该脱壳 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;} //启动FART脱壳线程public static void fartthread() {
if (!shouldUnpack()) { return; }
new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub try { Log.e("ActivityThread", "start sleep......"); Thread.sleep(1 * 60 * 1000); } catch (InterruptedException e) { // TODO Auto-generated catch block 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线程启动前加了个判断,在配置文件中的进程才需要脱壳。这样基本第一个优化就完成了。


3


对指定类列表进行脱壳


由于应用的有些类可能是被特殊处理了,主动调用的情况会导致程序崩溃或者退出。所以最好是可以单独对某些类进行主动调用。我调整了FART线程启动的逻辑,先是上面的判断是不是要脱壳的目标进程,然后判断有没有设定类列表,如果有类列表就只脱壳类列表,否则就完整主动调用。
//读取类列表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; } ... }


4


FART升级AOSP10

 
直接将FART修改的代码部分直接替换到AOSP10中。毫不意外的出现了一堆错误。不过问题比较集中。主要是对于CodeItem的成员访问方式发生了变化。这里可以参考文章:Android ART 虚拟机 - dex 文件格式要旨https://www.jianshu.com/p/18ff0b8e0b01
 
根据这篇文章中对CodeItem对象新的访问方式,对FART的源码部分做出修改。
 
修改文件是art_method.cc,我这里只贴上部分关键修改的代码:
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的使用以及命名空间的修改,这里我就不全部贴出了,最后我会贴出改完后的版本。


5


去FART指纹

 
由于FART的知名度还是挺高的,所以最好还是把FART中特有的一些函数名和文件保存路径给修改一下,下面整理下我参考Youpk做的一些修改。
 
1、将ActivityThread中的FART相关函数全部单独放到一个类cn.mik.Fartext中。这样如果别人对ActivityThread的函数检测就找不到FART相关的了。
 
2、将DexFile中的dumpMethodCode函数名修改为fartextMethodCode
 
3、将myfartInvoke函数名改成fartextInvoke
 
4、将所有使用/sdcard/fart的这个路径全部修改成/sdcard/fext
 
把这些常见的可能识别的方式都修改之后,一般就识别不出来了。我这种完全没知名度的,想必不会被人检测到了。



6


FART的函数修复

 
抽取壳的应用在脱壳后,有两种文件,一个是在当前时机dump出来的dex文件,另一个是保存codeitem出来的bin文件。
 
FART的修复组件是使用开源项目FART中那个py的脚本来解析dex文件,将bin的codeitem修复打印。对于里面的代码解析部分我之前也写了文章。感兴趣可以看看:dex起步探索https://bbs.pediy.com/thread-268465.htm
 
但是我仔细研究后,发现修复组件只进行了打印,。并没有修复成dex,而是直接解析打印。最理想的还是修复到dex,方便使用静态分析工具查看,有大佬也已经写了这个工具。

那就是前面参考的Youpk,。他的内部有个dexfixer的目录,就是实现了对导出的codeitem数据修复到dex中。不过他的codeitem的保存结构和FART的并不大一样,没关系,修改一下codeitem文件的解析部分就好了。

下面贴上Youpk和我修改后专门处理fart结果的dexfixer。
Youpk:https://github.com/Youlor/Youpk
dexfixer:https://github.com/dqzg12300/dexfixer


7


更深的主动调用

 

1、FART的主动调用深度


首先当然是看看FART的主动调用的深度是在哪里,这里的深度其实就是在函数的主动执行过程中,FART是在执行到哪个流程时,进行的脱壳处理。下面贴上FART主动调用的脱壳位置:
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时,就根据参数判断是主动调用的情况,就脱壳并结束了。

2、为什么需要更深的主动调用


一般的函数抽取壳,在执行到ArtMethod::Invoke前就已经对抽取函数还原了。但是也有一些抽取壳,执行到Invoke时依然还没有还原函数。譬如下面这种抽取壳:
.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的主动调用深度,发现函数真正执行前就已经被我们直接结束掉了,所以我们需要更深的主动调用才能够解决这个抽取壳。

3、Youpk更深的主动调用


我们回头看看上面的抽取壳,我们的目标是要判断如果这个函数的第一个指令是goto,就正常执行。然后执行到invoke-static的指令,这个指令完成之后就直接结束掉函数调用,避免真实函数调用会出现异常。
 
先参考Youpk的看看他是如何实现更深的主动调用来解决这个问题的。下面是第一步,先修改默认的解释器为Switch的解释器。这是因为Switch解释器的可读性更加高,方便我们直接修改源码来达到目的。
static constexpr InterpreterImplKind kInterpreterImplKind = kSwitchImplKind;

然后我们看看主动调用时Youpk是怎么模拟参数的。
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的函数调用模拟。
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函数就正常执行。
void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result, const char* shorty) { ... //patch by Youlor //++++++++++++++++++++++++++++ //如果是主动调用fake invoke并且不是native方法则强制走解释器 if (UNLIKELY(!runtime->IsStarted() || Dbg::IsForcedInterpreterNeededForCalling(self, this) || (Unpacker::isFakeInvoke(self, this) && !this->IsNative()))) { //++++++++++++++++++++++++++++ if (IsStatic()) { art::interpreter::EnterInterpreterFromInvoke( self, this, nullptr, args, result, /*stay_in_interpreter*/ true); } else { mirror::Object* receiver = reinterpret_cast<StackReference<mirror::Object>*>(&args[0])->AsMirrorPtr(); art::interpreter::EnterInterpreterFromInvoke( self, this, receiver, args + 1, result, /*stay_in_interpreter*/ true); } } else { //patch by Youlor //++++++++++++++++++++++++++++ //如果是主动调用fake invoke并且是native方法则不执行 if (Unpacker::isFakeInvoke(self, this) && this->IsNative()) { // Pop transition. self->PopManagedStackFragment(fragment); return; } //++++++++++++++++++++++++++++ ... } ...}

接下来看解释器的EnterInterpreterFromInvoke函数处理,这里Youpk没有什么处理。
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。
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修改的部分了,先看看修改的代码:
//patch by Youlor//++++++++++++++++++++++++++++#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,让他正常执行先,下面看看里面的逻辑处理:
//继续解释执行返回false, dump完成返回truebool 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);
//对于一般的方法抽取(非ijiami, najia), 直接在第一条指令处dump即可 if (inst_count == 0 && opcode != Instruction::GOTO && opcode != Instruction::GOTO_16 && opcode != Instruction::GOTO_32) { Unpacker::dumpMethod(method); return true; } //ijiami, najia的特征为: goto: goto_decrypt; nop; ... ; return; const vx, n; invoke-static xxx; goto: goto_origin; 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) { //写入时将第一条GOTO用nop填充 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下面的处理:
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) { ... //patch by Youlor //++++++++++++++++++++++++++++ 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; } ... } //patch by Youlor //++++++++++++++++++++++++++++ bool dumped = Unpacker::afterInstructionExecute(self, shadow_frame.GetMethod(), dex_pc, inst_count); if (dumped) { return JValue(); } //++++++++++++++++++++++++++++ } while (!interpret_one_instruction); // Record where we stopped. shadow_frame.SetDexPC(inst->GetDexPc(insns)); return result_register;} // NOLINT(readability/fn_size)

这里就看到每个指令都执行了PREAMBLE函数。然后每个指令执行完都执行了afterInstructionExecute这个函数。

在这里就可以判断,如果执行完的指令是INVOKE_STATIC,就可以直接return结束掉函数执行了,看看Youpk的处理:
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来控制下一个指令执行的时候来进行退出函数,我感觉这里退出应该也没啥问题。

到这里基本就走完大致的流程了,那么欣赏完别人的代码,可以开始我们的改造工作了。

4、FartExt更深的主动调用


和Youpk一样,第一步就是先把解释器给改成使用Switch解释器。但是由于我使用的是AOSP10,所以发现修改部分果然不大一样了。
#if ART_USE_CXX_INTERPRETERstatic constexpr InterpreterImplKind kInterpreterImplKind = kSwitchImplKind;#elsestatic constexpr InterpreterImplKind kInterpreterImplKind = kMterpImplKind;#endif

发现这里变成可以通过编译参数来控制的了,搜索一下:ART_USE_CXX_INTERPRETER的使用
if envTrue(ctx, "ART_USE_CXX_INTERPRETER") { cflags = append(cflags, "-DART_USE_CXX_INTERPRETER=1") }

发现这个好像可以通过cflags来配置了,所以我修改了下runtime下的Android.pb。如果不想改全局的,也可以在源码里面直接判断是主动调用就强制走switch解释器。
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在后续判断是否为主动调用:
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) { ... //add 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, /*stay_in_interpreter=*/ true); }else{ //注意这里是把非静态的也当静态的方式处理的。避免使用引用类型参数。 art::interpreter::EnterInterpreterFromInvoke( self, this, nullptr, args + 1, result, /*stay_in_interpreter=*/ true); } LOG(ERROR) << "fartext artMethod::Invoke Method Over "<<this->PrettyMethod().c_str(); self->PopManagedStackFragment(fragment); return; } //add end ...}

这里有个问题是上面这种模拟参数的方式,碰到引用类型的参数会报错。所以在处理参数入栈的时候,也要进行判断处理一下。
void EnterInterpreterFromInvoke(Thread* self, ArtMethod* method, ObjPtr<mirror::Object> receiver, uint32_t* args, JValue* result, bool stay_in_interpreter) { ... if (!method->IsStatic()) { //add 避免使用引用类型的参数 if(result!=nullptr&&result->GetI()==111111){ shadow_frame->SetVReg(cur_reg, args[0]); }else{ CHECK(receiver != nullptr); shadow_frame->SetVRegReference(cur_reg, receiver); } //add end //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': { //add 避免使用引用类型的参数 if(result!=nullptr&&result->GetI()==111111){ shadow_frame->SetVReg(cur_reg, args[0]); break; } //add end 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解释器的逻辑发生了较大的变动,先看看变成了啥样子:
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); //PREAMBLE变成这种方式调用了 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(); \ /* TODO: Advance 'inst' here, instead of explicitly in each handler */ \ if (UNLIKELY(exit_loop)) { \ return; \ } \ break; \ }DEX_INSTRUCTION_LIST(OPCODE_CASE)#undef OPCODE_CASE } if (UNLIKELY(interpret_one_instruction)) { break; } } // Record where we stopped. shadow_frame.SetDexPC(inst->GetDexPc(insns)); ctx->result = ctx->result_register; return;} // NOLINT(readability/fn_size)

看到了这两个部分都发生了较大的变化,那个超大的case都不见了,不过也只是处理的方式发生变化,我们跟着调整下就行了。
template<bool do_access_check, bool transaction_active>ATTRIBUTE_NO_SANITIZE_ADDRESS void ExecuteSwitchImplCpp(SwitchImplContext* ctx) { ... //add int32_t regvalue=ctx->result_register.GetI(); //这里很重要。需要把我们用来作为主动调用的值给改了。不然调用另外一个函数也会当成fart的主动调用的。 ctx->result_register=JValue(); int inst_count = -1; //当前第几个指令 bool flag=false; //第一个指令是否为goto //add end bool const interpret_one_instruction = ctx->interpret_one_instruction; while (true) { ... //add inst_count++; uint8_t opcode = inst->Opcode(inst_data) //如果是主动调用 if(regvalue==111111){ //第一个指令是goto的处理 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; } } //第二个指令是const的处理 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; } } } //add end 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(); \ /* TODO: Advance 'inst' here, instead of explicitly in each handler */ \ if (UNLIKELY(exit_loop)) { \ return; \ } \ break; \ }DEX_INSTRUCTION_LIST(OPCODE_CASE)#undef OPCODE_CASE } //add //指令执行结束后,再判断一下是不是主动调用的 if(regvalue==111111){ //如果这是第3个指令 if(inst_count==2&&flag){ //如果是下面两种操作码,就可以脱壳并结束了。 if(opcode == Instruction::INVOKE_STATIC || opcode == Instruction::INVOKE_STATIC_RANGE){ dumpArtMethod(shadow_frame.GetMethod()); ArtMethod::disableFartextInvoke(); break; } } //如果主动调用的情况还能执行到第4个指令。那就直接脱壳并结束掉。 if(inst_count>2){ dumpArtMethod(shadow_frame.GetMethod()); ArtMethod::disableFartextInvoke(); break; } } //add end if (UNLIKELY(interpret_one_instruction)) { break; } } // Record where we stopped. shadow_frame.SetDexPC(inst->GetDexPc(insns)); ctx->result = ctx->result_register; return;} // NOLINT(readability/fn_size)



8


流程图






9


更快的主动调用(暂未优化)


在测试FART的主动调用中发现,主动调用的耗时较长,根据上面的流程图。我们可以看到调用最耗时最核心的函数dumpArtMethod,就是在这里进行脱壳的,先看看FART里面做了什么:
extern "C" void dumpArtMethod(ArtMethod* artmethod) REQUIRES_SHARED(Locks::mutator_lock_) { //存放保存的dex路径 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(); // Start of data. size_t size_=dex_file->Size(); // Length of data.
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); //文件大小_dexfile.dex 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{ //dex的数据保存 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); //保存对应的classlist 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);
} } } //获取codeItem const DexFile::CodeItem* code_item = artmethod->GetCodeItem(); if (LIKELY(code_item != nullptr)) { int code_item_len = 0; uint8_t *item=(uint8_t *) code_item; //计算codeitem的大小 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; } //下面就是获取codeitem的idx和偏移,大小之类的。然后写入数据保存了 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文件,这样就能提高一定的效率。这里我就暂时不修改了,毕竟在这里脱整体壳也有一定的优势。如果想要更快的速度,也可以选择过滤主动调用的范围,来降低调用频率。


10


实战测试

 
编译完成之后,我们就可以来试试深度主动调用+dex修复的效果,为了防止风险,就不放测试的apk样本了。
 
安装好apk后,先去/data/local/tmp/fext.config中填入我们的目标进程。如果主动调用出现崩溃的情况。可以将class.txt的文件复制到/data/local/tmp/进程名称 来对指定的类进行主动调用,然后打开应用静静的等待脱壳结果,或者是使用整合怪来对指定类进行处理。
 

(1)如果第二次打开应用发现没有触发主动调用,请清理应用:adb shell pm clear packageName

(2)如果不想等待60秒,想自己触发fart的主动调用。可以使用frida扩

(3)如果想看logcat日志。搜索fartext即可,日志统一都添加了这个头部。方便查日志。
 
修复前的数据:
使用前面我修改的修复工具,用下面的命令来修复java -jar dexfixer.jar dexpath binpath outpath,或者是使用我整合怪的工具来修复:
修复后的函数结果如下:



11


联合frida扩展

 
可以结合frida来直接调用FART中准备的函数来对单个类或者类列表进行脱壳。
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的主动调用和类列表主动调用支持:



12


思考

 
整个流程梳理完成后,我们可以由此来借鉴来思考延伸一下。
 
比如,包装一些属于自己的系统层api调用。便于我们使用xposed或者是frida来调用一些功能。
 
再比如,加载应用时,读取配置文件作为开关,我们来对网络流量进行拦截写入保存,或者对所有的jni函数调用,或者是java函数调用进行trace。这种就属于是rom级别的打桩。
 
再比如,可以做一个应用来读写作为开关的配置文件,而rom读取配置文件后,对一些流程进行调整。例如控制FART是否使用更深调用。控制是否开启rom级别的打桩。
 
以上纯属个人瞎想,刚刚入门想的有点多,以后了解更深了,我再看看如何定制一个专属的rom逆向集合。



《安卓高级研修班(网课)》

春季班开始招生!


1、课程内容


2、服务对象

一定基础的初、中级安卓逆向研究员,迫切希望提高自身能力、学习能力强,升职加薪意愿强烈、学习意愿强烈。



3、服务内容
  • 上述列出的两大计划、各八大专题及其包含的二十四个细目;
  • 专属班主任,敦促学习、鼓励士气;良好的抱团学习的氛围;

  • 可以参加《安卓高级研修班》线下班,鼓励线下交流,与大佬谈笑风生;

  • 注意2W班和3W班是完全独立噢,没有交集;



4、开班时间

开班时间:2022年春季开班

PS:以上为总体服务计划,具体课程时间(段)安排以最终合同约定的课程表为准。



5、培训价格

高研网课

就业班

强化班

月薪三万计划

16999元

8599元

月薪两万计划

11199元

5599元

就业班注意事项:
  1. 就业班附带包就业服务(须达到合同规定的毕业标准),签合同保证就业及薪资,达不到退全款;

  2. 就业班有入学考核,缴费成功后进入考核流程,考核不通过退全款;

  3. 考核流程会包括简历筛选、班主任和老师(电话)面试等环节;


强化班注意事项:
  1. 强化班仅去除包就业服务,并且无入学考核,其余与就业班完全相同;

  2. 就业班与强化班一起授课,合计35人一个班,教学上不做任何区分。


金融风险注意事项:
  1. 《安卓高级研修班》全系列无任何金融计划,纯预付;无任何金融套路。

  2. 网络课程为虚拟商品,购买之前可以观看下述试看内容,购买成功之后不接受退款。


6、报名地址

网课月薪三万计划:

https://www.kanxue.com/book-brief-84.htm


扫码立即报名!

网课月薪两万链接:
https://www.kanxue.com/book-brief-83.htm

扫码立即报名!


试看地址:

3W:《ida trace分析非标准算法》

3W:《Fart&frida》

扫码免费试看


2W:《Fart中的脱壳点》
2W:《Dalvik下动态注册原理追踪 》

扫码免费试看



7、认证证书
为了更加针对性、更高效地培养安全人才,为企业的发展和壮大赋能,提高企业在招聘活动中及人才在求职过程中的对接效率,结合看雪自身在安全圈的深厚技术积累沉淀,看雪正式针对《安卓高级研修班》学员推出《看雪安卓应用安全能力认证》。


2w班

3w班

8、联系我们

课程顾问微信: r0ysue(备注“安卓高研网课”)

 
渴望知识和力量的你还在等什么,赶紧报名加入我们吧! 

免责条款

  • 以上所有宣传稿件内容均不作为服务承诺,最终以实际签订培训合同为准。

  • 课程大纲与细目会根据教学反馈不断优化、调整与更新,实际授课可能与宣传主题略有不同;


常见Q&A及预习指南

Q:有优惠么?!有优惠么?!有优惠么?!重要的事情说三遍!!

A:没有任何优惠噢。只送开学大礼包,把我们网课中需要准备的设备和环境直接送给大家。
3W班高研网课开学大礼包:
  • 一部pixel手机(sailfish)(安卓8脱壳镜像)

  • 安卓源码开发编译调试环境SSD移动硬盘500G

2W计划的话大礼包中的手机或移动硬盘二选一。

Q:网课内容与线下班内容一样么?

A:月薪三万计划的内容与线下班的内容是一样的,我们在线下班沉淀大家的切实的需求和疑问,重新编排和制作内容作为网课与大家分享。月薪两万计划的内容由三万计划的讲师全新制作,充分体现工作场景一线的需求,更加贴近实战、实用,有用、好用。


Q:网课内容与线下班内容一样么?

A:目前针对ollvm和vmp,任何所谓的自动化,都是带很多前提和条件限制的;目前最快的还原ollvm或vmp的方法,还是手动分析,一般快则两三日、慢则一两周,基本上可以还原出来。


ollvm或vmp虽然非常复杂,但是并不代表没有取巧和判断的方法;依托于我们丰富的经验,我们会在课上将我们调试和分析的普适方法和一般性及特殊性技巧教给大家,同时带领大家开发属于自己的ollvm和vmp虚拟机,让学员既能够自己给自己的程序加密,又能够分析别人的经过ollvm或vmp保护的算法,这才是我们看雪高级研修班所传达的授人以渔的精神。

Q:非常关心ollvm和vmp,可以详细介绍下还原的方法和细节么?

A:月薪两万计划推荐至少有实际安卓安全岗位工作经验一年以上为宜。初学者可以先看我们安卓版主非虫大佬的《Android软件安全权威指南》等安卓安全书籍进行入门,在看雪论坛看帖发帖提升自身水平,本套课程建议有工作经验的老手前来充电学习。


Q:想报名网课需要什么样的基础?像我这样的初学者可以报名么?

月薪三万计划视大家实际需求而定,一般看得懂目录及想要学习的人自己就懂,大家不用盲目跟风。如果看不懂目录及不理解目录的具体含义及意义,建议先从两万计划学起,多积累技术和经验。


Q:学习三万计划之前,需要先掌握两万计划的基础吗?

A:不需要,互相独立的。月薪两万计划的定位更加偏向工作岗位一线逆向需求,月薪三万计划则更加偏向于高级调试技巧,二者互为补充,相辅相成。有非常多地大佬两个计划一起报名了,我们也会确保直播时间不会冲突。


Q:想报三万的班,真的很想学高级技巧;但两万的班中也有很多是我想了解和学习的,大佬给些建议呢?

A:其实推荐两个班一起报,有好几位大佬就是两个班全报的。因为首先价格真心不贵,其实我们会将直播的时间错开,方便大家同时进修三万和两万计划,学习自己想要学习的、心仪的知识。


Q:直播答疑如果错过了,是否会有直播内容的回放?

A:每一场直播都有回放,在看雪课程中可以观看。


Q:就业班如何报名呢?流程是怎样的呢?

A:就业班是需要考核的。考核流程是先缴费报名,然后开始。会经过简历、(远程)一面和二面。通过之后补差价,不通过退全款。


Q:我已经报名了,趁开班前还想再预习一下,可否给个预习指南,让我好好利用开班前这段时间再恶补一下。
A:在月薪三万计划中,我们学习的主要目标是,掌握调试、分析和还原ollvm、vmp的方法,定制art虚拟机进行自动化脱壳的方法,主要涉及的技术栈是C\C++还原、arm(64),C++开发。

因此首先推荐邓凡平先生的《深入理解Android:Java虚拟机art》,里面的第五章详细讲解了art虚拟机的实现语言C++11,是阅读art源代码必备的知识;其余部分也详细讲解了Class文件、dex文件和ELF文件的格式和内容,以及art虚拟机的编译、runtime、解释执行、内存、线程等art的技术细节;

推荐的第二本书是《C++反汇编与逆向分析技术揭秘》,按照书中的方法自己编写实验代码对C++使用ndk编译后arm汇编进行对照,掌握c++数据类型、控制流、函数和类在编译后arm汇编的表现形式;希望大家预先掌握这些知识,即使现在不开始看,开课后也会要求大家必须掌握。

在月薪两万计划中,我们更加注重的是实际工作中遇到的各种场景、实际工作能力的提升,及解决实际问题的能力。因此各种逆向环境的搭建、逆向的综合能力和利用代码的编写是最关键的,这里主要涉及的技术栈也是比较杂的:比如网络、Ubuntu/安卓系统知识、应用安全开发、Frida/JS/Python等等、Java技巧,比较考验学员的计算机综合技术基础水平。

因此我们从工作实践中的需求出发,推荐大家首先强化安卓Java代码的开发、及各种网络和接口的知识,这两大技能被大量应用到应用安全、漏洞检测、渗透测试、黑灰攻防等方向,我们并不推荐具体的书目,只要涉及Java、安卓和网络的图书,都可以。有句话叫做开发的高度,决定了你逆向的高度,希望大家利用好开班前的时间,强化一下Java和网络开发的能力。



2021年度优秀学员作品展示:

# 十一月

《使用frida-net脱离pc在手机上直接暴漏app的算法供三方调用》《Frida分析违法应用Native层算法》《Frida实战:一次违法应用的破解尝试》《使用unidbg破解孤挺花字符串混淆并修复so》《破解某抢票软件的VPN抓包》《从SSL库的内存漫游开发dump自定义客户端证书的通杀脚本》

# 十月

《dexvmp后的算法逆向分析和还原》

《使用unicorn对ollvm字符串进行解密》

《Frida追踪定Socket接口自吐游戏APK的服务器IP和地址》

 Frida hook Java/Native与init_array 自吐最终方案 》


# 九月

《macOS安装调试llvm入门》

《fart的理解和分析过程》

《使用ollvm自定义简单的字符串加密》

《使用ida trace来还原ollvm混淆的非标准算法》


# 八月

ollvm算法还原案例分享

使用Frida打印Java类函数调用关系

# 七月

一个易上手的函数抽取样本还原

一个自定义classloader的函数抽取壳样本

利用Xposed对ollvm后的so中flag爆破

使用Frida分析动态注册jni函数绑定流程

frida跟踪应用中所有运行在解释模式的java函数


# 六月

举杯邀Frida,对影成三题

从三道题目入手入门frida

单纯使用Frida书写类抽取脱壳工具的一些心路历程和实践

某聊天app的音视频通话逆向


# 五月

记一次so文件动态解密

使用Frida简单实现函数粒度脱壳

初试IDA&FRIDA联合调试简单ollvm保护的加密函数源码

ollvm算法还原案例分享

# 四月

java函数转Native化的一些实践

某抽取壳的原理简析

frida辅助脱壳

一款最简单的关于动态注册的APP分析

# 三月

ollvm后的算法还原案例分享

ollvm CrackMe算法分析

ART下Hook系统函数修改内存中指定方法的运行指令逻辑案例分享

某类抽取加固APP的脱壳与修复


报 名 地 址



网课月薪三万计划:

https://www.kanxue.com/book-brief-84.htm


扫码立即报名!

网课月薪两万链接:
https://www.kanxue.com/book-brief-83.htm

扫码立即报名!



课程顾问微信: r0ysue(备注“安卓高研网课”)

 




球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存