做者介紹: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
想了解更多?
那就關注咱們吧!
小米開放平臺公衆號二維碼