zoukankan      html  css  js  c++  java
  • 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类调用关系

  • 相关阅读:
    xshell使用密钥登陆服务器
    SQLyog使用隧道登陆数据库
    puttygen.exe生成ppk格式密钥
    xshell生成密钥对
    性能测试基础---jmeter函数二次开发
    Python:基础知识(二)
    异常点检测
    Python:numpy.newaxis
    三张图读懂机器学习:基本概念、五大流派与九种常见算法
    机器学习:样本集、验证集(开发集)、测试集
  • 原文地址:https://www.cnblogs.com/theseventhson/p/14961107.html
Copyright © 2011-2022 走看看