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入门
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)