java导致native非法指令,Java代码引起的NATIVE野指针问题(上)
樸英敏,小米MIUI部門。從事嵌入式開發(fā)和調(diào)試工作8年多,擅長逆向分析方法,主要負責解決安卓系統(tǒng)穩(wěn)定性問題。
上周音樂組同事反饋了一個必現(xiàn)Native Crash問題,tombstone如下: pid:?5028,?tid:?5028,?name:?com.miui.player??>>>?com.miui.player?<<?????#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指向了一個沒有可執(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);
其中,Functor類重載了()操作符: class?Functor?{??public:??Functor()?{}??virtual?~Functor()?{}??=>?virtual?status_t?operator?()(int?/*what*/,?void*?/*data*/)?{?return?NO_ERROR;?}??};
因此,()操作其實就是調(diào)用了Functor類的一個虛函數(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()
崩潰時的寄存器值如下: 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)有點問題: 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的生成過程,就能發(fā)現(xiàn)其中的問題。
原來寄存器信息是錯位崩潰時的cpu context,保存在崩潰時的線程私有的信號棧和內(nèi)核棧中,直到debuggerd去獲取這個值,它是不會被修改的。
而內(nèi)存是進程中的各個線程共享的,所以在發(fā)生異常到debuggerd打印內(nèi)存信息這段過程中(其實是相對很長的一個過程),別的線程是有可能修改內(nèi)存值的。
為了證明別的線程在改這個內(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
可以確定確實有別的線程在修改這個值。
這里就存在兩個可能性了:
1、別的線程也持有functor指針,并修改內(nèi)容
2、functor是野指針,對應(yīng)的內(nèi)存已經(jīng)還回系統(tǒng),其他模塊可任意使用。
而對象的vtbl一般是不會修改的,所以2的可能性更大一些。
為了查明是哪個線程在改,對functor指向的內(nèi)存做了寫保護操作: 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到手機中復現(xiàn)問題,很容易抓到訪問權(quán)限引起的crash。
而每次的crash的線程和位置都不一樣,也就是不同的線程在不同的函數(shù)中讀寫這個地址。
這樣基本上就確定是野指針問題,進入下一階段的分析。
關(guān)于野指針:
所謂野指針就是一個對象被釋放后又被使用,可能是釋放的問題,也可能是使用的問題。
我們已經(jīng)知道使用的位置,接下來要找出是從哪釋放的。
找到釋放對象的最笨的方法,是在free()函數(shù)里打印調(diào)用棧。
但這么做有兩個問題:
1、log太量多,一秒內(nèi)可能會有成千上萬的malloc/free函數(shù)被調(diào)用。
2、打印調(diào)用棧的函數(shù)本身會調(diào)用free函數(shù),這樣會陷入死循環(huán)。
為了解決上面兩個問題,需要用到hook技術(shù)。
關(guān)于hook技術(shù):
要了解hook技術(shù),得先了解外部函數(shù)的調(diào)用過程。
所謂外部函數(shù)就是外部模塊中定義的函數(shù)。比如,libhwui.so中的某個源文件中調(diào)用了malloc函數(shù),而這個malloc函數(shù)是libc.so中定義的。
當編譯libhwui.so的這個源文件時,對應(yīng)調(diào)用malloc的地方會生成如下的匯編代碼: blx?addr
這里blx是arm的跳轉(zhuǎn)指令,addr是目標地址,也就是malloc函數(shù)的地址,那這個malloc函數(shù)的地址如何確定?
這個編譯的階段是無法確定的,只有當運行時進程加載完libc.so以后,malloc函數(shù)的地址才能被確定。
所以編譯器在編譯的時候會在libbinder.so中留出一部分空間作為地址表,專門用于存放外部函數(shù)的地址,這個區(qū)域叫g(shù)ot表。
每一個本模塊調(diào)用到的外部函數(shù)都對應(yīng)got表中的一項。
當然got表里面的內(nèi)容是在進程啟動階段,加載動態(tài)庫時被連接器linker填充的。
而編譯階段我們只需要將代碼寫成:
1、從got表對應(yīng)位置獲取外部函數(shù)地址
2、跳轉(zhuǎn)到這個外部函數(shù)的地址
這個動作需要由若干的指令來完成,所以跳轉(zhuǎn)指令blx addr中的addr其實指向本模塊的一組指令: blx?cb74?
這組指令所在的區(qū)域就是elf文件結(jié)構(gòu)里的plt表,plt表中每一個外部函數(shù)都對應(yīng)一個表項,如:
0000cb74 :
cb74: e28fc600 add ip, pc, #0, 12
cb78: e28cca29 add ip, ip, #167936 ;
cb7c: e5bcf1e8 ldr pc, [ip, #488]! ;
0000c8bc :
c8bc: e28fc600 add ip, pc, #0, 12
c8c0: e28cca29 add ip, ip, #167936 ;
c8c4: e5bcf3b8 ldr pc, [ip, #952]! ;
每一個plt表項都是做相同操作:
1、先獲取got表中外目標函數(shù)對應(yīng)的地址(前兩行);
2、從got表中獲取地址目標函數(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的代碼段是多個進程共享的,但它的數(shù)據(jù)段私有的,而got表就是數(shù)據(jù)段。
所以我們只修改music應(yīng)用進程的libhwui.so的got表中free函數(shù)對應(yīng)的項,影響范圍將大大減少。
那改成什么值呢?一般是我們自己定義的函數(shù),比如: void?inject_free(void?*ptr)???{?????ALOGI("free?ptr=%p",ptr);?????dumpNativeStack();?????dumpJavaStack();?????free(ptr);?}
為了不影響原來的邏輯,打印完debug信息,還是要調(diào)用原來被hook的函數(shù)。
有了hook技術(shù)后能完美的解決野指針中的兩個問題,下面繼續(xù)分析問題。
點贊 0
總結(jié)
以上是生活随笔為你收集整理的java导致native非法指令,Java代码引起的NATIVE野指针问题(上)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux 修改网卡报错xe,cento
- 下一篇: mysql里边字符函数_mysql函数(