由于虛擬機(jī)的存在,Android應(yīng)用開發(fā)者們通常不用考慮內(nèi)存訪問相關(guān)的錯誤。而一旦我們深入到Native世界中,原本面容和善的內(nèi)存便開始兇惡起來。這時,由于程序員寫法不規(guī)范、邏輯疏漏而導(dǎo)致的內(nèi)存錯誤會統(tǒng)統(tǒng)跳到我們面前,對我們嘲諷一番。
這些錯誤既影響了程序的穩(wěn)定性,也影響了程序的安全性,因為好多惡意代碼就通過內(nèi)存錯誤來完成入侵。不過麻煩的是,Native世界中的內(nèi)存錯誤很難排查,因為很多時候?qū)е聠栴}的地方和發(fā)生問題的地方相隔甚遠(yuǎn)。為了更好地解決這些問題,各路大神紛紛祭出自己手中的神器,相互PK,相互補(bǔ)充。
ASAN(Address Sanitizer)和HWASAN(Hardware-assisted Address Sanitizer)就是這些工具中的佼佼者。
在ASAN出來之前,市面上的內(nèi)存調(diào)試工具要么慢,要么只能檢測部分內(nèi)存錯誤,要么這兩個缺點都有??傊粔騼?yōu)秀。
HWASAN則是ASAN的升級版,它利用了64位機(jī)器上忽略高位地址的特性,將這些被忽略的高位地址重新利用起來,從而大大降低了工具對于CPU和內(nèi)存帶來的額外負(fù)載。
1. ASAN
ASAN工具包含兩大塊:
插樁模塊(Instrumentation module)
一個運行時庫(Runtime library)
插樁模塊主要會做兩件事:
對所有的memory access都去檢查該內(nèi)存所對應(yīng)的shadow memory的狀態(tài)。這是靜態(tài)插樁,因此需要重新編譯。
為所有棧上對象和全局對象創(chuàng)建前后的保護(hù)區(qū)(Poisoned redzone),為檢測溢出做準(zhǔn)備。
運行時庫也同樣會做兩件事:
替換默認(rèn)路徑的malloc/free等函數(shù)。為所有堆對象創(chuàng)建前后的保護(hù)區(qū),將free掉的堆區(qū)域隔離(quarantine)一段時間,避免它立即被分配給其他人使用。
對錯誤情況進(jìn)行輸出,包括堆棧信息。
1.1 Shadow Memory
如果想要了解ASAN的實現(xiàn)原理,那么shadow memory將是第一個需要了解的概念。
Shadow memory有一些元數(shù)據(jù)的思維在里面。它雖然也是內(nèi)存中的一塊區(qū)域,但是其中的數(shù)據(jù)僅僅反應(yīng)其他正常內(nèi)存的狀態(tài)信息。所以可以理解為正常內(nèi)存的元數(shù)據(jù),而正常內(nèi)存中存儲的才是程序真正需要的數(shù)據(jù)。
Malloc函數(shù)返回的地址通常是8字節(jié)對齊的,因此任意一個由(對齊的)8字節(jié)所組成的內(nèi)存區(qū)域必然落在以下9種狀態(tài)之中:最前面的k(0≤k≤8)字節(jié)是可尋址的,而剩下的8-k字節(jié)是不可尋址的。這9種狀態(tài)便可以用shadow memory中的一個字節(jié)來進(jìn)行編碼。
實際上,一個byte可以編碼的狀態(tài)總共有256(2^8)種,因此用在這里綽綽有余。
Shadow memory和normal memory的映射關(guān)系如上圖所示。一個byte的shadow memory反映8個byte normal memory的狀態(tài)。那如何根據(jù)normal memory的地址找到它對應(yīng)的shadow memory呢?
對于64位機(jī)器上的Android而言,二者的轉(zhuǎn)換公式如下:
Shadow memory address = (Normal memory address 》》 3) + 0x100000000
右移三位的目的是為了完成 81的映射,而加一個offset是為了和Normal memory區(qū)分開來。最終內(nèi)存空間種會存在如下的映射關(guān)系:
Bad代表的是shadow memory的shadow memory,因此其中數(shù)據(jù)沒有意義,該內(nèi)存區(qū)域不可使用。
上文中提到,8字節(jié)組成的memory region共有9中狀態(tài):
1~7個字節(jié)可尋址(共七種),shadow memory的值為1~7。
8個字節(jié)都可尋址,shadow memory的值為0。
0個字節(jié)可尋址,shadow memory的值為負(fù)數(shù)。
為什么0個字節(jié)可尋址的情況shadow memory不為0,而是負(fù)數(shù)呢?是因為0個字節(jié)可尋址其實可以繼續(xù)分為多種情況,譬如:
這塊區(qū)域是heap redzones
這塊區(qū)域是stack redzones
這塊區(qū)域是global redzones
這塊區(qū)域是freed memory
對所有0個字節(jié)可尋址的normal memory region的訪問都是非法的,ASAN將會報錯。而根據(jù)其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);
在每次內(nèi)存訪問時,都會執(zhí)行如上的偽代碼,以判斷此次內(nèi)存訪問是否合規(guī)。
首先根據(jù)normal memory的地址找到對應(yīng)shadow memory的地址,然后取出其中存取的byte值:k。
k!=0,說明Normal memory region中的8個字節(jié)并不是都可以被尋址的。
Addr & 7,將得知此次內(nèi)存訪問是從memory region的第幾個byte開始的。
AccessSize是此次內(nèi)存訪問需要訪問的字節(jié)長度。
(Addr&7)+AccessSize 》 k,則說明此次內(nèi)存訪問將會訪問到不可尋址的字節(jié)。(具體可分為k大于0和小于0兩種情況來分析)
當(dāng)此次內(nèi)存訪問可能會訪問到不可尋址的字節(jié)時,ASAN會報錯并結(jié)合shadow memory中具體的值明確錯誤類型。
1.3 典型錯誤
1.3.1 Use-After-Free
想要檢測UseAfterFree的錯誤,需要有兩點保證:
已經(jīng)free掉的內(nèi)存區(qū)域需要被標(biāo)記成特殊的狀態(tài)。在ASAN的實現(xiàn)里,free掉的normal memory對應(yīng)的shadow memory值為0xfd(猜測有freed的意思)。
已經(jīng)free掉的內(nèi)存區(qū)域需要放入隔離區(qū)一段時間,防止發(fā)生錯誤時該區(qū)域已經(jīng)通過malloc重新分配給其他人使用。一旦分配給其他人使用,則可能漏掉UseAfterFree的錯誤。
測試代碼:
// RUN: clang -O -g -fsanitize=address %t && 。/a.outint 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 0x7fff626b3248READ 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:4previously 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:3Shadow 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數(shù)值用中括號給圈出來了:[fd]。它表示的是此次出錯的內(nèi)存地址對應(yīng)的shadow memory的值。而其之前的fa表示Heap left redzone,它是之前該區(qū)域有效時的遺留產(chǎn)物。連續(xù)的fd總共有50個,每一個shadow memory的byte和8個normal memory byte對應(yīng),所以可以知道此次free的內(nèi)存總共是50×8=400bytes。這一點在上面的log中也得到了驗證,截取出來展示如下:
0x603e0001fc64 is located 4 bytes inside of 400-byte region [0x603e0001fc60,0x603e0001fdf0)
此外,ASAN的log中不僅有出錯時的堆棧信息,還有該內(nèi)存區(qū)域之前free時的堆棧信息。因此我們可以清楚地知道該區(qū)域是如何被釋放的,從而快速定位問題,解決問題。
1.3.2 Heap-Buffer-Overflow
想要檢測HeapBufferOverflow的問題,只需要保證一點:
正常的Heap前后需要插入一定長度的安全區(qū),而且此安全區(qū)對應(yīng)的shadow memory需要被標(biāo)記為特殊的狀態(tài)。在ASAN的實現(xiàn)里,安全區(qū)被標(biāo)記為0xfa。
測試代碼:
ASAN輸出的錯誤信息:
1405==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x0060bef84165 at pc 0x0058714bfb24 bp 0x007fdff09590 sp 0x007fdff09588WRITE 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對應(yīng)的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已經(jīng)成功地參與了眾多大型項目,譬如Chrome和Android。雖然它的表現(xiàn)很突出,但仍然有些地方不盡如人意,重點表現(xiàn)在以下幾點:
ASAN的運行是需要消耗memory和CPU資源的,此外它也會增加代碼大小。它的性能相比于之前的工具確實有了質(zhì)的提升,但仍然無法適用于某些壓力測試場景,尤其是需要全局打開的時候。這一點在Android上尤為明顯,每當(dāng)我們想要全局打開ASAN調(diào)試某些奇葩問題時,系統(tǒng)總會因為負(fù)載過重而跑不起來。
ASAN對于UseAfterFree的檢測依賴于隔離區(qū),而隔離時間是非永久的。也就意味著已經(jīng)free的區(qū)域過一段時間后又會重新被分配給其他人。當(dāng)它被重新分配給其他人后,原先的持有者再次訪問此塊區(qū)域?qū)⒉粫箦e。因為這一塊區(qū)域的shadow memory不再是0xfd。所以這算是ASAN漏檢的一種情況。
ASAN對于overflow的檢測依賴于安全區(qū),而安全區(qū)總歸是有大小的。它可能是64bytes,128bytes或者其他什么值,但不管怎么樣終歸是有限的。如果某次踩踏跨過了安全區(qū),踩踏到另一片可尋址的內(nèi)存區(qū)域,ASAN同樣不會報錯。這是ASAN的另一種漏檢。
2.HWASAN
HWASAN是ASAN工具的“升級版”,它基本上解決了上面所說的ASAN的3個問題。但是它需要64位硬件的支持,也就是說在32位的機(jī)器上該工具無法運行。
AArch64是64位的架構(gòu),指的是寄存器的寬度是64位,但并不表示內(nèi)存的尋址范圍是2^64。真實的尋址范圍和處理器內(nèi)部的總線寬度有關(guān),實際上ARMv8尋址只用到了低48位。也就是說,一個64bit的指針值,其中真正用于尋址的只有低48位。那么剩下的高16位干什么用呢?答案是隨意發(fā)揮。AArch64擁有地址標(biāo)記(Address tagging, or top-byte-ignore)的特性,它表示允許軟件使用64bit指針值的高8位開發(fā)特定功能。
HWASAN用這8bit來存儲一塊內(nèi)存區(qū)域的標(biāo)簽(tag)。接下來我們以堆內(nèi)存示例,展示這8bit到底如何起作用。
堆內(nèi)存通過malloc分配出來,HWASAN在它返回地址時會更改該有效地址的高8位。更改的值是一個隨機(jī)生成的單字節(jié)值,譬如0xaf。此外,該分配出來的內(nèi)存對應(yīng)的shadow memory值也設(shè)為0xaf。需要注意的是,HWASAN中normal memory和shadow memory的映射關(guān)系是161,而ASAN中二者的映射關(guān)系是81。
以下分別討論UseAfterFree和HeapOverFlow的情況。
2.1 Use-After-Free
當(dāng)一個堆內(nèi)存被分配出來時,返回給用戶空間的地址便已經(jīng)帶上了標(biāo)簽(存儲于地址的高8位)。之后通過該地址進(jìn)行內(nèi)存訪問,將先檢測地址中的標(biāo)簽值和訪問地址對應(yīng)的shadow memory的值是否相等。如果相等則驗證通過,可以進(jìn)行正常的內(nèi)存訪問。
當(dāng)該內(nèi)存被free時,HWASAN會為該塊區(qū)域分配一個新的隨機(jī)值,存儲于其對應(yīng)的shadow memory中。如果此后再有新的訪問,則地址中的標(biāo)簽值必然不等于shadow memory中存儲的新的隨機(jī)值,因此會有錯誤產(chǎn)生。通過如下圖示可以很好地明白這一點(圖中只用了4bit記錄標(biāo)記值,但不影響理解,8bit標(biāo)記值的檢測和它一致)。
2.2 Heap-Over-Flow
想要檢測HeapOverFlow,有一個前提需要滿足:相鄰的memory區(qū)域需要有不同的shadow memory值,否則將無法分辨兩個不同的memory區(qū)域。為每個memory區(qū)域隨機(jī)分配將有概率讓兩個相鄰區(qū)域具有同樣的shadow memory值,雖然概率比較小,但總歸是個缺陷。因此工具中會有其他邏輯保證這個前提。
下圖展示了HeapOverFlow的檢測過程。指針p的標(biāo)簽和訪問的地址p[32]所對應(yīng)的shadow memory值不一致,因此報錯(圖中只用了4bit記錄標(biāo)記值,但不影響理解,8bit標(biāo)記值的檢測和它一致)。
2.3 錯誤信息示例
Abort message: ‘==12528==ERROR: HWAddressSanitizer: tag-mismatch on address 0x003d557e2c20 at pc 0x00748b4a6918READ 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: 0Thread: 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區(qū)域的總長為6×16=96,與上述提示一致。
[0x003d557e2c20,0x003d557e2c80) is a small unallocated heap chunk; size: 96
2.4 優(yōu)缺點
和ASAN相比,HWASAN具有如下缺點:
可移植性較差,只適用于64位機(jī)器。
需要對Linux Kernel做一些改動以支持工具。
對于所有錯誤的檢測將有一定概率false negative(漏掉一些真實的錯誤),概率為1/256。原因是tag的生成只能從256(2^8)個數(shù)中選一個,因此不同地址的tag將有可能相同。
不過相對于這些缺點,HWASAN所擁有的優(yōu)點更加引人注目:
不再需要安全區(qū)來檢測buffer overflow,既極大地降低了工具對于內(nèi)存的消耗,也不會出現(xiàn)ASAN中某些overflow檢測不到的情況。
不再需要隔離區(qū)來檢測UseAfterFree,因此不會出現(xiàn)ASAN中某些UseAfterFree檢測不到的情況。
2.5 一個難題
上述的討論其實回避了一個問題:如果一個16字節(jié)的memory region中只有前幾個字節(jié)可尋址(假設(shè)是5),那么其對應(yīng)的shadow memory值也是5。這時,如果用地址去訪問該region的第2個字節(jié),那么如何判斷訪問是否合規(guī)呢?
此時直接對比地址的tag和shadow memory的值肯定是不行的,因為此時的shadow memory值含義發(fā)生了變化,它不再是一個類似于tag的隨機(jī)值,而是memory region中可訪問字節(jié)的數(shù)目。
為了解決這個難題,HWASAN在這種情況下將memory region的隨機(jī)值保存在最后一個字節(jié)中。所以即便地址的tag和shadow memory的值不等,但只要和memory region中最后一個字節(jié)相等,也表明該訪問合法。
? ? ? ?責(zé)任編輯:pj
評論
查看更多