1. 简介
Windows wdk提供了verifier校验器,用于排查内存泄漏等不好发现和定位的问题。
Linux上的校验器就比较多了,如:
- Dr.Memory, 检测未初始化的内存访问、double free、use after free 等错误;
- Mudflap, 检测指针的解引用,静态插桩;
- Insure, 检测内存泄漏;
- Valgrind, 慢;
- ASAN,本文主角。
如果只是检测 memcpy/memset/strcpy 危险函数,可以使用 _FORTIFY_SOURCE机制。
Google的ASAN(AddressSanitizer)应该是最给力的,结合编译器插桩和运行时快速内存检测,包括缓冲区溢出、空指针引用、野指针、Double Free等。
ASAN 不支持检测【使用未初始化的内存】,MSAN(MemorySanitizer)可以。Google其实是提供了一整套校验程序。
项目地址:https://github.com/google/sanitizers
工具原理wiki介绍得很详细。本问主要记录一下ASAN的用法。
ASAN原理
AddressSanitizer is a part of LLVM starting with version 3.1 and a part of GCC starting with version 4.8。
ASAN由两部分组成:
- 编译时插桩模块;
- 运行时库,替换一些内存操作函数,比如用
__asan_malloc 替换malloc。
检测原理是在变量的左右内存区域下毒,这些区域又叫雷区/redzone:
void *__asan_malloc(size_t sz) {
void *rz = malloc(RED_SZ);
Poison(rz, RED_SZ);
void *addr = malloc(sz);
UnPoison(addr, sz);
rz = malloc(RED_SZ);
Poison(rz, RED_SZ);
return addr;
}
内存空间会被划分为以下两个类别:
- Main application memory ,应用程序代码所使用的内存,后面简称应用内存。
- Shadow memory, 应用内存的元数据,每8字节应用内存对应1字节影子内存。
影子内存的这 1 字节应用内存的状态:
- 0,表示整个 8 字节内存都正常;
- 0n表示有n个字节被下毒,表示前 n 个字节正常、后 8-n 个字节被投毒了;
- 负数,如fa,表示整个 8 字节都被投毒了,不可访问。
0xfa 表示堆左边的 redzone、0xf1 表示栈左边的 redzone, 还有0xf2, 0xf3等含义,可以在报错信息下面的Shadow byte legend说明部分查看。
报错时,引起报错的memory byte会用方括号括起来。
gdb里也可以看malloc堆空间的信息:
(gdb) p __asan_describe_address
2. Demo
UAF
这是ASAN wiki的第一个例子
源码:
#include <stdlib.h>
int main() {
char *x = (char*)malloc(10 * sizeof(char*));
printf("malloc:0x%p\n", array);
free(x);
return x[5];
}
编译一下,需要有-fsanitize=address -O1 -fno-omit-frame-pointer 这几个编译参数,带上-g则可以定位到源码行数。
可以看到依赖库中有libasan.so。
$ gcc -fsanitize=address -O1 -fno-omit-frame-pointer -g use-after-free.c -o uaf
$ ldd uaf
linux-vdso.so.1 (0x00007ffe8658f000)
libasan.so.4 => /usr/lib/x86_64-linux-gnu/libasan.so.4 (0x00007fb511c27000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb511836000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fb511632000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007fb51142a000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fb51120b000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fb510e6d000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fb510c55000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb512de4000)
运行一下,报错信息很详细,一般就是关注爆粗原因、出错的代码行数(需要-g参数编译):
$ ./uaf
malloc:0x0x607000000090
=================================================================
==22264==ERROR: AddressSanitizer: heap-use-after-free on address 0x607000000095 at pc 0x55f9433bfa71 bp 0x7ffe9554f380 sp 0x7ffe9554f370
READ of size 1 at 0x607000000095 thread T0
0x607000000095 is located 5 bytes inside of 80-byte region [0x607000000090,0x6070000000e0)
freed by thread T0 here:
previously allocated by thread T0 here:
SUMMARY: AddressSanitizer: heap-use-after-free /home/starr/Documents/CProject/use-after-free.c:7 in main
Shadow bytes around the buggy address:
0x0c0e7fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c0e7fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c0e7fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c0e7fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c0e7fff8000: fa fa fa fa 00 00 00 00 00 00 00 00 00 fa fa fa
=>0x0c0e7fff8010: fa fa[fd]fd fd fd fd fd fd fd fd fd fa fa fa fa
0x0c0e7fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0e7fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0e7fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0e7fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0e7fff8060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
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
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
==22264==ABORTING
[fa] 正好是我们申请的、用0xfd填充的内存偏移为5的字节。
HeapOutOfBounds
https://github.com/google/sanitizers/wiki/AddressSanitizerExampleHeapOutOfBounds
源码:
#include <stdio.h>
int main(int argc, char **argv) {
int *array = new int[100];
printf("new:0x%p\n", array);
array[0] = 0;
int res = array[argc + 100];
delete [] array;
return res;
}
编译运行:
$ g++ -fsanitize=address -O1 -fno-omit-frame-pointer -g asan.cpp -o heapOutBound
$ ./heapOutBound
new:0x0x614000000040
=================================================================
==29504==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6140000001d4 at pc 0x5589e84b8afe bp 0x7fffed2e8950 sp 0x7fffed2e8940
READ of size 4 at 0x6140000001d4 thread T0
0x6140000001d4 is located 4 bytes to the right of 400-byte region [0x614000000040,0x6140000001d0)
allocated by thread T0 here:
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/starr/Documents/CProject/asan.cpp:7 in main
Shadow bytes around the buggy address:
0x0c287fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c287fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c287fff8000: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
0x0c287fff8010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c287fff8020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c287fff8030: 00 00 00 00 00 00 00 00 00 00[fa]fa fa fa fa fa
0x0c287fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c287fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c287fff8060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c287fff8070: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c287fff8080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
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
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
==29504==ABORTING
StackOutOfBounds
AddressSanitizerExampleStackOutOfBounds · google/sanitizers Wiki (github.com)
源码:
#include <stdio.h>
int main(int argc, char **argv) {
int stack_array[100];
stack_array[1] = 0;
printf("new:0x%p\n", stack_array);
return stack_array[argc + 100];
}
编译运行:
$ g++ -fsanitize=address -O1 -fno-omit-frame-pointer -g asan.cpp -o stackOutBound
$ ./stackOutBound
new:0x0x7ffc3b934e80
=================================================================
==31438==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc3b935014 at pc 0x55fd7c7f0caf bp 0x7ffc3b934e40 sp 0x7ffc3b934e30
READ of size 4 at 0x7ffc3b935014 thread T0
Address 0x7ffc3b935014 is located in stack of thread T0 at offset 436 in frame
This frame has 1 object(s):
[32, 432) 'stack_array' <== Memory access at offset 436 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /home/starr/Documents/CProject/asan.cpp:8 in main
Shadow bytes around the buggy address:
0x10000771e9b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10000771e9c0: 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1
0x10000771e9d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10000771e9e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10000771e9f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x10000771ea00: 00 00[f2]f2 00 00 00 00 00 00 00 00 00 00 00 00
0x10000771ea10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10000771ea20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10000771ea30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10000771ea40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10000771ea50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
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
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
==31438==ABORTING
最后一个0xf1 距离方括号字节8*(16*3+2) == 400 字节,正好是stack_array的大小。
useAfterReturn
https://github.com/google/sanitizers/wiki/AddressSanitizerExampleUseAfterReturn
这种错有时可以检查出来。
源码:
int *ptr;
__attribute__((noinline))
void FunctionThatEscapesLocalObject() {
int local[100];
ptr = &local[0];
}
int main(int argc, char **argv) {
FunctionThatEscapesLocalObject();
return ptr[argc];
}
编译运行:
$ g++ -fsanitize=address -O1 -fno-omit-frame-pointer -g asan.cpp -o useAfterRet
$ ASAN_OPTIONS=detect_stack_use_after_return=1 ./useAfterRet
\=================================================================
==584==ERROR: AddressSanitizer: stack-use-after-return on address 0x7f17e0700024 at pc 0x560a7f92ac12 bp 0x7ffde4dbabc0 sp 0x7ffde4dbabb0
READ of size 4 at 0x7f17e0700024 thread T0
Address 0x7f17e0700024 is located in stack of thread T0 at offset 36 in frame
This frame has 1 object(s):
[32, 432) 'local' <== Memory access at offset 36 is inside this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-use-after-return /home/starr/Documents/CProject/asan.cpp:17 in main
Shadow bytes around the buggy address:
0x0fe37c0d7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0fe37c0d7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0fe37c0d7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0fe37c0d7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0fe37c0d7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0fe37c0d8000: f5 f5 f5 f5[f5]f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5
0x0fe37c0d8010: f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5
0x0fe37c0d8020: f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5
0x0fe37c0d8030: f5 f5 f5 f5 f5 f5 f5 f5 00 00 00 00 00 00 00 00
0x0fe37c0d8040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0fe37c0d8050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
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
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
==584==ABORTING
useAfterScope
先复习一下volatile关键字:
volatile int i=10;
int j = i;
volatile 告诉编译器i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,所以看生成的汇编代码的话,j的赋值是从i的地址读取数据的。
测试源码:
volatile int *p = 0;
int main() {
{
int x = 0;
p = &x;
}
*p = 5;
return 0;
}
编译运行:
$ g++ -fsanitize=address -O1 -fno-omit-frame-pointer -fsanitize-address-use-after-scope -g asan.cpp -o useAfterScope
$ ASAN_OPTIONS=detect_stack_use_after_scope=0 ./useAfterScope
=================================================================
==1596==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7ffdbdcf4b20 at pc 0x5581327a9b99 bp 0x7ffdbdcf4af0 sp 0x7ffdbdcf4ae0
WRITE of size 4 at 0x7ffdbdcf4b20 thread T0
Address 0x7ffdbdcf4b20 is located in stack of thread T0 at offset 32 in frame
This frame has 1 object(s):
[32, 36) 'x' <== Memory access at offset 32 is inside this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-use-after-scope /home/starr/Documents/CProject/asan.cpp:15 in main
Shadow bytes around the buggy address:
0x100037b96910: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100037b96920: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100037b96930: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100037b96940: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100037b96950: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x100037b96960: f1 f1 f1 f1[f8]f2 f2 f2 00 00 00 00 00 00 00 00
0x100037b96970: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100037b96980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100037b96990: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100037b969a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100037b969b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
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
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
==1596==ABORTING
c++静态初始化顺序问题
源码:
// a.cc
#include <stdio.h>
extern int extern_global;
int __attribute__((noinline)) read_extern_global() {
return extern_global;
}
int x = read_extern_global() + 1;
int main() {
printf("%d\n", x);
return 0;
}
// b.cc
int foo() { return 42; }
int extern_global = foo();
编译顺序问题导致的错误:
$ clang++ tmp/init-order/example/a.cc tmp/init-order/example/b.cc && ./a.out
1
$ clang++ tmp/init-order/example/b.cc tmp/init-order/example/a.cc && ./a.out
43
用ASAN检查:
$ clang++ -fsanitize=address -g tmp/init-order/example/a.cc tmp/init-order/example/b.cc
$ ASAN_OPTIONS=check_initialization_order=true ./a.out
=================================================================
==26772==ERROR: AddressSanitizer: initialization-order-fiasco on address 0x000001068820 at pc 0x427e74 bp 0x7ffff8295010 sp 0x7ffff8295008
READ of size 4 at 0x000001068820 thread T0
...
内存泄露
AddressSanitizerLeakSanitizer · google/sanitizers Wiki (github.com)
这本来是LeakSanitizer的功能,ASAN也继承了这个功能,不过在x86_64 Linux和OS X上才支持。
源码:
#include <stdlib.h>
#include <stdio.h>
void *p;
int main() {
p = malloc(7);
printf("malloc:0x%p\n", p);
p = 0;
return 0;
}
编译参数有所改变,不能-O优化.
$ g++ -fsanitize=address -fno-omit-frame-pointer -g asan.cpp -o memLeak
$ ASAN_OPTIONS=detect_leaks=1 ./memLeak
malloc:0x0x602000000010
=================================================================
==3405==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 7 byte(s) in 1 object(s) allocated from:
SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s).
也可以用LeakSanitizer 试试,wiki给出了一个设置已知泄露并忽略的例子。
suppr.txt:
leak:FooBar
源码:
#include <stdlib.h>
void FooBar() {
malloc(7);
}
void Baz() {
malloc(5);
}
int main() {
FooBar();
Baz();
return 0;
}
编译后可以看到,依赖的库从libasan.so变成了liblsan.so:
$ g++ -fsanitize=leak -fno-omit-frame-pointer -g asan.cpp -o memLeak
$ ldd memLeak
linux-vdso.so.1 (0x00007ffca0f6c000)
liblsan.so.0 => /usr/lib/x86_64-linux-gnu/liblsan.so.0 (0x00007f0c97480000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0c9708f000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0c96e8b000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f0c96c83000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f0c96a64000)
/lib64/ld-linux-x86-64.so.2 (0x00007f0c9852a000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f0c9684c000)
运行:
$ ASAN_OPTIONS=detect_leaks=1 LSAN_OPTIONS=suppressions=suppr.txt ./memLeak
=================================================================
==4583==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 5 byte(s) in 1 object(s) allocated from:
-----------------------------------------------------
Suppressions used:
count bytes template
1 7 FooBar
-----------------------------------------------------
SUMMARY: LeakSanitizer: 5 byte(s) leaked in 1 allocation(s).
可以看到FooBar函数的泄露被忽略了。
其它用法用到了再说:
flag | default | description |
---|
exitcode | 23 | If non-zero, LSan will call _exit(exitcode) upon detecting leaks. This can be different from the exit code used to signal ASan errors. | max_leaks | 0 | If non-zero, report only this many top leaks. | suppressions | (none) | Path to file containing suppression rules (see below) | print_suppressions | 1 | If 1, print statistics for matched suppressions. | report_objects | 0 | If 1, LSan will report the addresses of individual leaked objects. | use_unaligned | 0 | If 0, LSan will only consider properly aligned 8-byte patterns when looking for pointers. Set to 1 to include unaligned patterns. This refers to the pointer itself, not the memory being pointed at. |
3. 其它参考文章
https://zhuanlan.zhihu.com/p/382994002
https://zhuanlan.zhihu.com/p/390555316
|