android逆向奇技淫巧十四:定制art内核(二):VMP逆向----仿method profiling跟踪jni函数执行

   1、对于逆向工作而言,最重要的工作内容之一就是trace了! 目前trace分两种场景:

  (1)dex-VMP壳、java层关键(比如加密)代码定位:这时需要trace函数的调用关系,目前已有android studio自带的method profiling工具可以干这事!

  (2)so层代码定位:

    • 函数级别的trace,查看c/c++函数的调用关系,已有现成的frida-trace功能;
    • 加密算法的还原,此时需要汇编指令级别的trace,IDA有该功能;配合用户自定义的python脚本效果更加!

    因为手上有一个数字公司的VMP壳,所以今天先看看第一种java层trace的函数调用关系!有些同学可能就会问了:android studio不是自带了method profiling了么?为啥不直接用了?重复造轮子有意义么?( ̄▽ ̄)"

    method profiling用来trace java层函数还存在缺陷:由于是固定死的,又没有提供接口,所以没法在这中间打印其他的关键信息,包括但不限于:函数参数内容、registerNative函数注册地址、java调用so层的函数,这些都在一定程度上成为了逆向的绊脚石。今天就分享一种定制art内核的办法trace函数的执行,并且还能根据自己的业务需求灵活打印其他所需的信息!

  2、既然是定制art内核,肯定就涉及到修改art的代码了。art代码辣么多,应该修改哪些地方了?

  (1)quick_jni_entrypoints.cc文件中的JniMethodStart方法:jni方法在被调用前会先执行这个方法,这里可以通过strstr挂钩!

extern uint32_t JniMethodStart(Thread* self) {
  JNIEnvExt* env = self->GetJniEnv();
  DCHECK(env != nullptr);
  uint32_t saved_local_ref_cookie = bit_cast<uint32_t>(env->local_ref_cookie);
  env->local_ref_cookie = env->locals.GetSegmentState();
  ArtMethod* native_method = *self->GetManagedStack()->GetTopQuickFrame();
  std::ostringstream oss;
  oss<<"[JniMethodStart]name:"<<native_method->PrettyMethod().c_str()<<",addr:"<<native_method->GetEntryPointFromJni();
  if(strstr(oss.str().c_str(),"JniMethodStartflag")!=nullptr){
      LOG(WARNING)<<oss.str();
  }
  if (!native_method->IsFastNative()) {
    // When not fast JNI we transition out of runnable.
    self->TransitionFromRunnableToSuspended(kNative);
  }
  return saved_local_ref_cookie;
}

  (2)reflection.cc文件中的InvokeWithArgArray方法: jni调用jni、jni调用java则会通过反射相关的InvokeWithArgArray方法最终调用ArtMethod的Invoke方法来实现,如下:

static 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()->check_jni)) {
    CheckMethodArguments(soa.Vm(), method->GetInterfaceMethodIfProxy(kRuntimePointerSize), args);
  }
    //before invoke
    //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:" << artMethod->PrettyMethod() << "---called:"<< method->PrettyMethod();
        if(strstr(oss.str().c_str(),"InvokeWithArgArrayBefore")){
            LOG(ERROR)<<oss.str();
        }

    }
    //add
  method->Invoke(soa.Self(), args, arg_array->GetNumBytes(), result, shorty);
    //add
    if(artMethod!= nullptr){
        std::ostringstream oss;
        oss << "[InvokeWithArgArray]aftercall caller:" << artMethod->PrettyMethod() << "---called:"<< method->PrettyMethod();
        if(strstr(oss.str().c_str(),"InvokeWithArgArrayAfter")){
            LOG(ERROR)<<oss.str();
        }
    }
    //add
}

  (3)interpreter.cc:art解释器一般都有switch/case、汇编等不同的smail执行方式;为了便于hook,这里需要强制使用swithc/case形式的解释器,代码如下:

    extern "C" void forceinterpreter(){
        Runtime* runtime=Runtime::Current();
        runtime->GetInstrumentation()->ForceInterpretOnly();
        LOG(WARNING)<<"forceinterpreter is called";
    }

       如下,代码这么放:

  

  (4)common_dex_operatioin.h中的PerformCall方法:jni方法都是在so层用c语言实现的,这里的c语言最终也需要被执行;执行的方式也可以用解释器,也可以直接用汇编的机器码执行;总之:不论以哪中方式执行,PerformCall这个函数是必经之路,适合挂钩打印! caller就是调用者,callee就是被调用者

inline void PerformCall(Thread* self,
                        const DexFile::CodeItem* code_item,
                        ArtMethod* caller_method,
                        const size_t first_dest_reg,
                        ShadowFrame* callee_frame,
                        JValue* result)
    REQUIRES_SHARED(Locks::mutator_lock_) {
    //add
    ArtMethod* called=callee_frame->GetMethod();
    std::ostringstream oss;
    oss << "[PerformCall]caller:" << caller_method->PrettyMethod() << "---called:"<< called->PrettyMethod();
    if(strstr(oss.str().c_str(),"PerformCallbeforerunflag")){
        LOG(ERROR)<<oss.str();
    }
    //add

  if (LIKELY(Runtime::Current()->IsStarted())) {
    ArtMethod* target = callee_frame->GetMethod();
    if (ClassLinker::ShouldUseInterpreterEntrypoint(
        target,
        target->GetEntryPointFromQuickCompiledCode())) {
      interpreter::ArtInterpreterToInterpreterBridge(self, code_item, callee_frame, result);
    } else {
      interpreter::ArtInterpreterToCompiledCodeBridge(
          self, caller_method, code_item, callee_frame, result);
    }
  } else {
    interpreter::UnstartedRuntime::Invoke(self, code_item, callee_frame, result, first_dest_reg);
  }
    if(strstr(oss.str().c_str(),"PerformCallafterrunflag")){
        LOG(ERROR)<<oss.str();
    }
}

  (5)ArtMethod.cc中的RegisterNative方法:可以通过hook参数查出native方法的名称和对应的注册地址;

const void* ArtMethod::RegisterNative(const void* native_method, bool is_fast) {
  CHECK(IsNative()) << PrettyMethod();
  CHECK(!IsFastNative()) << PrettyMethod();
  CHECK(native_method != nullptr) << PrettyMethod();
  if (is_fast) {
    AddAccessFlags(kAccFastNative);
  }
    std::ostringstream oss;
    oss <<"[ArtMethod::RegisterNative]" <<this->PrettyMethod()<<"--addr:"<<native_method;
    if(strstr(oss.str().c_str(),"RegisterNativeflag")!=nullptr){
        LOG(ERROR)<<this->PrettyMethod()<<"--addr:"<<native_method;
    }

  void* new_native_method = nullptr;
  Runtime::Current()->GetRuntimeCallbacks()->RegisterNativeMethod(this,
                                                                  native_method,
                                                                  /*out*/&new_native_method);
  SetEntryPointFromJni(new_native_method);
  return new_native_method;
}

  (6)最后一个PopLocalReference:jni函数执行完后的收尾工作,也可以插桩打印日志!

  

  理论上讲:art虚拟机中执行jni函数的整个流程都可以插桩,不局限于上述那几个函数;详细的art执行类方法过程分析解读可以参考文章末尾的链接1(墙裂推荐);

  2、源码改好了,现在该用frida去hook了,代码如下:

function LogPrint(log) {
    var threadid = Process.getCurrentThreadId();
    var theDate = new Date();
    var hour = theDate.getHours();
    var minute = theDate.getMinutes();
    var second = theDate.getSeconds();
    var mSecond = theDate.getMilliseconds();
    hour < 10 ? hour = "0" + hour : hour;
    minute < 10 ? minute = "0" + minute : minute;
    second < 10 ? second = "0" + second : second;
    mSecond < 10 ? mSecond = "00" + mSecond : mSecond < 100 ? mSecond = "0" + mSecond : mSecond;
    var time = hour + ":" + minute + ":" + second + ":" + mSecond;
    console.log("tid:" + threadid + "[" + time + "]" + "->" + log);

}

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 hookstrstr() {
    var libcmodule = Process.getModuleByName("libc.so");
    var strstr_addr = libcmodule.getExportByName("strstr");
    Interceptor.attach(strstr_addr, {
        onEnter: function (args) {
            this.arg0 = ptr(args[0]).readUtf8String();
            this.arg1 = ptr(args[1]).readUtf8String();
            if (this.arg1.indexOf("InvokeWithArgArray") != -1) {
                LogPrint(this.arg1 + "--" + this.arg0);
            }
            if (this.arg1.indexOf("RegisterNative") != -1) {
                LogPrint(this.arg1 + "--" + this.arg0);
            }
            if (this.arg1.indexOf("PerformCall") != -1) {
                LogPrint(this.arg1 + "--" + this.arg0);
            }
            if (this.arg1.indexOf("JniMethod") != -1) {
                LogPrint(this.arg1 + "--" + this.arg0);
            }
        }
    })
}

function main() {
    forceinterpreter();//这里强制调用我们预先埋好的forceinterpreter函数,强制使用interpreter模式
    hookstrstr();

}

setImmediate(main);

  效果如下:可以看到java层的interface11函数调用了registerNative注册了MainActivity(就是这里把java函数强行改成native函数的),然后就结束了,其他啥事也没干!

  

   两个重要的android框架函数也悉数登场:

       

   

   为了脱壳,也为了弄清楚到底是哪个函数脱的壳,这里也可以直接继续hook这两个函数试试,代码如下:

var savedexpath="/data/data/com.example.classloadertest/save.dex";
var number=0;
function savedex(savepath,bytes) {
    Java.perform(function () {
        var FileOutPutStreamClass=Java.use("java.io.FileOutputStream");
        var fou=FileOutPutStreamClass.$new(savepath);
        fou.write(bytes);
        fou.close();
    })
}
function enumerateClassloader() {
    Java.perform(function () {
        Java.enumerateClassLoadersSync().forEach(function (classloader) {//遍历Bootstrp、ExtClassLoader、AppClassLoader等classloader,看看到底是哪个加载了MainActivity
            try {
                var MainActivityClass = classloader.findClass("com.example.classloadertest.MainActivity");//找到MainActivity类
                var dex = MainActivityClass.getDex();//从内存dump脱壳,这里得到dex对象
                var bytes = dex.getBytes();
                number=number+1;
                savedex(savedexpath+number,bytes);
                LogPrint("find class success!" + dex);
            } catch (e) {
                LogPrint(e);
            }

        })
    })

}

function main() {
    Java.perform(function () {
        //android.app.Application.attach(android.content.Context)

        var ApplicationClass = Java.use("android.app.Application");
        ApplicationClass.attach.implementation = function (arg0) {//分别在attach执行前后dump内存的dex,看看有没有解密dex
            console.log("attach->before call attachBaseContext");
            enumerateClassloader();
            var result = this.attach(arg0);
            console.log("attach->after call attachBaseContext");
            enumerateClassloader();
            return result;
        }
        var InstrumentationClass = Java.use("android.app.Instrumentation");
        InstrumentationClass.callApplicationOnCreate.implementation = function (arg0) {//分别在callApplicationOnCreate执行前后dump内存的dex,看看有没有解密dex
            console.log("callApplicationOnCreate->before call onCreate");
            enumerateClassloader();
            var result=this.callApplicationOnCreate(arg0);
            console.log("callApplicationOnCreate->after call onCreate");
            enumerateClassloader();
            return result;
        }
    })

}

setImmediate(main);

  hook后,大家可以直接在/data/data/com.example.classloadertest/目录下找到5个脱壳后的dex文件了!

  总的来说: method profiling是可以打印java的函数调用,但是由于没有开放接口,用户没法扩展,非常死板! 通过hook整个函数调用链各个环节的函数,能清晰地展示jni函数的注册和调用过程!还能顺带打印部分参数、函数返回值。站在逆向角度,远比method profiling方便!顺带还能hook MainActivity来整体dump dex,达到脱壳的目的!

 

补充:

1、在阅读art源码的时候,经常遇到各种带“entry、invoke”等字眼的类、方法,通过通读代码,发现最终都是通过内联汇编、使用BLX跳转的,核心代码如下:

ENTRY art_quick_invoke_stub_internal
    SPILL_ALL_CALLEE_SAVE_GPRS             @ spill regs (9)

    ldr    ip, [r0, #ART_METHOD_QUICK_CODE_OFFSET_32]  @ get pointer to the code
    blx    ip                              @ call the method

 2、C++类成员函数的第一个参数:this指针,或则说是成员变量!由R0指向,R1才是用户传入的第一个参数

  如果类有虚函数,this指针起始位置指向虚函数表,也就是前面4个字节指向虚函数表,从第5个字节开始才指向成员变量

 

参考:

1、https://www.jianshu.com/p/2ff1b63f686b  Android ART执行类方法的过程

2、https://bbs.pediy.com/thread-263189.htm  使用frida打印java类调用关系

posted @ 2021-07-02 20:26  第七子007  阅读(3143)  评论(0编辑  收藏  举报