当前位置: 首页 > 科技观察

Java代码引起的NATIVE野指针问题(上)

时间:2023-03-18 19:15:11 科技观察

小米MIUI部朴英敏。从事嵌入式开发调试8年多,擅长逆向分析方法,主要负责解决Android系统稳定性问题。上周音乐组的同事反馈了一个必出现的NativeCrash问题。Thetombstoneisasfollows:pid:5028,tid:5028,name:com.miui.player>>>com.miui.player<<#01pc0002302f/system/lib/libhwui.so(android::uirenderer::OpenGLRenderer::callDrawGLFunction(android::Functor*,android::uirenderer::Rect&)+322)#02pc00015d91/system/lib/libhwui.so(android::uirenderer::DrawFunctorOp::applyDraw(android::uirenderer::OpenGLRenderer&,android::uirenderer::Rect&)+28)#03pc00014527/system/lib/libhwui.so(android::uirenderer::DrawBatch::replay(android::uirenderer::OpenGLRenderer&,android::uirenderer::Rect&,int)+74)#04pc00014413/system/lib/libhwui.so(android::uirenderer::DeferredDisplayList::flush(android::uirenderer::OpenGL渲染器&,android::uirenderer::Rect&)+218)#05pc0001d1cf/system/lib/libhwui.so(_ZN7android10uirenderer14OpenGLRenderer15drawDisplayListEPNS0_11DisplayListERNS0_4RectEi.part.47+230)#06pc0006820d/system/lib/libandso指出了崩溃的初步原因_没有可执行权限的内存地址:对应代码如下:status_tOpenGLRenderer::callDrawGLFunction(Functor*functor,Rect&dirty){if(mSnapshot->isIgnored())returnDrawGlInfo::kStatusDone;detachFunctor(functor);...interrupt();=>status_tresult=(*functor)(DrawGlInfo::kModeDraw,&info);其中,Functor类重载了()运算符:classFunctor{public:Functor(){}virtual~Functor(){}=>virtualstatus_toperator()(int/*what*/,void*/*data*/){返回NO_ERROR;}};因此,()操作实际上是在调用Functor类的一个虚函数,其具体实现目前还不清楚。Thecorrespondingassemblycodeisasfollows:23028:aa0baddr2,sp,#442302a:6803ldrr3,[r0,#0];r0isfunctor,r3=[r0]=functor.vtlb2302c:689dldrr5,[r3,#8];r5=[r3+8]=[functor.vtlb+8]=Functor.operator()2302e:47a8blxr5;callFunctor.operator()崩溃时的寄存器值如下:r07ac59c98r100000000r2bea7b174r3400fc1b8r4774c4c88r579801f28r6bea7b478r740c12bb8r87c1b68e8r9778781e8slbea7b478fpbea7b414ip00000001spbea7b148lr40c07031pc79801f28cpsr600f0010可以看到,r5和pc值是相等的,可以知道,Itisdeterminedthatthecrashisinthelineofassemblycode2302e.而查看寄存器对应的内存值,发现有点问题:memorynearr0:7ac59c78000000180000001b735a9b3823831ef07ac59c8823831ef0735a9b5000000018000000117ac59c98798223287776869800000010000000227ac59ca800000000000000000000000000000003memorynearr3:400fc1987c74c00000200000000000770d44acd8400fc1a80000000000000000400fc1a8400fc1a8400fc1b8400fc1b0400fc1b07c04acb87c78f008400fc1c87c021d987c78ffc07983bbf07c04bfa8[r0]=[7ac59c98]=798223298,这个和r3值(400fc1b8)不一样,同样[r3+8]=[400fc1b8+8]=7c04acb8,thisvalueisalsodifferentfromther5value(79801f28).Thisisveryrareinnormaltombstones!Atfirstglance,itisincredible,butifyouthinkaboutthetombstonegenerationprocesscarefully,youcanfindtheproblem.原来寄存器信息是错位崩溃时的cpu上下文,保存在崩溃时线程的私有信号栈和内核栈中。在debuggerd获得这个值之前,它不会被修改。内存是进程中各个线程共享的,所以在从异常到debuggerd打印内存信息的过程中(其实是一个比较长的过程),其他线程可能会修改内存值。Functor及其vtbl(虚函数表地址)值打印在callDrawGLFunction()函数的几个地方:status_tOpenGLRenderer::callDrawGLFunction(Functor*functor,Rect&dirty){AOGI).("functor=%p,vtbl=%p");sleep(1);if(mSnapshot->isIgnored())返回DrawGlInfo::kStatusDone;AOGI("functor=%p,vtbl=%p");1);detachFunctor(functor);...AOGI("functor=%p,vtbl=%p");sleep(1);interrupt();AOGI("functor=%p,vtbl=%p");sleep(1);status_tresult=(*functor)(DrawGlInfo::kDrawMode,&信息);Thecapturedlogisasfollows:10-2721:19:45.79480278027IOpenGLRenderer:functor=0x7a7b8530,vtbl=0x73648de010-2721:19:47.80127020x7a7b8530,vtbl=0x73648de010-2721:19:48.80180278027IOpenGLRenderer:functor=0x7a7b8530,vtbl=0x73648de010-2721:19:49.80180278027IOpenGLRenderer:functor=0x7a7b8530,vtbl=0x73648de010-2721:19:50.80480278027IOpenGLRenderer:functor=0x7a7b8530,vtbl=0x73648de010-2721:19:51.80480278027IOpenGLRenderer:functor=0x7a7b8530,vtbl=0x400fc1b8Itcanbedeterminedthatthere确实是其他线程修改这个这里有两种可能:1、其他线程也持有仿函数指针,修改内容。2、仿函数是一个野指针,对应的内存已经归还给系统,其他模块可以任意使用。对象的vtbl一般不会修改,所以2的可能性比较大。为了找出是哪个线程发生变化,对functor指向的内存进行写保护:staticint**s_saved_vtbl=NULL;staticvoid*s_saved_functor=NULL;staticvoidmprotect_local(int**p){//一旦发现vtbl发生变化,它会将相应的内存设置为只读if(p!=s_saved_vtbl){mprotect((void*)((unsignedint)s_saved_functor&0xfffff000),4096,PROT_READ);}sleep(1);}status_tOpenGLRenderer::callDrawGLFunction(Functor*functor,Rect&dirty){int*ptr=(int*)functor;s_saved_functor=(void*)ptr;s_saved_vtbl=(int**)*ptr;if(mSnapshot->isIgnored())returnDrawGlInfo::kStatusDone;mprotect_local((int**)*ptr);detachFunctor(仿函数);mprotect_local((int**)*ptr);...mprotect_local((int**)*ptr);中断();status_tresult=(*仿函数)(DrawGlInfo::kModeDraw,&info);push到手机上重现问题,很容易抓到访问权限导致的crash。每次crash的线程和位置都不一样,就是不同的线程在不同的函数中读写这个地址。这样就基本确定是野指针问题了,进入下一阶段的分析。关于野指针:所谓野指针,就是先释放再使用的对象。可能是发布问题,也可能是使用问题。现在我们知道它在哪里使用了,我们需要找出它是从哪里释放的。找到释放对象的最愚蠢的方法是在free()函数中打印调用堆栈。但是这样做有两个问题:1.日志太多,一秒可能会调用上千个malloc/free函数。2.打印调用栈的函数本身会调用free函数,会陷入死循环。为了解决以上两个问题,需要使用钩子技术。关于钩子技术:要了解钩子技术,首先要了解外部函数的调用过程。所谓外部函数就是在外部模块中定义的函数。比如在libhwui.so中的一个源文件中调用了一个malloc函数,而这个malloc函数是在libc.so中定义的。在编译libhwui.so的源文件时,对应调用malloc的地方会生成如下汇编代码:blxaddr其中blx是arm的跳转指令,addr是目标地址,也就是malloc函数的地址,那么malloc函数是如何确定地址的呢?这个编译阶段无法确定。malloc函数的地址只有在runtime进程加载libc.so后才能确定。所以编译器在编译时会在libbinder.so中预留一部分空间作为地址表,专门用来存放外部函数的地址。这个区域称为got表。该模块调用的每个外部函数都对应于got表中的一个项目。当然,got表中的内容是在进程启动阶段加载动态库时链接器填充的。在编译阶段,我们只需要将代码写成:1.从got表的对应位置获取外部函数的地址2.跳转到这个外部函数的地址。这个动作需要几条指令完成,所以跳转指令中的addrblxaddr其实就是指向这个模块的一组指令:blxcb74这组指令所在的区域就是plt表在elf文件结构中,plt表中每个外部函数对应一个entry,如:0000cb74:cb74:e28fc600addip,pc,#0,12cb78:e28cca29addip,ip,#167936;cb7c:e5bcf1e8ldrpc,[ip,#488]!;0000c8bc:c8bc:e28fc600添加ip,pc,#0,12c8c0:e28cca29添加ip,ip,#167936;c8c4:e5bcf3b8ldrpc,[ip,#952]!;对每个plt表项做同样的操作:1.先获取got表中的foreign和domesticobject函数对应的地址(前两行);2、从got表中获取地址目标函数的地址,赋值给pc寄存器(第三行)。下面给出got表和plt表在so文件中的位置:readelf-Slibhwui.so[Nr]NameTypeAddrOffSizeESFlgLkInfAl[0]NULL0000000000000000000000000[1].interpProgbits0000013400013400001300A001[2].dynysymdynsym0000014888800242010A314[3].dynstrstrtab0000256800A001[4].hahHash007c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c07c004[5].rel.dynREL00008d40008d40002bc808A204[6].rel.pltREL0000b90800b908000a7808A274=>[7].pltPROGBITS0000c380000c380[008].textProgbits0000d34800d34801EF3000AX08[9].Arm.exidxarm_exidx0002C2780278001fb808Al804[10].An.extabproters002e2300093000A04PROGBITS0002eb6002eb600036a400A004[12].fini_arrayFINI_ARRAY0003401003301000000400WA004[13].data.rel.roPROGBITS0003401803301800191000WA008[14].init_arrayINIT_ARRAY0003592803492800000c00WA004[15].动态动态0003593403493400014008WA304=>[16].gotPROGBITS00035a74034a7400058c00WA004[17].dataPROGBITS0000003500000025c00WA004[18].bssNOBITS0003625c04[019].commentPROGBITS0000000003525c00001001MS001[20].note.gnu.gold-veNOTE0000000003526c00001c0004[21].ARM.attributesARM_ATTRIBUTES0000000800200200352e0352.gnu_debuglinkPROGBITS000000000352c600001000001[23].shstrtabSTRTAB000000000352d60000dc00001我们的hook技术是通过修改so的got表来拦截so中的一些外部函数调用段被多个进程共享,但是它的数据段是私有的,得到的表就是数据段。所以我们在音乐应用过程中只修改libhwui.so的got表中free函数对应的项,影响范围会大大减小。应该改成什么值?一般是我们自己定义的函数,比如:voidinject_free(void*ptr){ALOGI("freeptr=%p",ptr);dumpNativeStack();dumpJavaStack();free(ptr);}为了不影响了原来的逻辑,打印出debug信息后,还是要调用原来hook的函数。使用钩子技术,可以完美解决字段指针中的两个问题。下面继续分析问题。【本文为“小米开放平台”专栏原创文章,“小米开放平台”微信公众号:xiaomideveloper]