字節Android Native Crash治理之Memory Corruption工具原理與實踐

內容摘要

​ MemCorruption工具是字節跳動AppHealth (Client Infrastructure - AppHealth) 團隊開發的一款用於定位野指針(UseAfterFree)、內存越界(HeapBufferOverflow)、重複釋放(DoubleFree)類問題檢測工具。廣泛用於字節跳動旗下各大 App 線上問題檢測。本文將通過方案原理和實踐案例來介紹此工具。

背景

​ 隨着 Android App 開發的技術棧不斷向Native層擴展,帶來的線上Native穩定性問題日趨嚴重。Android中有超過半數的漏洞都來源於Memory Corruption問題。分析定位線上此類問題的難點在於,首先線下難復現,其次問題發生時已經不是第一案發現場,且此類問題調用棧表現類型多樣化。這就導致了此類問題短期內難分析、難定位、難解決的現狀。

什麼是Memory Corruption問題

UseAfterFree

UseAfterFree下面簡稱UAF,野指針類問題;

void HeapUseAfterFree() {
  int *ptr1 = (int*)malloc(4);
  if(ptr1 != NULL){
    *ptr1 = 0xcccc;
    free(ptr1);           //free ptr1           
    *ptr1 = 0xabcd;       //free後write ptr1 mem這裏不會崩潰
  }
}

​ 這裏以UAF問題說明Native崩潰後不是第一現場的場景。假設上面代碼運行在線程A,第2行申請4byte大小的一塊堆內存,第5行釋放這塊堆內存,執行第6行前線程A時間片執行完,切換到線程B執行,線程B此時申請4byte大小的內存塊,內存管理器會概率性的分配之前已經釋放的ptr1指向的內存塊分配給線程B使用,線程B給ptr2指向內存賦值0xff,之後線程B時間片執行完讓出CPU,切換線程A執行,ptr1被賦值0xabcd,之後切換回線程B進行條件判斷,ptr2內存值不爲0xff觸發異常邏輯。不是線程B預期的值。這樣的場景在大型的App程序運行過程中時有發生。

DoubleFree

DoubleFree下面簡稱DF,堆內存二次釋放類問題;

void DoubleFree() {
  int *ptr = (int*)malloc(4);
  free(ptr);
  free(ptr);
}

​ 同一塊堆內存地址多次釋放問題,在實際開發中會有這樣的場景,A線程某個C++類X申請了一塊堆內存,將內存地址傳遞給Y類方法使用,使用後通過析構函數釋放,B線程中申請同樣大小的內存,申請到了這個已經釋放的地址,此時A線程的X類執行析構函數釋放對應內存。

HeapBufferOverflow

HeapBufferOverflow下面簡稱HBO,堆內存越界類問題;

void HeapBufferOverflow() {
  char *ptr = (char*)malloc(sizeof(char)*100);
  *(ptr+101) = "aa";
  *(ptr+102) = "bb";
  *(ptr+103) = "cc";
  *(ptr+104) = "dd";
  free(ptr);
}

堆越界問題就更容易理解了,這裏不再贅述。

工具現狀

​ 業界有很多優秀的工具用於Memory Corruption問題分析,如Asan(Address Sanitizer)、HWASAN、Valgrind或Coredump等。但由於兼容性、性能功耗、接入成本過高、系統限制等因素導致這些工具無法在Android App客戶端線上大規模使用。因此難以定位大規模用戶場景下的複雜問題。

工具對比:

字節方案

能否開發一個線上檢測Memory Corruption類問題的工具?答案是肯定的。

開發前首先要明確需要解決那些問題。

  • 需要解決的問題如下:

    • 兼容性強、性能開銷低、內存消耗小、穩定性高;
    • 棧回溯高效且準確,需要記錄線程信息、內存分配大小和內存地址信息;
  • 功能可配置化管理,方便線上線下使用,接入成本低;

  • 用戶無感知檢測,發生異常時不觸發崩潰;

​ 主旨思想是對App申請和釋放的內存進行統一管理,達到對內存分配和釋放的監管。由於內存申請釋放非常頻繁,如果監控所有內存並記錄想要的信息,會對性能造成影響,所以工具通過mmap來申請一塊內存,自己維護管理。內存申請策略根據隨機採樣分配,命中採樣規則後,通過工具管理的內存池進行分配和釋放,並對內存訪問權限進行控制,在分配的內存塊前後添加隔離區,對釋放後的內存設置爲不可讀寫權限,並標記內存狀態。通過一個數據結構來記錄線程信息、線程棧幀、記錄當前內存塊狀態達到檢測的目的。同時通過線上動態下發配置方式實現可配置化管理。

Hook工具選型

​ 定位Memory Corruption類問題,首先要Hook內存申請和釋放的相關函數,達到對內存監控。這裏涉及到Hook方案的選型,線上首先需要考慮的是高效穩定、兼容性好。

常用的線上Hook工具類型如下:

​ 從工具對比看,經過大量實驗,首選dispatch table hook,因爲malloc/free相關函數非常高頻使用,hook dispatch table方式高效穩定,性能影響小,線上可以大規模開啓。hook原理主要是找到dispatch表地址,替換表中malloc相關函數地址就可以達到hook malloc相關函數的需求。因爲是hook callee,所以不用考慮hook增量庫的問題。同時Google/LLVM在對malloc進行代理處理時就是使用這種方式。針對Memory Corruption類問題往往都是小內存申請釋放(4k以內)造成的問題,所以暫時不需要hook mmap相關函數。兼容方面我們適配了Android5.x~11。

棧回溯方案選型

​ 安卓上的棧回溯標準種類繁多,通過調研比較主流棧回溯方案。

​ 通過比較和實驗,在Arm64上我們選用了fp的方式來進行棧回溯,Arm64 設備幀指針默認是開啓狀態,且通過實驗觀察線上App中64位的so沒有關閉幀指針,且fp方式棧回溯幾乎不耗時,通過實際測試,15層棧幀回溯平均在1~2μs,其他棧回溯基本都在ms級別。

​ 對於 Arm32 設備,幀指針默認是關閉狀態。所以在Arm32設備無法通過fp棧回溯方式記錄App的內存分配和釋放流程。我們對libunwind_stack進行了優化,因此在Arm32下我們選擇libunwind_stack來實現棧回溯,來達到記錄分配和釋放堆棧軌跡。

雙採樣內存配置策略

​ 對於現有Memory Corruption類問題監控,往往是通過注入、插樁方式監控所有內存申請和釋放,而用戶使用中一次滑動事件,App程序都會申請釋放數千到數萬次。疊加棧回溯能力,會對被監控程序造成嚴重的性能影響,導致用戶體驗變差,出現卡頓等問題。

​ 針對這類問題,這裏採用雙採樣機制來控制用戶數與客戶端內存分配數的方式。雙採樣是指服務端發送配置文件採樣和客戶端針對內存分配進行隨機採樣分配管理的方法,來對內存分配和釋放進行監控**。**服務端配置文件採樣是通過服務端設置用戶採樣比,按照不同問題類型、版本、機型等策略來進行按比例採樣;客戶端隨機內存分配採樣是通過端上隨機分配採樣算法來實現。這樣對用戶量和端上監控內存數就可進行隨機內存分配配置化管理。

無感知檢測

​ 當發生Memory Corruption類問題時,正常是會觸發SIGSEGV類型崩潰。要做到用戶無感知,就不能讓程序發生崩潰退出。這裏我們的做法是通過註冊SIGSEGV信號處理函數,當受控內存塊被釋放後會設置爲不可讀寫權限,當發生異常時有代碼訪問不可讀寫的這塊內存就會觸發SIGSEGV,進入信號處理函數,在信號處理函數中,先確定當前發生異常的地址是否在我們管理的內存池中,如果是我們管理的內存段觸發的異常,通過恢復對應內存段的讀寫權限。來保障在信號處理流程中不觸發程序退出流程。達到用戶無感知檢測Memory Corruption類問題。如果觸發SIGSEGV的內存地址不在我們管理的內存段中,就轉發信號給原有的信號處理函數處理。

方案流程

方案優缺點

  • 優點

    • 線上線下可用,接入成本低,依賴aar組件初始化即可,無需額外操作;
    • 可配置化管理,通過雲端下發配置,動態開關功能;
    • 內存分配採樣管理,內存池線上控制在100KB~8MB,總內存開銷在700KB~8.6MB;

  • 缺點

    • 監控內存塊大小,最大4kb;
    • 對非堆內存導致的崩潰問題無法檢測;
    • 暫時不支持ios/x86,後期可支持;
    • 不支持Android4.4及以下版本,後期可支持;

線上效果與案例分析

​ MemCorruption工具在字節多個App上線後,目前發現各類基礎庫Memory Corruption問題200+。通過工具已定位解決問題30+。

案例1、UseAfterFree問題

​ 日誌記錄信息,異常棧、Free棧、Alloc棧。Abort msg會記錄內存分配大小,Free棧和Alloc棧記錄分配和釋放的線程信息。通過這些信息可以知道一塊內存的分配和釋放情況。結合源碼即可定位問題。

​ 下面是線上檢測字節頭部業務SDK有UAF問題,Abort msg信息可知是UAF問題、申請內存大小256byte,訪問內存0x7a25a28b00偏移240byte時觸發UAF檢測。這裏也就是訪問了一個結構體變量的成員變量時發生了UAF問題。說明對應結構體變量被釋放後又使用。

  • 異常棧與Abort msg

觸發檢測代碼邏輯

  • Free棧信息

​ 通過Free內存塊棧信息,可以確定是11170線程釋放了對應內存,結合代碼可定位釋放內存塊變量m_pDefaultFilter,而m_filterType是m_pDefaultFilter的成員變量。

Free對象內存代碼

  • Alloc內存信息

​ 通過上面信息我們很容易能判斷出是因爲m_pDefaultFilter實例對象已經被釋放,之後訪問其成員變量m_filtertype時內存已經釋放,就會觸發UAF檢測。MemCorruption工具比傳統的Tombstone只有一個異常棧的情況下,對分析問題更清晰,且抓到的是問題第一現場。縮短研發同學對問題排查時間,以提升問題處理效率。

案例2、DoubleFree問題

  • 異常棧與Abort msg

  • Free棧信息

  • Alloc棧信息

​ 從上述信息可知,libbinder庫中存在double free異常,free有兩條鏈路可以釋放Parcel類的mData或mObjects對象。

鏈路一:在java層調用recycle-->freebuffer--...-->freeData-->freeDataNoInit-->free,在freeDataNoInit中會free mData和mObjects兩個對象;

鏈路二:在java層調用writeString--> nativeWritexxx-->writexxx16-->writexxx-->continueWrite-->realloc-->free;

continueWrite和freeDataNoInit代碼在Parcel.cpp且沒有保護,對於mData和mObjects對象的生命週期存在併發導致的多次釋放問題。結合異常棧信息,業務代碼做保護修復。

總結

​ Memory Corruption問題是C/C++開發人員避不開的問題。MemCorruption工具原理並不複雜,在大規模用戶場景下,通過採樣方式監控內存分配和釋放,發現問題不觸發程序崩潰,能夠有效的發現線上低概率、邊緣場景引發的Memory Corruption問題。減少App程序漏洞、提升App穩定性。

​ MemCorruption工具只是字節治理線上Memory Corruption類問題的一個點。還有很多的方面需要完善。請持續關注字節跳動終端技術團隊,後續更加精彩。

後續計劃

​ MemCorruption工具爲了不影響線上App性能,對內存監控範圍做了限制,後續我們會擴展這部分的能力。同時iOS中也存在Memory Corruption類問題,iOS版本敬請期待。

​ 此工具未來將在 APMPlus 中上線,APMPlus 是字節跳動應用開發套件 MARS 下的性能監控產品,通過先進的數據採集與監控技術,爲企業提供全鏈路的應用性能監控服務,解決企業對各端監控的需求。具備非侵入式監控、豐富的異常現場還原能力,助力企業提升異常問題排查與解決的效率、優化應用品質,以降低成本提高收入。

​ 目前 APMPlus 面向新用戶提供試用30 天的限時免費服務。其中包含 App 監控、Web 監控、Server 監控、小程序監控,App 監控和 Web 監控各500 萬條事件量, Server 與小程序監控限時不限量,歡迎免費接入試用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章