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类调用关系