因为虚拟机的存在,Android应用开发者们一般不用考虑内存访问相关的错误。而一旦咱们深刻到Native世界中,本来面容和蔼的内存便开始凶恶起来。这时,因为程序员写法不规范、逻辑疏漏而致使的内存错误会通通跳到咱们面前,对咱们嘲讽一番。html
这些错误既影响了程序的稳定性,也影响了程序的安全性,由于好多恶意代码就经过内存错误来完成入侵。不过麻烦的是,Native世界中的内存错误很难排查,由于不少时候致使问题的地方和发生问题的地方相隔甚远。为了更好地解决这些问题,各路大神纷纷祭出本身手中的神器,相互PK,相互补充。linux
ASAN(Address Sanitizer)和HWASAN(Hardware-assisted Address Sanitizer)就是这些工具中的佼佼者。android
在ASAN出来以前,市面上的内存调试工具要么慢,要么只能检测部份内存错误,要么这两个缺点都有。总之,不够优秀。c++
HWASAN则是ASAN的升级版,它利用了64位机器上忽略高位地址的特性,将这些被忽略的高位地址从新利用起来,从而大大下降了工具对于CPU和内存带来的额外负载。git
ASAN工具包含两大块:程序员
插桩模块主要会作两件事:github
运行时库也一样会作两件事:算法
若是想要了解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中状态:
为何0个字节可寻址的状况shadow memory不为0,而是负数呢?是由于0个字节可寻址其实能够继续分为多种状况,譬如:
对全部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
复制代码
ShadowAddr = (Addr >> 3) + Offset;
k = *ShadowAddr;
if (k != 0 && ((Addr & 7) + AccessSize > k))
ReportAndCrash(Addr);
复制代码
在每次内存访问时,都会执行如上的伪代码,以判断这次内存访问是否合规。
首先根据normal memory的地址找到对应shadow memory的地址,而后取出其中存取的byte值:k。
当这次内存访问可能会访问到不可寻址的字节时,ASAN会报错并结合shadow memory中具体的值明确错误类型。
想要检测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时的堆栈信息。所以咱们能够清楚地知道该区域是如何被释放的,从而快速定位问题,解决问题。
想要检测HeapBufferOverflow的问题,只须要保证一点:
测试代码:
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。
自从2011年诞生以来,ASAN已经成功地参与了众多大型项目,譬如Chrome和Android。虽然它的表现很突出,但仍然有些地方不尽如人意,重点表如今如下几点:
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的状况。
当一个堆内存被分配出来时,返回给用户空间的地址便已经带上了标签(存储于地址的高8位)。以后经过该地址进行内存访问,将先检测地址中的标签值和访问地址对应的shadow memory的值是否相等。若是相等则验证经过,能够进行正常的内存访问。
当该内存被free时,HWASAN会为该块区域分配一个新的随机值,存储于其对应的shadow memory中。若是此后再有新的访问,则地址中的标签值必然不等于shadow memory中存储的新的随机值,所以会有错误产生。经过以下图示能够很好地明白这一点(图中只用了4bit记录标记值,但不影响理解,8bit标记值的检测和它一致)。
想要检测HeapOverFlow,有一个前提须要知足:相邻的memory区域须要有不一样的shadow memory值,不然将没法分辨两个不一样的memory区域。为每一个memory区域随机分配将有几率让两个相邻区域具备一样的shadow memory值,虽然几率比较小,但总归是个缺陷。所以工具中会有其余逻辑保证这个前提。
下图展现了HeapOverFlow的检测过程。指针p的标签和访问的地址p[32]所对应的shadow memory值不一致,所以报错(图中只用了4bit记录标记值,但不影响理解,8bit标记值的检测和它一致)。
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
复制代码
和ASAN相比,HWASAN具备以下缺点:
不过相对于这些缺点,HWASAN所拥有的优势更加引人注目:
上述的讨论其实回避了一个问题:若是一个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…
参考文章: