内存检查之libasan.so工具

内存检查之libasan.so工具

Author:Onceday date:2023年5月11日

参考文档:

1. 概述

使用C/C++开发程序,对于大型复杂代码,容易出现内存异常问题,这个可能导致非常多的意外情况。

这里使用的lisasan.so工具可以检测如下的内存问题:

  • Heap OOB(HeapOutOfBounds 堆内存越界)
  • Stack OOB(StackOutOfBounds 栈越界)
  • Global OOB(GlobalOutOfBounds 全局变量越界)
  • UAF(UseAfterFree 内存释放后使用)
  • UAR(UseAfterReturn 栈内存回收后使用,该功能还存在少量bug,默认未开启,开启ASAN_OPTIONS = detect_stack_use_after_return=1)
  • UMR(uninitialized memory reads读取未初始化内存)
  • Leaks(内存泄露)

ASAN全称:Address Sanitizer,google发明的一种内存地址错误检查器。目前已经被集成到各大编译器中。

其中踩内存的现象千奇百怪,可能导致coredump、死循环、异常分支等等意外。要定位这种问题,又十分麻烦,因为已经错过了案发现场。一般处理手段如下:

  • 分析被踩内存的特征值,比如是否是一个magic值,然后从代码库中找特征值,分析代码,缩小排查方向。
  • 找到必现条件,通过gdb的watch功能,watch被踩的内存地址,一旦被踩,gdb将会打出踩内存的堆栈。

ASAN的优点是性能下降不多,官方数据是下降两倍,而valgrind下降数十倍

该工具由一个编译器插装模块(目前是一个LLVM通道)和一个替代malloc函数的运行时库组成。

该工具适用于x86、ARM、MIPS(所有架构的32和64位版本)、PowerPC64。支持的操作系统有Linux、Darwin (OS X和iOS Simulator)、FreeBSD、Android。

1.1 原理概述

详细内容请参考:

ASAN要记录每一块内存的可用性. 把用户程序所在的内存区域叫做主内存, 而记录主内存可用性的内存区域, 则叫做影子内存 (Shadow memory).

所有主内存的分配都按照 8 字节的方式对齐. 然后按照 1:8 的压缩比例对主内存的可用性进行记录, 然后存入影子内存中. 影子内存无法被用户直接读写, 需要编译器生成相关的代码来访问.

每一次内存的分配和释放, 都会写入影子内存. 每次读/写内存区域前, 都会读取一下影子内存, 获得这块内存访问合法性 (是否被分配, 是否已被释放).

对影子内存的写入只在分配内存的时候发生, 所以只要分配内存是多线程安全的, ASan 就是多线程安全的, 这在大部分情况下也确实成立.

计算影子内存的地址需要快速, 他们采用了: 主内存地址除以 8, 再加上一个偏移量的做法. 因为堆栈分别在虚拟内存地址空间的两端, 这样影子内存就会落在中间. 而如果用户以外访问了影子内存, 那么影子内存的"影子内存"就会落到一个非法的范围 (Shadow Gap) 内, 就可以知道访问出了些问题.

因此对于ASan化的二进制文件,ulimit -v命令没有什么意义,因为ASan消耗20tb的虚拟内存(再加上一点),为了避免触发系统OOM,需要调大内存阈值

2. 参数介绍

2.1 编译参数

有两种编译方式,都需要携带符号表,以供输出具体的调用栈。

xxx/指定动态链接库的目录,一般编译工具链都已经自带该库了,在libc里面

动态链接编译如下:

-fsanitize=address -lasan -g -L xxx/   

动态编译的可执行文件执行时,往往需要使用LD_PRELOAD预先加载动态库,避免其他先加载的库已初始化大量内存。

静态链接编译如下:

-fsanitize=address -static-libasan -g -L xxx/   

此外,还有如下的可选编译参数:

flag description
-fsanitize=address 开启ASAN检查,将会对代码里面的malloc和free函数进行替换
-fno-omit-frame-pointer 保留下帧指针。允许快速栈回放正常工作。
-fsanitize-blacklist=path 指定黑名单文件,指定的文件里面不进行ASAN检查
-fno-common 不要将C中的全局变量视为公共变量(允许ASan检测它们)

ASan-specific compile-time flags are passed via clang flag -mllvm <flag>. In most cases you don’t need them.

flag default description
-asan-stack 1 Detect overflow/underflow for stack objects
-asan-globals 1 Detect overflow/underflow for global objects
-asan-use-private-alias 0 Use private aliases for global objects
2.2 运行参数

参数可通过两种方式进行传递,一种是运行时使用ASAN_OPTIONS=verbosity=1:malloc_context_size=20进行传递,另外一种是将参数嵌入到代码里面:

const char *__asan_default_options() {
  return "verbosity=1:malloc_context_size=20";
}

可通过下面的方式来得到当前版本支持的命令参数:

ASAN_OPTIONS=help=1 ./a.out

常见和完整的参数可以参考官方文档:

下面列举一些简单的参数例子:

Flag Default value Description
quarantine_size_mb 256 (16 on iOS or Android) 用于检测释放后使用错误的隔离大小(以Mb为单位)。较低的值可能会减少内存使用,但会增加假阴性的机会。
report_globals 1 控制处理全局变量的方式(0 -不检测全局变量上的缓冲区溢出,1 -检测缓冲区溢出,2 -打印关于注册全局变量的数据)。
check_initialization_order false 如果设置,则尝试捕获初始化顺序问题。
replace_str true 如果设置,则使用自定义包装器和替换libc字符串函数来查找更多错误。
replace_intrin true 如果设置,则使用memset/memcpy/memmove物理特性的自定义包装器。
detect_stack_use_after_return false 在运行时启用返回后使用堆栈检查。
sleep_before_dying 0 从打印错误报告到终止程序之间的睡眠秒数。用于调试目的(例如,当需要附加gdb时)。
alloc_dealloc_mismatch true (false on Darwin and Windows) 报告malloc/delete, new/free, new/delete[]等错误。
new_delete_type_mismatch true 报告new和delete大小不匹配的错误。
strict_init_order false 如果为true,则假定动态初始化器永远不能访问其他模块中的全局变量,即使后者已经初始化。
strict_string_checks false 如果为true,检查字符串参数是否正确地以空结束。
detect_invalid_pointer_pairs 0 如果非零,尝试检测无效指针对上的<,<=,>,>=和-等操作(例如,当指针属于不同对象时)。值越大,我们越努力。
detect_odr_violation 2 如果>=2,检测违反单定义规则(ODR);如果==1,只有当两个变量的大小不同时才检测odr违规
dump_instruction_bytes false 如果为true,则从导致SEGV的指令开始转储16字节
halt_on_error true 在打印第一个错误报告后崩溃程序(警告:使用风险自负!)。该标志只有在使用-fsanitize-recover=address compile选项编译代码时才有效。
log_path stderr Write logs to log_path.pid. The special values are stdout and stderr
2.3 关闭对某些函数的检查

在某些情况下,AddressSanitizer应该忽略(而不是插装)特定的函数:

  • 忽略一个已知能够正确加速应用程序的热门函数。

  • 忽略执行一些低级魔术的函数(例如,绕过帧边界遍历线程的堆栈)。

  • 不要报告已知的问题。无论哪种情况,都要非常小心。

使用以下的宏定义方式即可:

#if defined(__clang__) || defined (__GNUC__)
# define ATTRIBUTE_NO_SANITIZE_ADDRESS __attribute__((no_sanitize_address))
#else
# define ATTRIBUTE_NO_SANITIZE_ADDRESS
#endif
...
ATTRIBUTE_NO_SANITIZE_ADDRESS
void ThisFunctionWillNotBeInstrumented() {...}
2.4 官方github问答

问:AddressSanitizer在报告第一个错误后能继续运行吗?

答:可以,AddressSanitizer最近有了continue-after-error 模式。这在某种程度上是实验性的,因此可能还没有默认设置那么可靠(也没有及时得到支持)。还要记住,第一个错误之后的错误实际上可能是虚假的。要启用continue-after-error,使用-fsanitize -recover=address编译,然后使用ASAN_OPTIONS=halt_on_error=0运行代码。

问:为什么在我的代码中没有报告明显无效的内存访问?

A1:如果您的错误太明显,那么在Asan运行时编译器可能已经对其进行了优化。

A2:另一个c选项是访问不受Asan保护的全局通用符号(你可以使用-fno-common来禁用通用符号的生成,希望能检测到更多的错误)。

A3:如果启用了_FORTIFY_SOURCE,可能会有误报,请参见下一个问题。

问:我已经用-D_FORTIFY_SOURCE标志和ASan编译了我的代码,或者-D_FORTIFY_SOURCE在我的发行版(大多数现代发行版)中默认启用。现在ASan行为不正常(要么产生错误警告,要么没有发现一些错误)。

答:目前,ASan(和其他杀毒软件)不支持源强化,请参阅https://github.com/google/sanitizers/issues/247。修复很可能在glibc端,请参阅此处的(停滞的)讨论。

问:当我链接我的共享库与-fsanitize=地址,它失败了,由于一些未定义的ASan符号(例如asan_init_v4)?

答:最可能的链接是-Wl,-z,defs或-Wl,–no-undefined。这些标志不适用于ASan,除非您也使用-shared-libasan(这是GCC的默认模式,但不适用Clang)。

问:我的malloc堆栈轨迹太短或没有意义?

答:尝试使用-fno-omit-frame-pointer编译代码或设置ASAN_OPTIONS=fast_unwind_on_malloc=0(后者将是性能杀手,但除非您还指定malloc_context_size=2或更低)。注意,基于帧指针的展开在Thumb上不起作用。

问:我的new()和delete()堆栈痕迹太短或没有意义?

答:当c++标准库静态链接时可能会发生这种情况。预构建的libstdc++/libc++通常不使用帧指针,并且它打破了快速(基于帧指针的)展开。要么使用-shared-libstdc++标志切换到共享库,要么使用ASAN_OPTIONS=fast_unwind_on_malloc=0。后者可能非常缓慢。

问:我正在使用动态ASan运行时,我的程序在开始时崩溃,“影子内存范围与现有内存映射交织”。不能正常进行。”

A1:如果您正在使用共享的ASan DSO,请尝试在您的程序中LD_PRELOAD ASan运行时。

A2:否则,您可能会遇到动态运行时的已知限制。Libasan在程序启动结束时初始化,所以如果之前的库初始化器分配了大量内存,那么影子内存所需的内存区域可能会被不相关的映射占用。

问:在堆栈跟踪中打印的PC始终偏离1?

答:这不是一个bug,而是一个设计选择。在CISC平台上,很难精确地计算出前面指令的大小。所以a只是减少1,这对于addr2line或者readelf这样的工具来说已经足够了。

问:我用ASAN_OPTIONS= verbose =1运行,ASan告诉我类似的东西

 ==30654== Parsed ASAN_OPTIONS: verbosity=1
 ==30654== AddressSanitizer: failed to intercept 'memcpy'

答:这个警告是假的(详见https://gcc.gnu.org/bugzilla/show_bug.cgi?id=58680)。

问:我已经用ASan构建了我的主要可执行文件。我还需要构建共享库吗?

A:即使你只重建程序的一部分,它也能工作。但是您必须重新构建所有组件以检测所有错误。

问:我和ASan建立了我的共享库。我可以用未链接编译ASAN的(unsanitized)可执行文件运行它吗?

答:是的!您需要使用动态版本的ASan构建库,然后使用LD_PRELOAD=path/to/ ASan /runtime/lib运行可执行文件。

问:在Linux系统上,我看到类似这样的东西在启动时崩溃

ERROR: AddressSanitizer failed to allocate 0x400000000 (17179869184) bytes at address 67fff8000 (errno: 12)

A:确保在/proc/sys/vm/overcommit_memory中没有2个

问:我正在研究一个项目,该项目使用裸机操作系统,没有pthread (TLS)支持,也没有POSIX系统调用,并希望使用ASan,但其代码依赖于我的平台上不可用的一些东西(例如dlsym)。是否支持裸金属目标?

答:我们没有对您的用例提供开箱即用的支持。对您来说,最简单的方法是去掉所有您没有的东西并重新构建运行时。然而,已经有许多将ASan应用于裸机的尝试,至少有一些是成功的。例如http://events.linuxfoundation.org/sites/events/files/slides/Alexander_Popov-KASan_in_a_Bare-Metal_Hypervisor_0.pdf,也可以在https://groups.google.com/forum/#中搜索“裸金属”和类似的东西!论坛/ address-sanitizer组。

问:我可以运行AddressSanitizer并启用更积极的诊断吗?

答:是的!你可能特别想启用

 CFLAGS += -fsanitize-address-use-after-scope
 ASAN_OPTIONS=strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1

问:我的库在调用free 时 SIGABRT时崩溃了。出了什么问题?

答:最有可能的是你正在打开带有RTLD_DEEPBIND标志的库。ASan不支持RTLD_DEEPBIND,详细信息请参见问题#611。

问:我得到以下错误时,构建我的代码:gcc:错误:不能指定-static与-fsanitize=address。

A: ASan不能与静态链接一起工作。为了在代码中使用ASan,您需要禁用静态链接。

问:0xbebebebebe和0xbebebebe是什么意思?

A:默认情况下,ASan将0xbe写入新分配的内存(参见malloc_fill_byte)。0xbebebebebebebe(可能)是一个64位的值,已分配但未初始化。0xbebebebe也一样。

问:我可以将Clang检测的代码与GCC检测的代码混合使用吗?或者我至少可以用Clang编译代码,并在运行时链接GCC,反之亦然?

答:不,你不能,Clang和GCC有完全不兼容的ASan实现,你不能以任何方式混合它们。

3. 实际例子

实际编译指令可如下:

# 基础的asan编译标志
CFLAGS='-g -fsanitize=address'
# 保留栈指针,允许快速栈回溯
CFLAGS="${CFLAGS} -fno-omit-frame-pointer"
# 保留堆栈信息
CFLAGS="${CFLAGS} -fno-optimize-sibling-calls"
# 不将C中全局变量视为公共变量,允许ASAN检测
CFLAGS="${CFLAGS} -fno-common"
# 允许发生错误时继续允许ASAN
CFLAGS="${CFLAGS} -fsanitize-recover=address"

# 动态链接ASAN库
#CFLAGS="${CFLAGS} -lasan"
# 静态链接ASAN库,推荐,更省事
CFLAGS="${CFLAGS} -static-libasan"

# 编译第一个参数名的C源文件
gcc $CFLAGS $1.c -o $1.out

运行参数可如下:

# 基础的asan运行参数标识
# log_path=/home/xos/asan.log:内存检查问题日志存放文件路径
export ASAN_OPTIONS=log_path=./asan_err.log

# halt_on_error=0:检测内存错误后继续运行
# detect_leaks=1:使能内存泄露检测
# malloc_context_size=15:内存错误发生时,显示的调用栈层数为15
# suppressions=$SUPP_FILE:屏蔽打印某些内存错误
export ASAN_OPTIONS="${ASAN_OPTIONS}:halt_on_error=0:detect_leaks=1:malloc_context_size=15"

# detect_stack_use_after_return 开启返回堆栈检查
# 设置quarantine_size_mb,隔离区大小
# export ASAN_OPTIONS="${ASAN_OPTIONS} detect_stack_use_after_return=true:quarantine_size_mb=10"

# 运行指定函数
./$1.out

3.1 栈溢出访问错误

#include <stdio.h>
#include <stdlib.h>

/* 测试栈数组越界访问 */
void test_array_over(void)
{
    
    
    int a[10];
    a[10] = 0;
}

/* 测试申请内存未释放 */
void test_malloc(void)
{
    
    
    int *a = malloc(10);
    free(a);
    a[10] = 0;
}

int main(void)
{
    
    
    printf("hello world\n");
    test_array_over();
    test_malloc();
    return 0;
}

使用上面的参数进行运行,输入结果如下:

=================================================================
==2145553==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fff6b8b06c8 at pc 0x55db0db56d39 bp 0x7fff6b8b0660 sp 0x7fff6b8b0650
WRITE of size 4 at 0x7fff6b8b06c8 thread T0
    #0 0x55db0db56d38 in test_array_over gcc-test/asan-test.c:15
    #1 0x55db0db56e44 in main gcc-test/asan-test.c:29
    #2 0x7fad310b2d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #3 0x7fad310b2e3f in __libc_start_main_impl ../csu/libc-start.c:392
    #4 0x55db0da813a4 in _start (/home/ubuntu/free-cc/gcc-test/asan-test.out+0x83a4)

Address 0x7fff6b8b06c8 is located in stack of thread T0 at offset 88 in frame
    #0 0x55db0db56c6d in test_array_over gcc-test/asan-test.c:13

  This frame has 1 object(s):
    [48, 88) 'a' (line 14) <== Memory access at offset 88 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 gcc-test/asan-test.c:15 in test_array_over
Shadow bytes around the buggy address:
  0x10006d70e080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10006d70e090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10006d70e0a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10006d70e0b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10006d70e0c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1
=>0x10006d70e0d0: f1 f1 f1 f1 00 00 00 00 00[f3]f3 f3 f3 f3 00 00
  0x10006d70e0e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10006d70e0f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10006d70e100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10006d70e110: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10006d70e120: 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
=================================================================
==2145553==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000038 at pc 0x55db0db56e1c bp 0x7fff6b8b0700 sp 0x7fff6b8b06f0
WRITE of size 4 at 0x602000000038 thread T0
    #0 0x55db0db56e1b in test_malloc gcc-test/asan-test.c:23
    #1 0x55db0db56e49 in main gcc-test/asan-test.c:30
    #2 0x7fad310b2d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #3 0x7fad310b2e3f in __libc_start_main_impl ../csu/libc-start.c:392
    #4 0x55db0da813a4 in _start (/home/ubuntu/free-cc/gcc-test/asan-test.out+0x83a4)

0x602000000038 is located 30 bytes to the right of 10-byte region [0x602000000010,0x60200000001a)
freed by thread T0 here:
    #0 0x55db0db10d87 in free (/home/ubuntu/free-cc/gcc-test/asan-test.out+0x97d87)
    #1 0x55db0db56ddd in test_malloc gcc-test/asan-test.c:22
    #2 0x55db0db56e49 in main gcc-test/asan-test.c:30
    #3 0x7fad310b2d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

previously allocated by thread T0 here:
    #0 0x55db0db110d7 in __interceptor_malloc (/home/ubuntu/free-cc/gcc-test/asan-test.out+0x980d7)
    #1 0x55db0db56dcd in test_malloc gcc-test/asan-test.c:21
    #2 0x55db0db56e49 in main gcc-test/asan-test.c:30
    #3 0x7fad310b2d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: AddressSanitizer: heap-buffer-overflow gcc-test/asan-test.c:23 in test_malloc
Shadow bytes around the buggy address:
  0x0c047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c047fff8000: fa fa fd fd fa fa fa[fa]fa fa fa fa fa fa fa fa
  0x0c047fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8050: 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

猜你喜欢

转载自blog.csdn.net/Once_day/article/details/130632755