一、asan介绍
Google ASan工具ASAN,全称 AddressSanitizer,也即地址消毒技术。可以用来检测内存问题,例如缓冲区溢出或对悬空指针的非法访问等。
ASan主要是进行编译器级别的HOOK与插桩,目前主流Clang,GCC,MSVC都支持,再结合运行时对影子内存的诊断输出,相当于双管齐下,整体效果不错;官方说是2倍左右性能开销,1/8的内存到2倍的开销。
- 内存操作进行插桩: 对new,malloc,delete,free,memcpy,其它内存访问等操作进行编译时替换与代码插入,是编译器完成的;
- 内存映射与诊断:按照一定的算法对原始内存进行一分影子内存的拷贝生成,目前不是1:1的拷贝,而是巧妙的按1/8大小进行处理,并进行一定的下毒与标记,减少内存的浪费。正常访问内存前,先对影子内存进行检查访问,如果发现数据不对,就进行诊断报错处理。
ASan的诊断功能:
释放后访问(野指针)
栈内存溢出
堆内存溢出
全局对象溢出访问
函数返回后访问
超出作用区访问
初始化顺序问题
内存泄露
ASan的弊端:
对内存溢出检查:依赖正常内存左右两端设定毒药区域大小;比如128字节,虽然这个值可以调,但越界超出这个值后,依然无法检查的到。
释放后访问检查:目前是对该内存进行隔离,并对影子内存标记为0xFD,但这个隔离不可能永久;一但被重新复用后,也可能造成严重内存问题,有类像内存池复用崩溃问题;
性能问题:由于插桩引入了很多汇编指令(Andorid平台还会有动态库),性能与内存上对比其它产品虽然还可以,但也只能在内部环境或Debug环境部署,无法直接应用到线上;
二、asan原理
- 1、运行时库:libasan.so.x(libasan.so.x)会接管malloc和free等内存操作函数。malloc执行完后,已分配内存的前后(称为“红区”)会被标记为“中毒”状态,而释放的内存则会被隔离起来(暂时不会分配出去)且也会被标记为“中毒”状态。
- 2、编译器插桩模块:
加了ASAN相关的编译选项后,代码中的每一次内存访问操作都会被编译器修改为如下方式:
编译前:
*address = ...; // or ... = *address;
编译后:
if (IsPoisoned(address)) { // 判断内存是否中毒
ReportError(address, kAccessSize, kIsWrite);
}
*address = ...; // or: ... = *address;
该方式的关键点就在于读写内存前会判断地址是否处于“中毒”状态,还有如何把IsPoisoned
实现的非常快,把ReportError
实现的非常紧凑,从而避免插入的代码过多。
ASan对缓冲区溢出防护的的基本步骤如下:
- 通过在被保护的栈、全局变量、堆周围建立标记为
中毒状态(Poisnoned)的red-zones
;red-zones区会写一个特殊值,该值称为“影子值”。比如fa\fd等 - 将缓冲区和red-zone通过每8字节对应1字节的映射的方式建立影子内存区,影子内存区的获取函数为MemToShadow。
- 如果出现对red-zone的读、写或执行的访问,则ASan可以ShadowIsPoisoned检测出来并报错。
关于影子值,针对任何8字节对齐的主应用区内存,总共有9种不同的影子内存值:
全部8字节都未“中毒”(可访问的),影子值是00。
全部8字节都“中毒”(不可访问的),影子值是负数。
前k个字节未“中毒”,后8-k字节“中毒”,影子值是k。这一功能的达成是由malloc函数总是返回8字节对齐的内存块来保证的,唯一能出现该情况的场景就在申请内存区域的尾部。例如,我们申请13个字节,即malloc(13),这样我们会得到一个完整的未“中毒”的00和前5个字节未“中毒”、后3个字节“中毒”的03。即00 03
三、常用的 ASAN 部署
1. 编译选项(Gcc编译选项)
常用的选项
ASAN_CFLAGS += -fno-stack-protector -fno-omit-frame-pointer -fno-var-tracking -g1
-fsanitize=address
: 开启内存越界检测
-fsanitize-recover=address/all
: 一般后台程序为保证稳定性,不能遇到错误就简单退出,而是继续运行,采用该选项支持内存错之后程序继续运行,需要叠加设置ASAN_OPTIONS=halt_on_error=0才会生效;若未设置此选项,则内存出错即报错退出
ASAN_CFLAGS += -fsanitize=address -fsanitize-recover=address
-fno-stack-protector:去使能栈溢出保护
- 以堆栈溢出为代表的缓冲区溢出 已成为最为普遍的安全漏洞,由此引发的安全问题比比皆是。我们知道攻击者利用堆栈溢出漏洞时,通常会破坏当前的函数栈。在gcc中,通过编译选项可以添加 函数栈的保护机制,通过重新对局部变量进行布局来实现,达到监测函数栈是否非破坏的目的。
- gcc中有3个与堆栈保护相关的编译选项(默认不开启这些编译选项的话,gcc 也会对局部变量重新布局)
-fstack-protector:启用堆栈保护,不过只为局部变量中含有char数组的函数插入保护代码。编译器会对局部变量的组织方式进行重新布局,数组往高地址存放,局部变量往低地址存放
-fstack-protector-all:启用堆栈保护,为所有函数插入保护代码。编译器会对局部变量的组织方式进行重新布局,数组往高地址存放,局部变量往低地址存放
-fno-stack-protector :禁用堆栈保护。编译器不会对局部变量的组织方式进行重新布局
-fno-omit-frame-pointer
:表示将堆栈帧指针存储在寄存器中,帧指针是用来指示当前函数的栈帧(stack frame)的指针,在调试时可以帮助跟踪函数调用的堆栈信息。在 AArch32 架构中,堆栈帧指针存储在寄存器 R11(A32代码)或寄存器 R7(T32代码)中;在AArch64架构中,堆栈帧指针存储在寄存器 X29 中。作为帧指针使用的寄存器不能用作通用寄存器,但如果使用 -fomit-frame-pointer 选项编译,则可用作通用寄存器。
-fno-var-tracking:默认选项为-fvar-tracking,会导致运行非常慢,是GCC编译器的一个选项,它用于控制是否启用变量跟踪分配的切换(待补充)
-g1:表示最小调试信息,通常debug版本用-g即-g2
2. Ld链接选项
ASAN_LDFLAGS += -fsanitize=address -g1
如果使用gcc链接,此处可忽略。
3. ASAN运行选项
export ASAN_OPTIONS=halt_on_error=0:use_sigaltstack=0:symbolize=1:detect_leaks=1:malloc_context_size=15:log_path=./asan.log:suppressions=$SUPP_FILE
3.1 ASAN_OPTIONS设置
ASAN_OPTIONS是Address-Sanitizier的运行选项环境变量。
-
halt_on_error=0:检测内存错误后继续运行
-
detect_leaks=1:使能内存泄露检测
-
malloc_context_size=15:内存错误发生时,显示的调用栈层数为15
-
log_path=/home/xos/asan.log:内存检查问题日志存放文件路径
-
suppressions=$SUPP_FILE:屏蔽打印某些内存错误
export ASAN_OPTIONS=halt_on_error=0:use_sigaltstack=0:detect_leaks=1:malloc_context_size=15:log_path=./asan.log:suppressions=$SUPP_FILE
3.2 其他选项
ASAN_OPTIONS=${ASAN_OPTIONS}:verbosity=0:handle_segv=1:allow_user_segv_handler=1:detect_stack_use_after_return=1:fast_unwind_on_fatal=1:fast_unwind_on_check=1:fast_unwind_on_malloc=1:quarantine_size=4194304
-
detect_stack_use_after_return=1:检查访问指向已被释放的栈空间
-
handle_segv=1:处理段错误;也可以添加handle_sigill=1处理SIGILL信号
-
quarantine_size=4194304:内存cache可缓存free内存大小4M
3.3 LSAN_OPTIONS设置
export LSAN_OPTIONS=exitcode=0:use_unaligned=4
- LSAN_OPTIONS是LeakSanitizier运行选项的环境变量,而LeakSanitizier是ASAN的内存泄漏检测模块,常用运行选项有:
- exitcode=0:设置内存泄露退出码为0,默认情况内存泄露退出码0x16
- use_unaligned=4:4字节对齐
四、常见的 ASAN 识别技巧
1. 程序结束堆内存未释放 leaked
-
用长度去判断
SUMMARY: AddressSanitizer: 100 byte(s) leaked in 1 allocation(s). -
用文件地址去判断
#1 0x4011ac in main /home/ofcx/ju/main.c:15 -
用 asan log 形式判断
堆内存未释放一般不会指出具体位置,log 不会有具体的位置非法信息,整个log 以SUMMARY 那行收尾
2. 悬空指针非法访问 heap-use-after-free
-
四要素:访问 悬空指针的位置和大小 、内存被释放位置、内存的分配 位置的堆栈信息以及线程信息
-
提供了错误访问的内存地址对应的shadow 内存的详细,其中 fa表示堆区内存的red zone,fd表示已经释放的堆区内存
-
判断方法:
-
影子区看
状态
=>0x0c167fff8000: fa fa fa fa fa fa fa fa[fd]
fd fd fd fd fd fd fd -
fd 表示已经释放的堆区内存
-
SUMMARY log 看
返错
SUMMARY: AddressSanitizer:heap-use-after-free
-
内存区域看
大小
0x60b000000040 is located 0 bytes inside of100-byte
region
[0x60b000000040,0x60b0000000a4)
-
3. 检测堆溢出 heap-buffer-overflow
- 怎么判断(SUMMARY, fa):
- SUMMARY log 返错 heap-buffer-overflow
- 快照 fa 堆红灯区
- 需要哪些信息
-
申请堆栈
allocated by thread T0 here:
#0 0x7f623521abb8 in __interceptor_malloc (/lib64/libasan.so.5+0xefbb8)
#1 0x4011bc in main (/home/ofcx/ju/main+0x4011bc)
#2 0x7f6234b69492 in __libc_start_main (/lib64/libc.so.6+0x23492 -
堆大小
0x60200000001c is located 0 bytes to the right of 12-byte
region[0x602000000010,0x60200000001c) -
快照
-
4. 检测栈溢出 stack-buffer-overflow
-
如何判断 从 summary 可以直接读出(SUMMARY, f3)
- SUMMARY : AddressSanitizer: stack-buffer-overflow
- 快照也可以反映,通过 f1、f3。f3 踩上界
-
要素
栈大小、踩内存地址
如 [32, 432) ‘stack_array’ <== Memory access at offset 436 overflows
this variable
5. 检测全局缓冲区溢出
- 如何判断 出(SUMMARY, f9)
-
SUMMARY 总结
SUMMARY: AddressSanitizer: global-buffer-overflow -
快照 f9
-
参考:
内存检测工具——ASan(AddressSanitizer)的介绍和使用 - 知乎
(zhihu.com)