zoukankan      html  css  js  c++  java
  • android逆向奇技淫巧十三:定制art内核(一):跟踪jni函数注册和调用,绕过反调试

      1、从kanxue上拿到了一个VMP样本,用GDA打开,发现是数字壳;从androidMainfest可以找到原入口,但是已经被壳抽取了,如下:

      

       这种情况我也不知道加壳后的apk从哪开始执行(之前分析的壳都是从壳自定义的MainActivity开始,这里还是保留了原入口,但从包结构看原入口已经没了)!java层静态分析的路走不通了,继续看so;lib目录下有个libjiagu_art.so,但却是0KB,很明显也不是;继续翻找其目录,在assets目录下找到了appkey、libjiagu.so和libjiagu_x86.so; appkey疑似某个key,在哪用现在还不清楚(有可能是解密opcode或其他关键代码的);libjiagu.so疑似包括了vmp解释器,用ida打开康康了:

      

       第一个看的肯定是入口函数Jni_onload啦,如上:结果很失望,没有RegisterNativeMethod方法调用,java层有大量的native方法不知道在哪去找实现!这里肯定也被故意隐藏了!至此:java层和so层都没法进一步静态分析,唯一能指望的只剩调试了!由于jni_onload这里明显被做了手脚,那么在linker下个断点呗,理论上会在init_array做一些解密的工作,否则jni_onload是没法正常执行的!于是乎用IDA选择加载so时断下:

      

       顺利在加载libjiagu.so时断下: 只要这个so加载完毕,那么VMP的解释器肯定都加载进内存了!

       

       由于需要查看JNI函数在so层的地址,所以找到libart中的registerNative函数下断,如下:

         

       然后继续F9运行,结果直接崩掉!IDA上只显示FFFFFF,其他都没了.......  很明显触发了反调试机制........

       事已至此,该怎么继续推进了? 这么多反调试的方法,挨个去找代码,然后挨个NOP掉?好麻烦啊,想想都头大~~~~~

            

       2、回到在IDA下断点调试那里,我们在registerNative下断点的初衷和目的是啥?不就是想看看jni函数在内存的地址么?有没有其他方式也能达到同样的效果了?这里以8.0版本的art为例,在art_method.cc中定义了RegisterNative方法(http://androidxref.com/8.0.0_r4/xref/art/runtime/art_method.cc#native_method),如下:

    379  const void* ArtMethod::RegisterNative(const void* native_method, bool is_fast) {
    380  CHECK(IsNative()) << PrettyMethod();
    381  CHECK(!IsFastNative()) << PrettyMethod();
    382  CHECK(native_method != nullptr) << PrettyMethod();
    383  if (is_fast) {
    384    AddAccessFlags(kAccFastNative);
    385  }
    386  void* new_native_method = nullptr;
    387  Runtime::Current()->GetRuntimeCallbacks()->RegisterNativeMethod(this,
    388                                                                  native_method,
    389                                                                  /*out*/&new_native_method);
    390  SetEntryPointFromJni(new_native_method);
    391  return new_native_method;
    392}

      大家有没有发现,这个方法是在ArtMethod类里面啊!ArtMethod是用来“描述”jni方法的,每个jni方法都有一个对应的ArtMethod类来管理(有点类似于元数据);这个类有个非常重要的方法PrettyMethod,代码如下:

    774 std::string ArtMethod::PrettyMethod(bool with_signature) {
    775  ArtMethod* m = this;
    776  if (!m->IsRuntimeMethod()) {
    777    m = m->GetInterfaceMethodIfProxy(Runtime::Current()->GetClassLinker()->GetImagePointerSize());
    778  }
    779  std::string result(PrettyDescriptor(m->GetDeclaringClassDescriptor()));
    780  result += '.';
    781  result += m->GetName();
    782  if (UNLIKELY(m->IsFastNative())) {
    783    result += "!";
    784  }
    785  if (with_signature) {
    786    const Signature signature = m->GetSignature();
    787    std::string sig_as_string(signature.ToString());
    788    if (signature == Signature::NoSignature()) {
    789      return result + sig_as_string;
    790    }
    791    result = PrettyReturnType(sig_as_string.c_str()) + " " + result +
    792        PrettyArguments(sig_as_string.c_str());
    793  }
    794  return result;
    795}

      这个方法可以返回对应jni函数的全称,形式为返回值 包名.类名.函数名(参数),可以说是完整的函数声明;如果能在RegiserNative函数调用PrettyMethod方法,是不是就能直接得到当前注册的jni函数名了? 这个简单,在原函数中加上部分代码即可,更改后的完整代码如下:这里用log把当前注册的完整函数名和函数地址都打印出来

    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;
          sleep(100);
      }
      
      void* new_native_method = nullptr;
      Runtime::Current()->GetRuntimeCallbacks()->RegisterNativeMethod(this,
                                                                      native_method,
                                                                      /*out*/&new_native_method);
      SetEntryPointFromJni(new_native_method);
      return new_native_method;
    }

      同理,还有另一个非常重要的函数JniMethodStart,代码如下(http://androidxref.com/8.0.0_r4/xref/art/runtime/entrypoints/quick/quick_jni_entrypoints.cc#65):

    64// Called on entry to JNI, transition out of Runnable and release share of mutator_lock_.
    65extern uint32_t JniMethodStart(Thread* self) {
    66  JNIEnvExt* env = self->GetJniEnv();
    67  DCHECK(env != nullptr);
    68  uint32_t saved_local_ref_cookie = bit_cast<uint32_t>(env->local_ref_cookie);
    69  env->local_ref_cookie = env->locals.GetSegmentState();
    70  ArtMethod* native_method = *self->GetManagedStack()->GetTopQuickFrame();
    71  if (!native_method->IsFastNative()) {
    72    // When not fast JNI we transition out of runnable.
    73    self->TransitionFromRunnableToSuspended(kNative);
    74  }
    75  return saved_local_ref_cookie;
    76}

      从代码本身的注释就能看出来:在开始执行jni函数前,这个函数就会被调用!VMP加壳的方式之一就是把java层的函数native化,放在so中执行,增加逆向难度;所以只要hook这里,是不是就能找到一些关键的函数,比如onCreate在so的地址了?参考上面的方式,继续在这个函数插桩,如下:

    // Called on entry to JNI, transition out of Runnable and release share of mutator_lock_.
    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();
      
      const char* methodname=native_method->PrettyMethod().c_str();
      if(strstr(methodname,"JniMethodStart")!=nullptr){
          sleep(100);
      }
      
      if (!native_method->IsFastNative()) {
        // When not fast JNI we transition out of runnable.
        self->TransitionFromRunnableToSuspended(kNative);
      }
      return saved_local_ref_cookie;
    }

      这两个地方都用了strstr函数插桩,这里简单说明一下原因:

    • 如果不用strstr插桩,而是直接用frida hook函数后打印参数,会导致日志过多,影响分析的效率;
    • 部分函数还是inline内联的,编译器可能会把这部分代码和其他函数合并,hook的时候不好找地方!
    • strstr是导出函数,有现成的API能找到!
    • 自己写代码得到函数名,需要“传递”到我们的程序。此处只能通过strstr的参数保存函数名,便于后续用过hook得到
    • 用strstr插桩后,直接hook;如果第二个参数是RegisterNativeflag或JniMethodStart,说明已经已经进入这两个函数开始执行,这时再打印第一个参数,就能得到正在注册的jni函数和即将执行的jni函数了;
    • 通过hook控制返回值,间接控制了是否在这两个函数sleep!如果需要用IDA调试,可以让strstr返回不为0,程序sleep 100秒;此时再用IDA附加,可绕过前面的所有反调试方法

       hook的代码如下:

    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("JniMethodStart")!=-1){
                    console.log("jnimethod:"+ this.call_name +" before");
                }
                if(this.call_name.indexOf("RegisterNativeflag")!=-1){
                    console.log("RegisterNative:"+ this.call_name +" before");
                }
            },onLeave:function(retval){
                if (this.call_name.indexOf("JniMethodStart")!=-1 //此处说明代码进入了JniMethodStart,jni函数即将执行
                    && this.method_name.indexOf("MainActivity.onCreate")!=-1){ //即将执行的是MainActivity.onCreate
                    retval.replace(0x1);//返回值不为0,让函数sleep,便于IDA附加调试
                }
            }
        })
    }
    
    function main(){
        hook_start();
    }

      打印结果如下:从日志可以看出,先是大量的jni函数通过RegisterNative注册。然后再被执行前,调用了JniMethodStart函数:

       

       onCreate函数的注册地址也打印出来了,接下来可以用IDA跳转到这里附加调试啦!

      最后说明:各种反调试的手法本质上也是一段代码,只要是代码肯定需要被执行才能达到反调试的效果!android和windows类似,代码执行的最小单位是线程!所以执行这些反调试的代码只可能在两个地方:

    • apk执行的主线程
    • 单独新生成线程

      如果是第一种情况:既然都已经执行到RegisterNative,这时壳自己的反调试代码大概率已经执行完毕(注意:本次调试崩掉是在linker加载so后运行时崩掉的,并不是断在RegisterNative时崩掉的);如果是第二种情况,那就更简单了,直接把这些反调试的线程挂起即可

      

    参考:

    1、https://bbs.pediy.com/thread-248898.htm    源码简析之ArtMethod结构与涉及技术介绍

    2、https://www.kancloud.cn/alex_wsc/androids/473623   Android运行时ART加载类和方法的过程分析

    3、https://missking.cc/2020/11/16/vmp/  vmp入门

  • 相关阅读:
    网页图表Highcharts实践教程标之添加题副标题版权信息
    S3C6410 裸机硬件JPEG解码(转)
    FPGA UART简单的串口接收模块
    unicode转GBK,GNK转unicode,解决FATFS中文码表占用ROM问题(转)
    Java 基础类型转换byte数组, byte数组转换基础类型
    JNI错误总结(转)
    Java通过JNI调用dll详细过程(转)
    UDP传输包大小(转)
    SD卡兼容性问题(转)
    汉字与区位码互转(转)
  • 原文地址:https://www.cnblogs.com/theseventhson/p/14952092.html
Copyright © 2011-2022 走看看