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