ASAN和HWASAN原理解析

因爲虛擬機的存在,Android應用開發者們一般不用考慮內存訪問相關的錯誤。而一旦咱們深刻到Native世界中,本來面容和藹的內存便開始兇惡起來。這時,因爲程序員寫法不規範、邏輯疏漏而致使的內存錯誤會通通跳到咱們面前,對咱們嘲諷一番。html

這些錯誤既影響了程序的穩定性,也影響了程序的安全性,由於好多惡意代碼就經過內存錯誤來完成入侵。不過麻煩的是,Native世界中的內存錯誤很難排查,由於不少時候致使問題的地方和發生問題的地方相隔甚遠。爲了更好地解決這些問題,各路大神紛紛祭出本身手中的神器,相互PK,相互補充。linux

ASAN(Address Sanitizer)和HWASAN(Hardware-assisted Address Sanitizer)就是這些工具中的佼佼者。android

在ASAN出來以前,市面上的內存調試工具要麼慢,要麼只能檢測部份內存錯誤,要麼這兩個缺點都有。總之,不夠優秀。c++

HWASAN則是ASAN的升級版,它利用了64位機器上忽略高位地址的特性,將這些被忽略的高位地址從新利用起來,從而大大下降了工具對於CPU和內存帶來的額外負載。git

1. ASAN

ASAN工具包含兩大塊:程序員

  • 插樁模塊(Instrumentation module)
  • 一個運行時庫(Runtime library)

插樁模塊主要會作兩件事:github

  1. 對全部的memory access都去檢查該內存所對應的shadow memory的狀態。這是靜態插樁,所以須要從新編譯。
  2. 爲全部棧上對象和全局對象建立先後的保護區(Poisoned redzone),爲檢測溢出作準備。

運行時庫也一樣會作兩件事:算法

  1. 替換默認路徑的malloc/free等函數。爲全部堆對象建立先後的保護區,將free掉的堆區域隔離(quarantine)一段時間,避免它當即被分配給其餘人使用。
  2. 對錯誤狀況進行輸出,包括堆棧信息。

1.1 Shadow Memory

若是想要了解ASAN的實現原理,那麼shadow memory將是第一個須要瞭解的概念。安全

Shadow memory有一些元數據的思惟在裏面。它雖然也是內存中的一塊區域,可是其中的數據僅僅反應其餘正常內存的狀態信息。因此能夠理解爲正常內存的元數據,而正常內存中存儲的纔是程序真正須要的數據。bash

Malloc函數返回的地址一般是8字節對齊的,所以任意一個由(對齊的)8字節所組成的內存區域必然落在如下9種狀態之中:最前面的k(0≤k≤8)字節是可尋址的,而剩下的8-k字節是不可尋址的。這9種狀態即可以用shadow memory中的一個字節來進行編碼。

實際上,一個byte能夠編碼的狀態總共有256(2^8)種,所以用在這裏綽綽有餘。

Shadow memory和normal memory的映射關係如上圖所示。一個byte的shadow memory反映8個byte normal memory的狀態。那如何根據normal memory的地址找到它對應的shadow memory呢?

對於64位機器上的Android而言,兩者的轉換公式以下:

Shadow memory address = (Normal memory address >> 3) + 0x100000000

右移三位的目的是爲了完成 8➡1的映射,而加一個offset是爲了和Normal memory區分開來。最終內存空間種會存在以下的映射關係:

Bad表明的是shadow memory的shadow memory,所以其中數據沒有意義,該內存區域不可以使用。

上文中提到,8字節組成的memory region共有9中狀態:

  • 1~7個字節可尋址(共七種),shadow memory的值爲1~7。
  • 8個字節均可尋址,shadow memory的值爲0。
  • 0個字節可尋址,shadow memory的值爲負數。

爲何0個字節可尋址的狀況shadow memory不爲0,而是負數呢?是由於0個字節可尋址其實能夠繼續分爲多種狀況,譬如:

  • 這塊區域是heap redzones
  • 這塊區域是stack redzones
  • 這塊區域是global redzones
  • 這塊區域是freed memory

對全部0個字節可尋址的normal memory region的訪問都是非法的,ASAN將會報錯。而根據其shadow memory的值即可以具體判斷是哪種錯。

Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:     fa (實際上Heap right redzone也是fa)
  Freed Heap region:     fd
  Stack left redzone:    f1
  Stack mid redzone:     f2
  Stack right redzone:   f3
  Stack after return:    f5
  Stack use after scope: f8
  Global redzone:        f9
  Global init order:     f6
  Poisoned by user:      f7
  Container overflow:    fc
  Array cookie:          ac
  Intra object redzone:  bb
  ASan internal:         fe
  Left alloca redzone:   ca
  Right alloca redzone:  cb
  Shadow gap:            cc
複製代碼

1.2 檢測算法

ShadowAddr = (Addr >> 3) + Offset;
k = *ShadowAddr;
if (k != 0 && ((Addr & 7) + AccessSize > k))
	ReportAndCrash(Addr);
複製代碼

在每次內存訪問時,都會執行如上的僞代碼,以判斷這次內存訪問是否合規。

首先根據normal memory的地址找到對應shadow memory的地址,而後取出其中存取的byte值:k。

  • k!=0,說明Normal memory region中的8個字節並非均可以被尋址的。
  • Addr & 7,將得知這次內存訪問是從memory region的第幾個byte開始的。
  • AccessSize是這次內存訪問須要訪問的字節長度。
  • (Addr&7)+AccessSize > k,則說明這次內存訪問將會訪問到不可尋址的字節。(具體可分爲k大於0和小於0兩種狀況來分析)

當這次內存訪問可能會訪問到不可尋址的字節時,ASAN會報錯並結合shadow memory中具體的值明確錯誤類型。

1.3 典型錯誤

1.3.1 Use-After-Free

想要檢測UseAfterFree的錯誤,須要有兩點保證:

  1. 已經free掉的內存區域須要被標記成特殊的狀態。在ASAN的實現裏,free掉的normal memory對應的shadow memory值爲0xfd(猜想有freed的意思)。
  2. 已經free掉的內存區域須要放入隔離區一段時間,防止發生錯誤時該區域已經經過malloc從新分配給其餘人使用。一旦分配給其餘人使用,則可能漏掉UseAfterFree的錯誤。

測試代碼:

// RUN: clang -O -g -fsanitize=address %t && ./a.out
int main(int argc, char **argv) {
  int *array = new int[100];
  delete [] array;
  return array[argc];  // BOOM
}
複製代碼

ASAN輸出的錯誤信息:

=================================================================
==6254== ERROR: AddressSanitizer: heap-use-after-free on address 0x603e0001fc64 at pc 0x417f6a bp 0x7fff626b3250 sp 0x7fff626b3248
READ of size 4 at 0x603e0001fc64 thread T0
    #0 0x417f69 in main example_UseAfterFree.cc:5
    #1 0x7fae62b5076c (/lib/x86_64-linux-gnu/libc.so.6+0x2176c)
    #2 0x417e54 (a.out+0x417e54)
0x603e0001fc64 is located 4 bytes inside of 400-byte region [0x603e0001fc60,0x603e0001fdf0)
freed by thread T0 here:
    #0 0x40d4d2 in operator delete[](void*) /home/kcc/llvm/projects/compiler-rt/lib/asan/asan_new_delete.cc:61
    #1 0x417f2e in main example_UseAfterFree.cc:4
previously allocated by thread T0 here:
    #0 0x40d312 in operator new[](unsigned long) /home/kcc/llvm/projects/compiler-rt/lib/asan/asan_new_delete.cc:46
    #1 0x417f1e in main example_UseAfterFree.cc:3
Shadow bytes around the buggy address:
  0x1c07c0003f30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x1c07c0003f40: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x1c07c0003f50: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x1c07c0003f60: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x1c07c0003f70: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x1c07c0003f80: fa fa fa fa fa fa fa fa fa fa fa fa[fd]fd fd fd
  0x1c07c0003f90: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x1c07c0003fa0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x1c07c0003fb0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fa fa
  0x1c07c0003fc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x1c07c0003fd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
複製代碼

能夠看到,=>指向的那行有一個byte數值用中括號給圈出來了:[fd]。它表示的是這次出錯的內存地址對應的shadow memory的值。而其以前的fa表示Heap left redzone,它是以前該區域有效時的遺留產物。連續的fd總共有50個,每個shadow memory的byte和8個normal memory byte對應,因此能夠知道這次free的內存總共是50×8=400bytes。這一點在上面的log中也獲得了驗證,截取出來展現以下:

0x603e0001fc64 is located 4 bytes inside of 400-byte region [0x603e0001fc60,0x603e0001fdf0)
複製代碼

此外,ASAN的log中不只有出錯時的堆棧信息,還有該內存區域以前free時的堆棧信息。所以咱們能夠清楚地知道該區域是如何被釋放的,從而快速定位問題,解決問題。

1.3.2 Heap-Buffer-Overflow

想要檢測HeapBufferOverflow的問題,只須要保證一點:

  • 正常的Heap先後須要插入必定長度的安全區,並且此安全區對應的shadow memory須要被標記爲特殊的狀態。在ASAN的實現裏,安全區被標記爲0xfa。

測試代碼:

ASAN輸出的錯誤信息:

=================================================================
==1405==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x0060bef84165 at pc 0x0058714bfb24 bp 0x007fdff09590 sp 0x007fdff09588
WRITE of size 1 at 0x0060bef84165 thread T0
    #0 0x58714bfb20 (/system/bin/bootanimation+0x8b20)
    #1 0x7b434cd994 (/apex/com.android.runtime/lib64/bionic/libc.so+0x7e994)

0x0060bef84165 is located 1 bytes to the right of 100-byte region [0x0060bef84100,0x0060bef84164)
allocated by thread T0 here:
    #0 0x7b4250a1a4 (/system/lib64/libclang_rt.asan-aarch64-android.so+0xc31a4)
    #1 0x58714bfac8 (/system/bin/bootanimation+0x8ac8)
    #2 0x7b434cd994 (/apex/com.android.runtime/lib64/bionic/libc.so+0x7e994)
    #3 0x58714bb04c (/system/bin/bootanimation+0x404c)
    #4 0x7b45361b04 (/system/bin/bootanimation+0x54b04)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/system/bin/bootanimation+0x8b20) 
Shadow bytes around the buggy address:
  0x001c17df07d0: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
  0x001c17df07e0: fd fd fd fd fd fa fa fa fa fa fa fa fa fa fa fa
  0x001c17df07f0: fd fd fd fd fd fd fd fd fd fd fd fd fd fa fa fa
  0x001c17df0800: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
  0x001c17df0810: fd fd fd fd fd fa fa fa fa fa fa fa fa fa fa fa
=>0x001c17df0820: 00 00 00 00 00 00 00 00 00 00 00 00[04]fa fa fa
  0x001c17df0830: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x001c17df0840: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x001c17df0850: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x001c17df0860: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x001c17df0870: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
複製代碼

能夠看到最終出錯的shadow memory值爲0x4,表示該shadow memroy對應的normal memory中只有前4個bytes是可尋址的。0x4的shadow memory前還有12個0x0,表示其前面的12個memory region(每一個region有8個byte)都是徹底可尋址的。所以全部可尋址的大小=12×8+4=100,正是代碼中malloc的size。之因此這次訪問會出錯,是由於地址0x60bef84165意圖訪問最後一個region的第五個byte,而該region只有前四個byte可尋址。因爲0x4後面是0xfa,所以這次錯誤屬於HeapBufferOverflow。

1.4 缺陷

自從2011年誕生以來,ASAN已經成功地參與了衆多大型項目,譬如Chrome和Android。雖然它的表現很突出,但仍然有些地方不盡如人意,重點表如今如下幾點:

  1. ASAN的運行是須要消耗memory和CPU資源的,此外它也會增長代碼大小。它的性能相比於以前的工具確實有了質的提高,但仍然沒法適用於某些壓力測試場景,尤爲是須要全局打開的時候。這一點在Android上尤其明顯,每當咱們想要全局打開ASAN調試某些奇葩問題時,系統總會由於負載太重而跑不起來。
  2. ASAN對於UseAfterFree的檢測依賴於隔離區,而隔離時間是非永久的。也就意味着已經free的區域過一段時間後又會從新被分配給其餘人。當它被從新分配給其餘人後,原先的持有者再次訪問此塊區域將不會報錯。由於這一塊區域的shadow memory再也不是0xfd。因此這算是ASAN漏檢的一種狀況。
  3. ASAN對於overflow的檢測依賴於安全區,而安全區總歸是有大小的。它多是64bytes,128bytes或者其餘什麼值,但無論怎麼樣終歸是有限的。若是某次踩踏跨過了安全區,踩踏到另外一片可尋址的內存區域,ASAN一樣不會報錯。這是ASAN的另外一種漏檢。

2.HWASAN

HWASAN是ASAN工具的「升級版」,它基本上解決了上面所說的ASAN的3個問題。可是它須要64位硬件的支持,也就是說在32位的機器上該工具沒法運行。

AArch64是64位的架構,指的是寄存器的寬度是64位,但並不表示內存的尋址範圍是2^64。真實的尋址範圍和處理器內部的總線寬度有關,實際上ARMv8尋址只用到了低48位。也就是說,一個64bit的指針值,其中真正用於尋址的只有低48位。那麼剩下的高16位幹什麼用呢?答案是隨意發揮。AArch64擁有地址標記(Address tagging, or top-byte-ignore)的特性,它表示容許軟件使用64bit指針值的高8位開發特定功能。

HWASAN用這8bit來存儲一塊內存區域的標籤(tag)。接下來咱們以堆內存示例,展現這8bit到底如何起做用。

堆內存經過malloc分配出來,HWASAN在它返回地址時會更改該有效地址的高8位。更改的值是一個隨機生成的單字節值,譬如0xaf。此外,該分配出來的內存對應的shadow memory值也設爲0xaf。須要注意的是,HWASAN中normal memory和shadow memory的映射關係是16➡1,而ASAN中兩者的映射關係是8➡1。

如下分別討論UseAfterFree和HeapOverFlow的狀況。

2.1 Use-After-Free

當一個堆內存被分配出來時,返回給用戶空間的地址便已經帶上了標籤(存儲於地址的高8位)。以後經過該地址進行內存訪問,將先檢測地址中的標籤值和訪問地址對應的shadow memory的值是否相等。若是相等則驗證經過,能夠進行正常的內存訪問。

當該內存被free時,HWASAN會爲該塊區域分配一個新的隨機值,存儲於其對應的shadow memory中。若是此後再有新的訪問,則地址中的標籤值必然不等於shadow memory中存儲的新的隨機值,所以會有錯誤產生。經過以下圖示能夠很好地明白這一點(圖中只用了4bit記錄標記值,但不影響理解,8bit標記值的檢測和它一致)。

2.2 Heap-Over-Flow

想要檢測HeapOverFlow,有一個前提須要知足:相鄰的memory區域須要有不一樣的shadow memory值,不然將沒法分辨兩個不一樣的memory區域。爲每一個memory區域隨機分配將有機率讓兩個相鄰區域具備一樣的shadow memory值,雖然機率比較小,但總歸是個缺陷。所以工具中會有其餘邏輯保證這個前提。

下圖展現了HeapOverFlow的檢測過程。指針p的標籤和訪問的地址p[32]所對應的shadow memory值不一致,所以報錯(圖中只用了4bit記錄標記值,但不影響理解,8bit標記值的檢測和它一致)。

2.3 錯誤信息示例

Abort message: '==12528==ERROR: HWAddressSanitizer: tag-mismatch on address 0x003d557e2c20 at pc 0x00748b4a6918 READ of size 4 at 0x003d557e2c20 tags: d1/9b (ptr/mem) in thread T0 #0 0x748b4a6914 (/system/lib64/libutils.so+0x11914) #1 0x748a521bdc (/apex/com.android.runtime/lib64/bionic/libc.so+0x121bdc) #2 0x748a51ad7c (/apex/com.android.runtime/lib64/bionic/libc.so+0x11ad7c) #3 0x748a47f830 (/apex/com.android.runtime/lib64/bionic/libc.so+0x7f830) [0x003d557e2c20,0x003d557e2c80) is a small unallocated heap chunk; size: 96 offset: 0 Thread: T0 0x006b00002000 stack: [0x007fcd371000,0x007fcdb71000) sz: 8388608 tls: [0x000000000000,0x000000000000) HWAddressSanitizer can not describe address in more detail. Memory tags around the buggy address (one tag corresponds to 16 bytes): e1 e1 e1 e1 83 83 83 83 83 00 a3 a3 a3 a3 a3 a3 b7 b7 b7 b7 b7 00 01 01 01 01 01 00 95 95 95 95 95 00 ec ec ec ec ec 00 c8 c8 c8 c8 c8 00 21 21 21 21 21 00 cb cb cb cb cb 00 b8 b8 b8 b8 b8 00 14 14 14 14 14 14 b9 b9 b9 b9 b9 b9 89 89 89 89 89 89 95 95 95 95 95 95 47 47 47 47 47 00 fe fe fe fe fe 00 c5 c5 c5 c5 c5 00 8e 8e 8e 8e 8e 8e 5c 5c 5c 5c 5c 5c af af af af af af b0 b0 b0 b0 => b0 b0 [9b] 9b 9b 9b 9b 9b 1f 1f 1f 1f 1f 1f 69 69 <= 69 69 69 a0 7a 7a 7a 7a 7a ff eb eb eb eb eb eb 16 16 16 16 16 16 81 81 81 81 81 81 7f 7f 7f 7f 7f 7f 57 57 57 57 57 57 e0 e0 e0 e0 e0 e0 94 94 94 94 94 00 35 35 35 35 35 35 98 98 98 98 98 00 7d 7d 7d 7d 7d 7d 6e 6e 6e 6e 6e 6e 59 59 59 59 59 59 8e 8e 8e 8e 8e 8e 6d 6d 6d 6d 6d 6d 69 69 69 69 69 69 d5 d5 d5 d5 d5 d5 63 63 63 63 63 63 複製代碼

0x9b總共有6個,所以該memory區域的總長爲6×16=96,與上述提示一致。

[0x003d557e2c20,0x003d557e2c80) is a small unallocated heap chunk; size: 96
複製代碼

2.4 優缺點

和ASAN相比,HWASAN具備以下缺點:

  1. 可移植性較差,只適用於64位機器。
  2. 須要對Linux Kernel作一些改動以支持工具。
  3. 對於全部錯誤的檢測將有必定機率false negative(漏掉一些真實的錯誤),機率爲1/256。緣由是tag的生成只能從256(2^8)個數中選一個,所以不一樣地址的tag將有可能相同。

不過相對於這些缺點,HWASAN所擁有的優勢更加引人注目:

  1. 再也不須要安全區來檢測buffer overflow,既極大地下降了工具對於內存的消耗,也不會出現ASAN中某些overflow檢測不到的狀況。
  2. 再也不須要隔離區來檢測UseAfterFree,所以不會出現ASAN中某些UseAfterFree檢測不到的狀況。

2.5 一個難題

上述的討論其實迴避了一個問題:若是一個16字節的memory region中只有前幾個字節可尋址(假設是5),那麼其對應的shadow memory值也是5。這時,若是用地址去訪問該region的第2個字節,那麼如何判斷訪問是否合規呢?

此時直接對比地址的tag和shadow memory的值確定是不行的,由於此時的shadow memory值含義發生了變化,它再也不是一個相似於tag的隨機值,而是memory region中可訪問字節的數目。

爲了解決這個難題,HWASAN在這種狀況下將memory region的隨機值保存在最後一個字節中。因此即使地址的tag和shadow memory的值不等,但只要和memory region中最後一個字節相等,也代表該訪問合法。

具體可參考連接:clang.llvm.org/docs/Hardwa…

參考文章:

  1. www.usenix.org/system/file…
  2. arxiv.org/ftp/arxiv/p…
  3. clang.llvm.org/docs/Hardwa…
  4. github.com/google/sani…
相關文章
相關標籤/搜索