干货 | JAVA代码引发的NATIVE野指针问题(上)

干货 | JAVA代码引发的NATIVE野指针问题(上)

做者介绍:html

简介:朴英敏,小米MIUI部门。从事嵌入式开发和调试工做8年多,擅长逆向分析方法,主要负责解决安卓系统稳定性问题。android

 

上周音乐组同事反馈了一个必现Native Crash问题,tombstone以下:app

崩溃的缘由是pc指向了一个没有可执行权限的内存地址上。函数

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


初步分析:
对应的代码以下:ui

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类重载了()操做符:spa

class Functor {

public:

    Functor() {}

    virtual ~Functor() {}

=>  virtual status_t operator ()(int /*what*/, void* /*data*/) { return NO_ERROR; }

};

所以,()操做其实就是调用了Functor类的一个虚函数,它的具体实现目前还不清楚。
对应的汇编代码以下:  .net

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这一行汇编代码中。
而查看寄存器对应的内存值,发现有点问题:debug

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

能够肯定确实有别的线程在修改这个值。
这里就存在两个可能性了:
一、别的线程也持有functor指针,并修改内容
二、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()函数里打印调用栈。
 

但这么作有两个问题:
一、log太量多,一秒内可能会有成千上万的malloc/free函数被调用。
二、打印调用栈的函数自己会调用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填充的。 
而编译阶段咱们只须要将代码写成:
一、从got表对应位置获取外部函数地址
二、跳转到这个外部函数的地址

这个动做须要由若干的指令来完成,因此跳转指令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表项都是作相同操做:
一、先获取got表中外目标函数对应的地址(前两行);
二、从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技术后能完美的解决野指针中的两个问题,下面继续分析问题。

<见下篇>

小米开放平台重磅推出小米账号接入有礼活动自今日起至2016年12月31日前成功接入小米账号便可得到小米开放平台免费提供的平台资源(小米应用商店、小米卡包、小米推送vip、小米账号联盟等资源),机会不容错过,咱们期待您的加入!
活动报名地址:http://dev.xiaomi.com/console/hd/account.html?hmsr=%E5%BC%80%E6%BA%90%E4%B8%AD%E5%9B%BD%E5%8D%9A%E5%AE%A2%E6%B8%A0%E9%81%93&hmpl=&hmcu=&hmkw=&hmci=
官方QQ交流群:398616987

想了解更多?

那就关注咱们吧!

小米开放平台公众号二维码

相关文章
相关标签/搜索