Java代码引起的NATIVE野指针问题(上)
朴英敏,小米MIUI部门。从事嵌入式开发和调试工作8年多,擅长逆向分析方法,主要负责解决安卓系统稳定性问题。
上周音乐组同事反馈了一个必现Native Crash问题,tombstone如下:
- pid: 5028, tid: 5028, name: com.miui.player >>> com.miui.player <<<
- signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 79801f28
- r0 7ac59c98 r1 00000000 r2 bea7b174 r3 400fc1b8
- r4 774c4c88 r5 79801f28 r6 bea7b478 r7 40c12bb8
- r8 7c1b68e8 r9 778781e8 sl bea7b478 fp bea7b414
- ip 00000001 sp bea7b148 lr 40c07031 pc 79801f28 cpsr 600f0010
- backtrace:
- #00 pc 0000bf28 <unknown>
- #01 pc 0002302f /system/lib/libhwui.so (android::uirenderer::OpenGLRenderer::callDrawGLFunction(android::Functor*, android::uirenderer::Rect&)+322)
- #02 pc 00015d91 /system/lib/libhwui.so (android::uirenderer::DrawFunctorOp::applyDraw(android::uirenderer::OpenGLRenderer&, android::uirenderer::Rect&)+28)
- #03 pc 00014527 /system/lib/libhwui.so (android::uirenderer::DrawBatch::replay(android::uirenderer::OpenGLRenderer&, android::uirenderer::Rect&, int)+74)
- #04 pc 00014413 /system/lib/libhwui.so (android::uirenderer::DeferredDisplayList::flush(android::uirenderer::OpenGLRenderer&, android::uirenderer::Rect&)+218)
- #05 pc 0001d1cf /system/lib/libhwui.so (_ZN7android10uirenderer14OpenGLRenderer15drawDisplayListEPNS0_11DisplayListERNS0_4RectEi.part.47+230)
- #06 pc 0006820d /system/lib/libandroid_runtime.so
崩溃的原因是pc指向了一个没有可执行权限的内存地址上。
初步分析:
对应的代码如下:
- status_t OpenGLRenderer::callDrawGLFunction(Functor* functor, Rect& dirty) {
- if (mSnapshot->isIgnored()) return DrawGlInfo::kStatusDone;
- detachFunctor(functor);
- ...
- interrupt();
- => status_t result = (*functor)(DrawGlInfo::kModeDraw, &info);
其中,Functor类重载了()操作符:
- class Functor {
- public:
- Functor() {}
- virtual ~Functor() {}
- => virtual status_t operator ()(int /*what*/, void* /*data*/) { return NO_ERROR; }
- };
因此,()操作其实就是调用了Functor类的一个虚函数,它的具体实现目前还不清楚。
对应的汇编代码如下:
- 23028: aa0b add r2, sp, #44
- 2302a: 6803 ldr r3, [r0, #0] ; r0是functor,r3 = [r0] = functor.vtlb
- 2302c: 689d ldr r5, [r3, #8] ; r5 = [r3 + 8] = [functor.vtlb + 8] = Functor.operator()
- 2302e: 47a8 blx r5 ; call Functor.operator()
崩溃时的寄存器值如下:
- r0 7ac59c98 r1 00000000 r2 bea7b174 r3 400fc1b8
- r4 774c4c88 r5 79801f28 r6 bea7b478 r7 40c12bb8
- r8 7c1b68e8 r9 778781e8 sl bea7b478 fp bea7b414
- ip 00000001 sp bea7b148 lr 40c07031 pc 79801f28 cpsr 600f0010
可以看到,r5和pc值是相等的,可以知道,确定是崩溃在2302e这一行汇编代码中。
而查看寄存器对应的内存值,发现有点问题:
- memory near r0:
- 7ac59c78 00000018 0000001b 735a9b38 23831ef0
- 7ac59c88 23831ef0 735a9b50 00000018 00000011
- 7ac59c98 79822328 77768698 00000010 00000022
- 7ac59ca8 00000000 00000000 00000000 00000003
- memory near r3:
- 400fc198 7c74c000 00200000 00000077 0d44acd8
- 400fc1a8 00000000 00000000 400fc1a8 400fc1a8
- 400fc1b8 400fc1b0 400fc1b0 7c04acb8 7c78f008
- 400fc1c8 7c021d98 7c78ffc0 7983bbf0 7c04bfa8
[r0] = [7ac59c98] = 798223298,这个和r3值(400fc1b8)不一样,
同样
[r3+8] = [400fc1b8 + 8] = 7c04acb8,这个值也和r5值(79801f28)不一样。
这在平时的tombstone里是非常少见的!
乍一看非常不可思议,但仔细想想tombstone的生成过程,就能发现其中的问题。
原来寄存器信息是错位崩溃时的cpu context,保存在崩溃时的线程私有的信号栈和内核栈中,直到debuggerd去获取这个值,它是不会被修改的。
而内存是进程中的各个线程共享的,所以在发生异常到debuggerd打印内存信息这段过程中(其实是相对很长的一个过程),别的线程是有可能修改内存值的。
为了证明别的线程在改这个内存值,在callDrawGLFunction()函数中的若干处打印了Functor和它的vtbl(虚函数表地址)值:
- status_t OpenGLRenderer::callDrawGLFunction(Functor* functor, Rect& dirty) {
- AOGI("functor=%p,vtbl=%p");
- sleep(1);
- if (mSnapshot->isIgnored()) return DrawGlInfo::kStatusDone;
- AOGI("functor=%p,vtbl=%p");
- sleep(1);
- detachFunctor(functor);
- ...
- AOGI("functor=%p,vtbl=%p");
- sleep(1);
- interrupt();
- AOGI("functor=%p,vtbl=%p");
- sleep(1);
- status_t result = (*functor)(DrawGlInfo::kModeDraw, &info);
抓到的log如下:
- 10-27 21:19:45.794 8027 8027 I OpenGLRenderer: functor=0x7a7b8530,vtbl=0x73648de0
- 10-27 21:19:47.801 8027 8027 I OpenGLRenderer: functor=0x7a7b8530,vtbl=0x73648de0
- 10-27 21:19:48.801 8027 8027 I OpenGLRenderer: functor=0x7a7b8530,vtbl=0x73648de0
- 10-27 21:19:49.801 8027 8027 I OpenGLRenderer: functor=0x7a7b8530,vtbl=0x73648de0
- 10-27 21:19:50.804 8027 8027 I OpenGLRenderer: functor=0x7a7b8530,vtbl=0x73648de0
- 10-27 21:19:51.804 8027 8027 I OpenGLRenderer: functor=0x7a7b8530,vtbl=0x400fc1b8
可以确定确实有别的线程在修改这个值。
这里就存在两个可能性了:
1、别的线程也持有functor指针,并修改内容
2、functor是野指针,对应的内存已经还回系统,其他模块可任意使用。
而对象的vtbl一般是不会修改的,所以2的可能性更大一些。
为了查明是哪个线程在改,对functor指向的内存做了写保护操作:
- static int** s_saved_vtbl = NULL;
- static void* s_saved_functor = NULL;
- static void mprotect_local(int** p) {
- // 一旦发现vtbl有变化就将对应内存设置为只读
- if(p != s_saved_vtbl) {
- mprotect((void*)((unsigned int)s_saved_functor&0xfffff000), 4096, PROT_READ);
- }
- sleep(1);
- }
- status_t OpenGLRenderer::callDrawGLFunction(Functor* functor, Rect& dirty) {
- int* ptr = (int*)functor;
- s_saved_functor = (void*)ptr;
- s_saved_vtbl = (int**)*ptr;
- if (mSnapshot->isIgnored()) return DrawGlInfo::kStatusDone;
- mprotect_local((int**)*ptr);
- detachFunctor(functor);
- mprotect_local((int**)*ptr);
- ...
- mprotect_local((int**)*ptr);
- interrupt();
- status_t result = (*functor)(DrawGlInfo::kModeDraw, &info);
push到手机中复现问题,很容易抓到访问权限引起的crash。
而每次的crash的线程和位置都不一样,也就是不同的线程在不同的函数中读写这个地址。
这样基本上就确定是野指针问题,进入下一阶段的分析。
关于野指针:
所谓野指针就是一个对象被释放后又被使用,可能是释放的问题,也可能是使用的问题。
我们已经知道使用的位置,接下来要找出是从哪释放的。
找到释放对象的最笨的方法,是在free()函数里打印调用栈。
但这么做有两个问题:
1、log太量多,一秒内可能会有成千上万的malloc/free函数被调用。
2、打印调用栈的函数本身会调用free函数,这样会陷入死循环。
为了解决上面两个问题,需要用到hook技术。
关于hook技术:
要了解hook技术,得先了解外部函数的调用过程。
所谓外部函数就是外部模块中定义的函数。比如,libhwui.so中的某个源文件中调用了malloc函数,而这个malloc函数是libc.so中定义的。
当编译libhwui.so的这个源文件时,对应调用malloc的地方会生成如下的汇编代码:
- blx addr
这里blx是arm的跳转指令,addr是目标地址,也就是malloc函数的地址,那这个malloc函数的地址如何确定?
这个编译的阶段是无法确定的,只有当运行时进程加载完libc.so以后,malloc函数的地址才能被确定。
所以编译器在编译的时候会在libbinder.so中留出一部分空间作为地址表,专门用于存放外部函数的地址,这个区域叫got表。
每一个本模块调用到的外部函数都对应got表中的一项。
当然got表里面的内容是在进程启动阶段,加载动态库时被连接器linker填充的。
而编译阶段我们只需要将代码写成:
1、从got表对应位置获取外部函数地址
2、跳转到这个外部函数的地址
这个动作需要由若干的指令来完成,所以跳转指令blx addr中的addr其实指向本模块的一组指令:
- blx cb74 <malloc@plt>
这组指令所在的区域就是elf文件结构里的plt表,plt表中每一个外部函数都对应一个表项,如:
0000cb74 <malloc@plt>:
cb74: e28fc600 add ip, pc, #0, 12
cb78: e28cca29 add ip, ip, #167936 ;
cb7c: e5bcf1e8 ldr pc, [ip, #488]! ;
0000c8bc <free@plt>:
c8bc: e28fc600 add ip, pc, #0, 12
c8c0: e28cca29 add ip, ip, #167936 ;
c8c4: e5bcf3b8 ldr pc, [ip, #952]! ;
每一个plt表项都是做相同操作:
1、先获取got表中外目标函数对应的地址(前两行);
2、从got表中获取地址目标函数的地址,并赋给pc寄存器(第三行)。
下面给出got表和plt表在so文件中的位置:
readelf -S libhwui.so
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 00000134 000134 000013 00 A 0 0 1
[ 2] .dynsym DYNSYM 00000148 000148 002420 10 A 3 1 4
[ 3] .dynstr STRTAB 00002568 002568 0056a4 00 A 0 0 1
[ 4] .hash HASH 00007c0c 007c0c 001134 04 A 2 0 4
[ 5] .rel.dyn REL 00008d40 008d40 002bc8 08 A 2 0 4
[ 6] .rel.plt REL 0000b908 00b908 000a78 08 A 2 7 4
=>[ 7] .plt PROGBITS 0000c380 00c380 000fc8 00 AX 0 0 4
[ 8] .text PROGBITS 0000d348 00d348 01ef30 00 AX 0 0 8
[ 9] .ARM.exidx ARM_EXIDX 0002c278 02c278 001fb8 08 AL 8 0 4
[10] .ARM.extab PROGBITS 0002e230 02e230 000930 00 A 0 0 4
[11] .rodata PROGBITS 0002eb60 02eb60 0036a4 00 A 0 0 4
[12] .fini_array FINI_ARRAY 00034010 033010 000004 00 WA 0 0 4
[13] .data.rel.ro PROGBITS 00034018 033018 001910 00 WA 0 0 8
[14] .init_array INIT_ARRAY 00035928 034928 00000c 00 WA 0 0 4
[15] .dynamic DYNAMIC 00035934 034934 000140 08 WA 3 0 4
=>[16] .got PROGBITS 00035a74 034a74 00058c 00 WA 0 0 4
[17] .data PROGBITS 00036000 035000 00025c 00 WA 0 0 4
[18] .bss NOBITS 0003625c 03525c 000068 00 WA 0 0 4
[19] .comment PROGBITS 00000000 03525c 000010 01 MS 0 0 1
[20] .note.gnu.gold-ve NOTE 00000000 03526c 00001c 00 0 0 4
[21] .ARM.attributes ARM_ATTRIBUTES 00000000 035288 00003e 00 0 0 1
[22] .gnu_debuglink PROGBITS 00000000 0352c6 000010 00 0 0 1
[23] .shstrtab STRTAB 00000000 0352d6 0000dc 00 0 0 1
我们的hook技术就是通过修改so的got表来截获so中的某些外部函数调用。
so的代码段是多个进程共享的,但它的数据段私有的,而got表就是数据段。
所以我们只修改music应用进程的libhwui.so的got表中free函数对应的项,影响范围将大大减少。
那改成什么值呢?一般是我们自己定义的函数,比如:
- void inject_free(void *ptr) {
- ALOGI("free ptr=%p",ptr);
- dumpNativeStack();
- dumpJavaStack();
- free(ptr);
- }
为了不影响原来的逻辑,打印完debug信息,还是要调用原来被hook的函数。
有了hook技术后能完美的解决野指针中的两个问题,下面继续分析问题。
作者:朴英敏
来源:51CTO

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Java代码引起的NATIVE野指针问题(下)
朴英敏,小米MIUI部门。从事嵌入式开发和调试工作8年多,擅长逆向分析方法,主要负责解决安卓系统稳定性问题。 实施hook: 我们有了hook,但目前还不知道是哪个so中释放了functor。 如果无法确定是哪个so,可以多hook几个so就行了。 当然对于特定的例子,也有技巧来确定so,比如我们这个例子: 被析构的对象是Functor类的对象,由于它的vtbl地址我们能够从log中获取到, 而vtbl一般指向定义了该类的so中,所以用vtbl值(0×73648de0)去map表中找,就能确定是哪个so了。 ... 73635000-73646000rw-p0000000000:000 73646000-73648000r-xp00000000b3:181287/system/lib/libwebviewchromium_plat_support.so =>73648000-73649000r--p00001000b3:181287/system/lib/libwebviewchromium_plat_support.so 73649000-7364a000rw-p0000200...
- 下一篇
大量APP使用超声波追踪技术获取用户信息,隐私安全或将难以保障
研究人员在上周的IEEE欧洲议会上表示,他们在近期的一项研究中发现了234种安卓应用会向用户发出“允许使用麦克风”的请求,以此通过超声波信号追踪用户信息。基于超声波跨设备追踪技术(Ultrasonic Cross-Device Tracking,uXDT)是许多市场和广告公司的“宠儿”。 超声波音频信标可以植入电视广告或网页广告,而装有接收器的移动APP则可以收集这些信标。由此,广告商可以通过此项技术跨设备追踪用户信息,创建用户的个性化档案,通过分析设备收集的数据了解用户的兴趣所在,从而为每位用户推荐他们感兴趣的广告。 越来越多的APP开始使用uXDT技术 在这项研究中,研究人员针对VirusTotal服务的数百万Android应用进行了分析,他们发现一小部分应用采用名为Shopkick和Lisnr的超声波音频技术。还有不少应用则采用了SilverPush SDK,这是个可让开发者对用户进行跨设备追踪的SDK。SilverPush、Lisnr和Shopkick都是为开发者准备的SDK,这三款SDK都采用超声波信标给移动设备发送信息。 开发者可以通过SilverPush跨多个设备追踪用户...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8编译安装MySQL8.0.19
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16