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入门

原文地址:https://www.cnblogs.com/theseventhson/p/14952092.html