原文链接:https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm
运行时库替换了malloc和free函数。malloc区域周围的内存(红区)被标记为污染。free的内存被放置在隔离区并标记为污染。程序中的每个内存访问都会被编译器转换为以下方式:
转换前:
*address = ...; // or: ... = *address;
转换后:
if (IsPoisoned(address)) {
ReportError(address, kAccessSize, kIsWrite);
}
*address = ...; // or: ... = *address;
关键是如何快速实现IsPoisoned和非常紧凑的ReportError。此外,对某些访问进行仪器化可能被证明是冗余的。
虚拟地址空间被分为两个不相交的类别:
应用程序内存(Mem):该内存由常规应用程序代码使用。
影子内存(Shadow):该内存包含影子值(或元数据)。影子内存与主应用程序内存之间存在对应关系。在主内存中毒一个字节意味着向相应的影子内存中写入某些特殊值。
这两个内存类别应该以一种能够快速计算影子内存(MemToShadow)的方式进行组织。
编译器执行的仪器化:
shadow_address = MemToShadow(address);
if (ShadowIsPoisoned(shadow_address)) {
ReportError(address, kAccessSize, kIsWrite);
}
AddressSanitizer将应用程序内存的8个字节映射为影子内存的1个字节。
对于任何对齐的8个字节的应用程序内存,只有9种不同的值:
这是由malloc返回8字节对齐的内存块所保证的。唯一的情况是对齐的qword的不同字节具有不同的状态,是malloc分配的区域的尾部。例如,如果我们调用malloc(13),我们将有一个完整的未污染的qword和一个有5个未污染字节的qword。
仪器化如下所示:
byte *shadow_address = MemToShadow(address);
byte shadow_value = *shadow_address;
if (shadow_value) {
if (SlowPathCheck(shadow_value, address, kAccessSize)) {
ReportError(address, kAccessSize, kIsWrite);
}
}
//检查我们访问qword的前k个字节的情况,并且这些k个字节未被污染。
bool SlowPathCheck(shadow_value, address, kAccessSize) {
last_accessed_byte = (address & 7) + kAccessSize - 1;
return (last_accessed_byte >= shadow_value);
}
MemToShadow(ShadowAddr)落入了不可寻址的ShadowGap区域。因此,如果程序尝试直接访问影子区域中的内存位置,程序将会崩溃。
Shadow = (Mem >> 3) + 0x7fff8000;
[0x10007fff8000, 0x7fffffffffff] | HighMem |
---|---|
[0x02008fff7000, 0x10007fff7fff] | HighShadow |
[0x00008fff7000, 0x02008fff6fff] | ShadowGap |
[0x00007fff8000, 0x00008fff6fff] | LowShadow |
[0x000000000000, 0x00007fff7fff] | LowMem |
Shadow = (Mem >> 3) + 0x20000000;
[0x40000000, 0xffffffff] | HighMem |
---|---|
[0x28000000, 0x3fffffff] | HighShadow |
[0x24000000, 0x27ffffff] | ShadowGap |
[0x20000000, 0x23ffffff] | LowShadow |
[0x00000000, 0x1fffffff] | LowMem |
可以使用更紧凑的影子内存,例如:
Shadow = (Mem >> 7) | kOffset;
实验正在进行中。
ReportError可以作为调用实现(这是默认值),但是还有一些稍微更有效率和/或更紧凑的解决方案。在某个时候默认行为是:
也可以只使用一个指令(例如ud2),但这将需要在运行时库中具有完整的反汇编器(或其他一些hack)。
为了捕获堆栈缓冲区溢出,AddressSanitizer对代码进行如下仪器化:
原始代码:
void foo() {
char a[8];
...
return;
}
仪器化代码:
void foo() {
char redzone1[32]; // 32-byte aligned
char a[8]; // 32-byte aligned
char redzone2[24];
char redzone3[32]; // 32-byte aligned
int *shadow_base = MemToShadow(redzone1);
shadow_base[0] = 0xffffffff; // poison redzone1
shadow_base[1] = 0xffffff00; // poison redzone2, unpoison 'a'
shadow_base[2] = 0xffffffff; // poison redzone3
...
shadow_base[0] = shadow_base[1] = shadow_base[2] = 0; // unpoison all
return;
}
# long load8(long *a) { return *a; }
0000000000000030 <load8>:
30: 48 89 f8 mov %rdi,%rax
33: 48 c1 e8 03 shr $0x3,%rax
37: 80 b8 00 80 ff 7f 00 cmpb $0x0,0x7fff8000(%rax)
3e: 75 04 jne 44 <load8+0x14>
40: 48 8b 07 mov (%rdi),%rax <<<<<< original load
43: c3 retq
44: 52 push %rdx
45: e8 00 00 00 00 callq __asan_report_load8
# int load4(int *a) { return *a; }
0000000000000000 <load4>:
0: 48 89 f8 mov %rdi,%rax
3: 48 89 fa mov %rdi,%rdx
6: 48 c1 e8 03 shr $0x3,%rax
a: 83 e2 07 and $0x7,%edx
d: 0f b6 80 00 80 ff 7f movzbl 0x7fff8000(%rax),%eax
14: 83 c2 03 add $0x3,%edx
17: 38 c2 cmp %al,%dl
19: 7d 03 jge 1e <load4+0x1e>
1b: 8b 07 mov (%rdi),%eax <<<<<< original load
1d: c3 retq
1e: 84 c0 test %al,%al
20: 74 f9 je 1b <load4+0x1b>
22: 50 push %rax
23: e8 00 00 00 00 callq __asan_report_load4
当前的紧凑映射不会捕获不对齐的部分越界访问:
int *x = new int[2]; // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);
*u = 1; // Access to range [6-9]
在https://github.com/google/sanitizers/issues/100中描述了一种可行的解决方案,但它会带来性能成本。
运行时库替换了malloc/free并提供了错误报告函数,如__asan_report_load8。
malloc分配所请求的内存量,并在其周围使用红区。相应于红区的阴影值被标记为污染,主内存区域的阴影值被清除。
free将整个区域的阴影值标记为污染,并将内存块放入隔离队列中(这样在某段时间内,malloc不会再次返回该块)。