-- TOC --
打开-fsanitize=address
编译选项后,代码在编译时会被instrumentation,在发生内存读写地址错误的时候,会打印出一个report,给出很多可以协助定位bug的信息!(需安装libasan库)
AddressSanitinzer论文:https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/37752.pdf
比如下面这段代码:
#include <stdlib.h>
int main(){
char *p = malloc(18);
p[21] = '1'; // heap-buffer-overflow
free(p);
return 0;
}
编译和运行:
$ gcc -Wall -Wextra test2.c -o test2 # no error and warning
$ ./test2 # nothing happened
$ gcc -Wall -Wextra test2.c -fsanitize=address -o test2 # -fsanitize=address
$ ./test2
=================================================================
==280728==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x603000000055 at pc 0x0000004011b8 bp 0x7fff9aab5dc0 sp 0x7fff9aab5db8
WRITE of size 1 at 0x603000000055 thread T0
#0 0x4011b7 in main (/home/xinlin/test2/test2+0x4011b7)
#1 0x7f706a740eaf in __libc_start_call_main (/lib64/libc.so.6+0x3feaf)
#2 0x7f706a740f5f in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x3ff5f)
#3 0x4010a4 in _start (/home/xinlin/test2/test2+0x4010a4)
0x603000000055 is located 3 bytes to the right of 18-byte region [0x603000000040,0x603000000052)
allocated by thread T0 here:
#0 0x7f706a9b891f in __interceptor_malloc (/lib64/libasan.so.6+0xae91f)
#1 0x401177 in main (/home/xinlin/test2/test2+0x401177)
#2 0x7f706a740eaf in __libc_start_call_main (/lib64/libc.so.6+0x3feaf)
SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/xinlin/test2/test2+0x4011b7) in main
Shadow bytes around the buggy address:
0x0c067fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c067fff8000: fa fa 00 00 00 fa fa fa 00 00[02]fa fa fa fa fa
0x0c067fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8050: 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
Shadow gap: cc
==280728==ABORTING
编译时加上-fsanitize=address
,运行时就能跑出这个刻意为之的heap-buffer-overflow错误。
如何阅读这个错误report?
首先是错误类型,heap-buffer-overflow,并且给出了出错的地址,PC指针地址和调用栈,对出错地址的操作是read还是write,以及size,这些信息足够我们定位触发错误的代码,以及做一些分析判断。
=================================================================
==280728==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x603000000055 at pc 0x0000004011b8 bp 0x7fff9aab5dc0 sp 0x7fff9aab5db8
WRITE of size 1 at 0x603000000055 thread T0
#0 0x4011b7 in main (/home/xinlin/test2/test2+0x4011b7)
#1 0x7f706a740eaf in __libc_start_call_main (/lib64/libc.so.6+0x3feaf)
#2 0x7f706a740f5f in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x3ff5f)
#3 0x4010a4 in _start (/home/xinlin/test2/test2+0x4010a4)
紧接着,report给出了这块内存申请时的size,用左闭右开的区间表示,然后计算了出错的地址与这个区间的相对距离,还有申请这块内存的调用栈。(可以看到malloc接口来自libasan库)
0x603000000055 is located 3 bytes to the right of 18-byte region [0x603000000040,0x603000000052)
allocated by thread T0 here:
#0 0x7f706a9b891f in __interceptor_malloc (/lib64/libasan.so.6+0xae91f)
#1 0x401177 in main (/home/xinlin/test2/test2+0x401177)
#2 0x7f706a740eaf in __libc_start_call_main (/lib64/libc.so.6+0x3feaf)
ASAN的report最后这段summary,给出了shadow-memory的信息。下面这部分信息,包含shadow-memory的内容,以及如何解读这部分内容的说明。
SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/xinlin/test2/test2+0x4011b7) in main
Shadow bytes around the buggy address:
0x0c067fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c067fff8000: fa fa 00 00 00 fa fa fa 00 00[02]fa fa fa fa fa
0x0c067fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8050: 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
Shadow gap: cc
==280728==ABORTING
默认情况下,ASAN的shadow-memory使用1个字节来表达8个字节的状态,其地址对应关系如下:
shadow-memory = (address>>3) + 0x7FFF8000
对地址(正数)左移3位,相当于/8
,然后加上个固定的偏移,就是shadow-memory的区域,这里的每个字节,对应了程序的8个字节。因为,malloc返回的起始地址,一定是8字节对齐的!
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
00表示可以正常读写,01-07表示可部分读写。比如本例malloc(18),最后的字节在shadow-memory 中,就是02,表示只有前2个字节可读写,即:
=>0x0c067fff8000: fa fa 00 00 00 fa fa fa 00 00[02]fa fa fa fa fa
以上这些信息,都对定位内存bug很有帮助。建议测试时,打开-fsanitize=address
编译选项。
看看汇编:
$ gcc -S -masm=intel -fsanitize=address test2.c
main:
.LASANPC6:
.LFB6:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
sub rsp, 16
mov edi, 18
call malloc
mov QWORD PTR [rbp-8], rax
mov rax, QWORD PTR [rbp-8]
lea rcx, [rax+21]
mov rax, rcx
mov rdx, rax
shr rdx, 3
add rdx, 2147450880
movzx edx, BYTE PTR [rdx]
test dl, dl
setne sil
mov rdi, rax
and edi, 7
cmp dil, dl
setge dl
and edx, esi
test dl, dl
je .L2
mov rdi, rax
call __asan_report_store1
.L2:
mov BYTE PTR [rcx], 49
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call free
mov eax, 0
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
代码已被修改,整个过程是自动的。
测试代码:
int main(){
char a[10] = {};
a[10] = 1;
return 0;
}
编译:
$ gcc -fsanitize=address test3.c -o test3
运行:
=================================================================
==283010==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffa3c72c8a at pc 0x000000401286 bp 0x7fffa3c72c50 sp 0x7fffa3c72c48
WRITE of size 1 at 0x7fffa3c72c8a thread T0
#0 0x401285 in main (/home/xinlin/test2/a.out+0x401285)
#1 0x7f3b30f21eaf in __libc_start_call_main (/lib64/libc.so.6+0x3feaf)
#2 0x7f3b30f21f5f in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x3ff5f)
#3 0x4010a4 in _start (/home/xinlin/test2/a.out+0x4010a4)
Address 0x7fffa3c72c8a is located in stack of thread T0 at offset 42 in frame
#0 0x401175 in main (/home/xinlin/test2/a.out+0x401175)
This frame has 1 object(s):
[32, 42) 'a' (line 2) <== Memory access at offset 42 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow (/home/xinlin/test2/a.out+0x401285) in main
Shadow bytes around the buggy address:
0x100074786540: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100074786550: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100074786560: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100074786570: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100074786580: 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1
=>0x100074786590: 00[02]f3 f3 00 00 00 00 00 00 00 00 00 00 00 00
0x1000747865a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1000747865b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1000747865c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1000747865d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1000747865e0: 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
Shadow gap: cc
==283010==ABORTING
形式一样,内容稍有不同。
从前面的case中可以理解,overflow是高地址越界。可以很容易的测试,underflow就是低地址越界,ASAN也支持underflow的检测。从编程的角度看,overflow更容易发生,要触发underflow,array要使用负的index,一般很难出现用负index的情况。(这不是Python)
测试代码:
int main(){
char a[10] = {};
a[-1] = 1;
return 0;
}
后面只给测试代码,不再贴出ASAN的report!
#include <stdlib.h>
int main(){
char *p = malloc(32);
free(p);
p[0] = 2;
return 0;
}
#include <stdlib.h>
int main(){
char *p = malloc(32);
free(p);
free(p);
return 0;
}
C++版double free:
int main(){
char *p = new char[24];
delete[] p;
delete[] p;
return 0;
}
int main(){
char *p;
{
char a[10] = {};
p = a;
}
p[0] = 2;
return 0;
}
#include <stdlib.h>
char ga[8];
int main(){
ga[8] = 1;
return 0;
}
#include <string>
#include <cstring>
using namespace std;
int main() {
string *p = new string[4];
for(size_t i{}; i<4; ++i)
p[i] = "hello";
// array cookie's type is size_t,
// clean array cookie!
memset((char*)p-8, 0, 8);
delete[] p;
return 0;
}
在 cxxabi array-cookies 要求中,对于 non-trivial 类型的 operator new[] 实际需要在内存前端存储实际分配的元素个数 (使用 std::size_t 类型来记录)。如果不小心内存越界将 array-cookies 给写没了,那么在 delete[] non-trivial 类型时它们的 dtor 就没法正确被调用,因而无法正确析构。
#include <stdlib.h>
char ga[8];
void __attribute__((no_sanitize("address"))) func(){
ga[8] = 1;
}
int main(){
func();
return 0;
}
给需要关闭ASAN功能的函数增加属性:__attribute__((no_sanitize("address")))
。
本文链接:https://cs.pynote.net/sf/c/cdm/202310261/
-- EOF --
-- MORE --