Android-DexVMP快速分析

概述

Android DexVMP主要有以下两种表现形式:

  1. 抽象出一个统一返回值和参数的函数,对这个函数进行VMP保护

DexVMP表现形式1

  1. onCreate Native化,对其进行VMP保护

DexVMP表现形式2

Android DexVMP的实现原理就是对ART解释器进行自定义,壳代码运行在传统ART解释器下,被加固应用运行在自定义解释器下.对于这种情况,我们可以对其函数调用过程进行trace,通过分析某个函数内部调用的其他函数,然后借助Frida进行关键函数的Hook来帮助我们完成对VMP保护的函数进行快速分析,接下来将以Android13为例,进行ART的定制来完成函数调用关系的打印.

定制ART

Java调用关系

Java函数有以下两种执行模式:

  1. 解释模式

  2. 经过dex2oat编译后在quick模式执行

对于所有在解释模式下执行的函数都会经过interpreter.cc中的Execute函数.

 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
//http://aospxref.com/android-13.0.0_r3/xref/art/runtime/interpreter/interpreter.cc
static inline JValue Execute(
    Thread* self,
    const CodeItemDataAccessor& accessor,
    ShadowFrame& shadow_frame,
    JValue result_register,
    bool stay_in_interpreter = false,
    bool from_deoptimize = false) REQUIRES_SHARED(Locks::mutator_lock_) {
  DCHECK(!shadow_frame.GetMethod()->IsAbstract());
  DCHECK(!shadow_frame.GetMethod()->IsNative());

  //----------------------------------------------------------
    if (!stay_in_interpreter && !self->IsForceInterpreter()) {
      jit::Jit* jit = Runtime::Current()->GetJit();
      if (jit != nullptr) {
        jit->MethodEntered(self, shadow_frame.GetMethod());
        if (jit->CanInvokeCompiledCode(method)) {
          JValue result;

          // Pop the shadow frame before calling into compiled code.
          self->PopShadowFrame();
          // Calculate the offset of the first input reg. The input registers are in the high regs.
          // It's ok to access the code item here since JIT code will have been touched by the
          // interpreter and compiler already.
          uint16_t arg_offset = accessor.RegistersSize() - accessor.InsSize();
          ArtInterpreterToCompiledCodeBridge(self, nullptr, &shadow_frame, arg_offset, &result);
          // Push the shadow frame back as the caller will expect it.
          self->PushShadowFrame(&shadow_frame);

          return result;
        }
      }
    }
  }

  ArtMethod* method = shadow_frame.GetMethod();

  DCheckStaticState(self, method);

  // Lock counting is a special version of accessibility checks, and for simplicity and
  // reduction of template parameters, we gate it behind access-checks mode.
  DCHECK_IMPLIES(method->SkipAccessChecks(), !method->MustCountLocks());

  VLOG(interpreter) << "Interpreting " << method->PrettyMethod();

  return ExecuteSwitch(
      self, accessor, shadow_frame, result_register, /*interpret_one_instruction=*/ false);
}

从Android13的Execute函数源码中可以看到,如果Java函数始终运行在解释模式下,那么最终所有流程会走到ExecuteSwitch函数中.

ExecuteSwitch函数的内部流程如下:

 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
//http://aospxref.com/android-13.0.0_r3/xref/art/runtime/interpreter/interpreter.cc#ExecuteSwitch
static JValue ExecuteSwitch(Thread* self,
                            const CodeItemDataAccessor& accessor,
                            ShadowFrame& shadow_frame,
                            JValue result_register,
                            bool interpret_one_instruction) REQUIRES_SHARED(Locks::mutator_lock_)
//http://aospxref.com/android-13.0.0_r3/xref/art/runtime/interpreter/interpreter_switch_impl.h
->ALWAYS_INLINE JValue ExecuteSwitchImpl(Thread* self, const CodeItemDataAccessor& accessor,
                                       ShadowFrame& shadow_frame, JValue result_register,
                                       bool interpret_one_instruction)
//http://aospxref.com/android-13.0.0_r3/xref/art/runtime/interpreter/interpreter_switch_impl-inl.h
->template<bool do_access_check, bool transaction_active>
void ExecuteSwitchImplCpp(SwitchImplContext* ctx) {
  Thread* self = ctx->self;
  const CodeItemDataAccessor& accessor = ctx->accessor;
  ShadowFrame& shadow_frame = ctx->shadow_frame;
  self->VerifyStack();

  uint32_t dex_pc = shadow_frame.GetDexPC();
  const auto* const instrumentation = Runtime::Current()->GetInstrumentation();
  const uint16_t* const insns = accessor.Insns();
  const Instruction* next = Instruction::At(insns + dex_pc);

  DCHECK(!shadow_frame.GetForceRetryInstruction())
      << "Entered interpreter from invoke without retry instruction being handled!";

  bool const interpret_one_instruction = ctx->interpret_one_instruction;
  while (true) {
    const Instruction* const inst = next;
    dex_pc = inst->GetDexPc(insns);
    shadow_frame.SetDexPC(dex_pc);
    TraceExecution(shadow_frame, inst, dex_pc);
    uint16_t inst_data = inst->Fetch16(0);
    bool exit = false;
    bool success;  // Moved outside to keep frames small under asan.
    if (InstructionHandler<do_access_check, transaction_active, Instruction::kInvalidFormat>(
            ctx, instrumentation, self, shadow_frame, dex_pc, inst, inst_data, next, exit).
            Preamble()) {
      DCHECK_EQ(self->IsExceptionPending(), inst->Opcode(inst_data) == Instruction::MOVE_EXCEPTION);
      switch (inst->Opcode(inst_data)) {
      //这里可以看到对不同的OPCODE进行处理
#define OPCODE_CASE(OPCODE, OPCODE_NAME, NAME, FORMAT, i, a, e, v)                                \
        case OPCODE: {                                                                            \
          next = inst->RelativeAt(Instruction::SizeInCodeUnits(Instruction::FORMAT));             \
          success = OP_##OPCODE_NAME<do_access_check, transaction_active>(                        \
              ctx, instrumentation, self, shadow_frame, dex_pc, inst, inst_data, next, exit);     \
          if (success && LIKELY(!interpret_one_instruction)) {                                    \
            continue;                                                                             \
          }                                                                                       \
          break;                                                                                  \
        }
  DEX_INSTRUCTION_LIST(OPCODE_CASE)
#undef OPCODE_CASE
      }
    }
    if (exit) {
      shadow_frame.SetDexPC(dex::kDexNoIndex);
      return;  // Return statement or debugger forced exit.
    }
    if (self->IsExceptionPending()) {
      if (!InstructionHandler<do_access_check, transaction_active, Instruction::kInvalidFormat>(
              ctx, instrumentation, self, shadow_frame, dex_pc, inst, inst_data, next, exit).
              HandlePendingException()) {
        shadow_frame.SetDexPC(dex::kDexNoIndex);
        return;  // Locally unhandled exception - return to caller.
      }
      // Continue execution in the catch block.
    }
    if (interpret_one_instruction) {
      shadow_frame.SetDexPC(next->GetDexPc(insns));  // Record where we stopped.
      ctx->result = ctx->result_register;
      return;
    }
  }
}  // NOLINT(readability/fn_size)

接下来我们来看下ART解释器对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
//http://aospxref.com/android-13.0.0_r3/xref/art/runtime/interpreter/interpreter_switch_impl-inl.h
//其他invoke指令同理,它们调用的流程相同
HANDLER_ATTRIBUTES bool INVOKE_STATIC()
->HANDLER_ATTRIBUTES bool HandleInvoke()
//http://aospxref.com/android-13.0.0_r3/xref/art/runtime/interpreter/interpreter_common.h
->static ALWAYS_INLINE bool DoInvoke(Thread* self,
                                   ShadowFrame& shadow_frame,
                                   const Instruction* inst,
                                   uint16_t inst_data,
                                   JValue* result)
->bool DoCall(ArtMethod* called_method, Thread* self, ShadowFrame& shadow_frame,
            const Instruction* inst, uint16_t inst_data, JValue* result)
->static inline bool DoCallCommon(ArtMethod* called_method,
                                Thread* self,
                                ShadowFrame& shadow_frame,
                                JValue* result,
                                uint16_t number_of_inputs,
                                uint32_t (&arg)[Instruction::kMaxVarArgRegs],
                                uint32_t vregC)
//http://aospxref.com/android-13.0.0_r3/xref/art/runtime/common_dex_operations.h
->inline void PerformCall(Thread* self,
                        const CodeItemDataAccessor& accessor,
                        ArtMethod* caller_method,
                        const size_t first_dest_reg,
                        ShadowFrame* callee_frame,
                        JValue* result,
                        bool use_interpreter_entrypoint)

在PerformCall这个函数中,通过其参数传递,我们可以拿到调用方和被调方的函数名,因此我们在这个地方进行插桩.

插桩代码如下:

 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
inline void PerformCall(Thread* self,
                        const CodeItemDataAccessor& accessor,
                        ArtMethod* caller_method,
                        const size_t first_dest_reg,
                        ShadowFrame* callee_frame,
                        JValue* result,
                        bool use_interpreter_entrypoint)
    REQUIRES_SHARED(Locks::mutator_lock_) {

    //add
    ArtMethod* called = callee_frame->GetMethod();
    std::ostringstream oss;
    oss << "[PerformCall] caller:\t" << caller_method->PrettyMethod() << "\t-->called:\t"<< called->PrettyMethod();
    if(strstr(oss.str().c_str(),"PerformCallFlag")){
        LOG(ERROR) << oss.str();
    }
    //add

  if (LIKELY(Runtime::Current()->IsStarted())) {
    if (use_interpreter_entrypoint) {
      interpreter::ArtInterpreterToInterpreterBridge(self, accessor, callee_frame, result);
    } else {
      interpreter::ArtInterpreterToCompiledCodeBridge(
          self, caller_method, callee_frame, first_dest_reg, result);
    }
  } else {
    interpreter::UnstartedRuntime::Invoke(self, accessor, callee_frame, result, first_dest_reg);
  }
}

另外为了让Java函数始终运行在解释模式下,我们还需要导出一个接口供Frida进行调用.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//http://aospxref.com/android-13.0.0_r3/xref/art/runtime/interpreter/interpreter.cc
namespace art {
  //add
  extern "C" void forceinterpreter(){
        Runtime* runtime=Runtime::Current();
        runtime->GetInstrumentation()->ForceInterpretOnly();
        LOG(WARNING)<<"forceinterpreter is called";
    }
  //add
  //---
}

Jni调用关系

为了解决VMP实现的兼容性和复杂性问题,VMP会使用标准Jni调用java函数的流程来实现对invoke类型指令的解释执行.

  1. 解析invoke指令的参数部分,得到Methodldx
  2. 解析Dex结构,获得当前要调用的Methodldx的类名和函数属性
  3. 调用Jni的Findclass函数,获得对应类的JClass
  4. 调用Jni的GetMethodID或GetStaticMethodID得到函数的MethodID
  5. 调用Jni的CalIXXXMethod,完成对函数的调用

接下来我们以CallStaticIntMethod为例,来看下其内部调用流程.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//http://aospxref.com/android-13.0.0_r3/xref/art/runtime/jni/jni_internal.cc
static jint CallStaticIntMethod(JNIEnv* env, jclass, jmethodID mid, ...)
//http://aospxref.com/android-13.0.0_r3/xref/art/runtime/reflection.cc
->JValue InvokeWithVarArgs(const ScopedObjectAccessAlreadyRunnable& soa,
                         jobject obj,
                         ArtMethod* method,
                         va_list args) REQUIRES_SHARED(Locks::mutator_lock_)
->void InvokeWithArgArray(const ScopedObjectAccessAlreadyRunnable& soa,
                               ArtMethod* method, ArgArray* arg_array, JValue* result,
                               const char* shorty)

我们可以在InvokeWithArgArray这个函数中进行插桩,来完成函数调用关系的打印.

插桩代码如下:

 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
void InvokeWithArgArray(const ScopedObjectAccessAlreadyRunnable& soa,
                               ArtMethod* method, ArgArray* arg_array, JValue* result,
                               const char* shorty)
    REQUIRES_SHARED(Locks::mutator_lock_) {
  uint32_t* args = arg_array->GetArray();
  if (UNLIKELY(soa.Env()->IsCheckJniEnabled())) {
    CheckMethodArguments(soa.Vm(), method->GetInterfaceMethodIfProxy(kRuntimePointerSize), args);
  }

  //add
  //ArtMethod* native_method = *self->GetManagedStack()->GetTopQuickFrame();
    ArtMethod* artMethod = nullptr;
    Thread* self = Thread::Current();
    const ManagedStack* managedStack= self->GetManagedStack();
    if(managedStack != nullptr){
        ArtMethod** tmpartmethod= managedStack->GetTopQuickFrame();
        if(tmpartmethod != nullptr){
            artMethod = *tmpartmethod;
        }
    }
    if(artMethod != nullptr) {
        std::ostringstream oss;
        oss << "[InvokeWithArgArray]beforeCall caller:\t" << artMethod->PrettyMethod() << "\t-->called:\t"<< method->PrettyMethod();
        if(strstr(oss.str().c_str(),"InvokeWithArgArrayFlag")){
            LOG(ERROR) << oss.str();
        }
    }
  //add

  method->Invoke(soa.Self(), args, arg_array->GetNumBytes(), result, shorty);

  //add
  if(artMethod != nullptr) {
    std::ostringstream oss;
    oss << "[InvokeWithArgArray]after Call caller:\t" << artMethod->PrettyMethod() << "\t-->called:\t"<< method->PrettyMethod();
    if(strstr(oss.str().c_str(),"InvokeWithArgArrayFlag")){
        LOG(ERROR) << oss.str();
    }
  }
  //add
}

Hook代码

上面我们对Android源码进行了修改,还需要借助Frida来完成函数调用关系的打印,对应的代码如下:

 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
function forceinterpreter() {
    var libartmodule = Process.getModuleByName("libart.so");
    var forceinterpreter_addr = libartmodule.getExportByName("forceinterpreter");
    console.log("forceinterpreter:" + forceinterpreter_addr);
    var forceinterpreter = new NativeFunction(forceinterpreter_addr, "void", []);
    Interceptor.attach(forceinterpreter_addr, {
        onEnter: function (args) {
            console.log("go into forceinterpreter");
        },
        onLeave: function (retval) {
            console.log("leave forceinterpreter");
        }
    });
    forceinterpreter();
}

function hook_start() {
    var libcModule = Process.getModuleByName("libc.so");
    var strstr = libcModule.getExportByName("strstr");
    Interceptor.attach(strstr, {
        onEnter: function (args) {
            this.arg0 = args[0];
            this.arg1 = args[1];
            this.method_name = ptr(this.arg0).readUtf8String();
            this.call_name = ptr(this.arg1).readUtf8String();

            if (this.call_name.indexOf("PerformCallFlag") != -1) {
                console.log(this.method_name);
            }

            if (this.call_name.indexOf("InvokeWithArgArrayFlag") != -1) {
                console.log(this.method_name);
            }

        },
        onLeave: function (retval) {
            if (this.call_name.indexOf("InvokeWithArgArrayFlag") != -1 || this.call_name.indexOf("PerformCallFlag") != -1) {
                retval.replace(0);
            }

        }
    })
}

function main() {
    forceinterpreter();
    hook_start();
}

setImmediate(main)

示例

这里我们以某VMP样本为例,进行函数调用关系的trace.

加固前代码如下:

加固前

加固后代码如下:

加固后

函数trace如下:

函数调用trace

总结

VMP的加固强度很强,但是我们可以通过对Android解释模式以及Jni调用流程进行分析,在其中某些流程进行插桩并且和Frida进行结合,打印出被VMP保护函数的函数调用.然后再进一步分析,对关键函数进行Hook拿到我们想要的信息.

参考链接

VMP逆向—-仿method profiling跟踪jni函数执行


相关内容

0%