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類調用關系


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM