乾貨 | 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

想了解更多?

那就關注咱們吧!

小米開放平臺公衆號二維碼

相關文章
相關標籤/搜索