Java代碼引起的NATIVE野指針問題(上)
樸英敏,小米MIUI部門。從事嵌入式開發(fā)和調(diào)試工作8年多,擅長逆向分析方法,主要負(fù)責(zé)解決安卓系統(tǒng)穩(wěn)定性問題。
上周音樂組同事反饋了一個(gè)必現(xiàn)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指向了一個(gè)沒有可執(zhí)行權(quán)限的內(nèi)存地址上。
初步分析:
對應(yīng)的代碼如下:
- status_t OpenGLRenderer::callDrawGLFunction(Functor* functor, Rect& dirty) {
- if (mSnapshot->isIgnored()) return DrawGlInfo::kStatusDone;
- detachFunctor(functor);
- ...
- interrupt();
- => status_t result = (*functor)(DrawGlInfo::kModeDraw, &info);
其中,F(xiàn)unctor類重載了()操作符:
- class Functor {
- public:
- Functor() {}
- virtual ~Functor() {}
- => virtual status_t operator ()(int /*what*/, void* /*data*/) { return NO_ERROR; }
- };
因此,()操作其實(shí)就是調(diào)用了Functor類的一個(gè)虛函數(shù),它的具體實(shí)現(xiàn)目前還不清楚。
對應(yīng)的匯編代碼如下:
- 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()
崩潰時(shí)的寄存器值如下:
- 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這一行匯編代碼中。
而查看寄存器對應(yīng)的內(nèi)存值,發(fā)現(xiàn)有點(diǎn)問題:
- 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,這個(gè)和r3值(400fc1b8)不一樣,
同樣
[r3+8] = [400fc1b8 + 8] = 7c04acb8,這個(gè)值也和r5值(79801f28)不一樣。
這在平時(shí)的tombstone里是非常少見的!
乍一看非常不可思議,但仔細(xì)想想tombstone的生成過程,就能發(fā)現(xiàn)其中的問題。
原來寄存器信息是錯(cuò)位崩潰時(shí)的cpu context,保存在崩潰時(shí)的線程私有的信號棧和內(nèi)核棧中,直到debuggerd去獲取這個(gè)值,它是不會被修改的。
而內(nèi)存是進(jìn)程中的各個(gè)線程共享的,所以在發(fā)生異常到debuggerd打印內(nèi)存信息這段過程中(其實(shí)是相對很長的一個(gè)過程),別的線程是有可能修改內(nèi)存值的。
為了證明別的線程在改這個(gè)內(nèi)存值,在callDrawGLFunction()函數(shù)中的若干處打印了Functor和它的vtbl(虛函數(shù)表地址)值:
- 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
可以確定確實(shí)有別的線程在修改這個(gè)值。
這里就存在兩個(gè)可能性了:
1、別的線程也持有functor指針,并修改內(nèi)容
2、functor是野指針,對應(yīng)的內(nèi)存已經(jīng)還回系統(tǒng),其他模塊可任意使用。
而對象的vtbl一般是不會修改的,所以2的可能性更大一些。
為了查明是哪個(gè)線程在改,對functor指向的內(nèi)存做了寫保護(hù)操作:
- static int** s_saved_vtbl = NULL;
- static void* s_saved_functor = NULL;
- static void mprotect_local(int** p) {
- // 一旦發(fā)現(xiàn)vtbl有變化就將對應(yīng)內(nèi)存設(shè)置為只讀
- 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到手機(jī)中復(fù)現(xiàn)問題,很容易抓到訪問權(quán)限引起的crash。
而每次的crash的線程和位置都不一樣,也就是不同的線程在不同的函數(shù)中讀寫這個(gè)地址。
這樣基本上就確定是野指針問題,進(jìn)入下一階段的分析。
關(guān)于野指針:
所謂野指針就是一個(gè)對象被釋放后又被使用,可能是釋放的問題,也可能是使用的問題。
我們已經(jīng)知道使用的位置,接下來要找出是從哪釋放的。
找到釋放對象的最笨的方法,是在free()函數(shù)里打印調(diào)用棧。
但這么做有兩個(gè)問題:
1、log太量多,一秒內(nèi)可能會有成千上萬的malloc/free函數(shù)被調(diào)用。
2、打印調(diào)用棧的函數(shù)本身會調(diào)用free函數(shù),這樣會陷入死循環(huán)。
為了解決上面兩個(gè)問題,需要用到hook技術(shù)。
關(guān)于hook技術(shù):
要了解hook技術(shù),得先了解外部函數(shù)的調(diào)用過程。
所謂外部函數(shù)就是外部模塊中定義的函數(shù)。比如,libhwui.so中的某個(gè)源文件中調(diào)用了malloc函數(shù),而這個(gè)malloc函數(shù)是libc.so中定義的。
當(dāng)編譯libhwui.so的這個(gè)源文件時(shí),對應(yīng)調(diào)用malloc的地方會生成如下的匯編代碼:
- blx addr
這里blx是arm的跳轉(zhuǎn)指令,addr是目標(biāo)地址,也就是malloc函數(shù)的地址,那這個(gè)malloc函數(shù)的地址如何確定?
這個(gè)編譯的階段是無法確定的,只有當(dāng)運(yùn)行時(shí)進(jìn)程加載完libc.so以后,malloc函數(shù)的地址才能被確定。
所以編譯器在編譯的時(shí)候會在libbinder.so中留出一部分空間作為地址表,專門用于存放外部函數(shù)的地址,這個(gè)區(qū)域叫g(shù)ot表。
每一個(gè)本模塊調(diào)用到的外部函數(shù)都對應(yīng)got表中的一項(xiàng)。
當(dāng)然got表里面的內(nèi)容是在進(jìn)程啟動階段,加載動態(tài)庫時(shí)被連接器linker填充的。
而編譯階段我們只需要將代碼寫成:
1、從got表對應(yīng)位置獲取外部函數(shù)地址
2、跳轉(zhuǎn)到這個(gè)外部函數(shù)的地址
這個(gè)動作需要由若干的指令來完成,所以跳轉(zhuǎn)指令blx addr中的addr其實(shí)指向本模塊的一組指令:
- blx cb74 <malloc@plt>
這組指令所在的區(qū)域就是elf文件結(jié)構(gòu)里的plt表,plt表中每一個(gè)外部函數(shù)都對應(yīng)一個(gè)表項(xiàng),如:
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]! ;
每一個(gè)plt表項(xiàng)都是做相同操作:
1、先獲取got表中外目標(biāo)函數(shù)對應(yīng)的地址(前兩行);
2、從got表中獲取地址目標(biāo)函數(shù)的地址,并賦給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技術(shù)就是通過修改so的got表來截獲so中的某些外部函數(shù)調(diào)用。
so的代碼段是多個(gè)進(jìn)程共享的,但它的數(shù)據(jù)段私有的,而got表就是數(shù)據(jù)段。
所以我們只修改music應(yīng)用進(jìn)程的libhwui.so的got表中free函數(shù)對應(yīng)的項(xiàng),影響范圍將大大減少。
那改成什么值呢?一般是我們自己定義的函數(shù),比如:
- void inject_free(void *ptr) {
- ALOGI("free ptr=%p",ptr);
- dumpNativeStack();
- dumpJavaStack();
- free(ptr);
- }
為了不影響原來的邏輯,打印完debug信息,還是要調(diào)用原來被hook的函數(shù)。
有了hook技術(shù)后能完美的解決野指針中的兩個(gè)問題,下面繼續(xù)分析問題。
【本文是51CTO專欄“小米開放平臺”的原創(chuàng)文章,“小米開放平臺”微信公眾號:xiaomideveloper】