asan原理

通过asan检测出栈溢出的例子

对应代码

// overflow.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, const char *argv[]) {
    char *s = (char*)malloc(12);
    strcpy(s, "Hello world!");
    printf("string is: %s\n", s);
    free(s);
    return 0;
}

asan分析

一旦 ASAN 命中,进程会立即退出。日志中应出现:
AddressSanitizer: heap-buffer-overflow

同时包括四段关键信息:
WRITE of size ...
写入栈:Connection::RemoveData / UfileAc::Put
分配栈:posix_memalign / UfileAc::Put
越界位置:位于分配区右侧 N bytes
shadow bytes / redzone 信息

~/Code/test$ gcc overflow.c -o overflow -fsanitize=address -g
~/Code/test$ ./overflow 
=================================================================
==2172878==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000001c at pc 0x7f1cd3d3d58d bp 0x7ffee78e6500 sp 0x7ffee78e5ca8     //1)
WRITE of size 13 at 0x60200000001c thread T0        // 2)
    #0 0x7f1cd3d3d58c in __interceptor_memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:790
    #1 0x555593131261 in main /home/chenbing/Code/test/overflow.c:7
    #2 0x7f1cd3ad4082 in __libc_start_main ../csu/libc-start.c:308
    #3 0x55559313116d in _start (/home/chenbing/Code/test/overflow+0x116d)

0x60200000001c is located 0 bytes to the right of 12-byte region [0x602000000010,0x60200000001c)    // 3)
allocated by thread T0 here:
    #0 0x7f1cd3daf808 in __interceptor_malloc ../../../../src/libsanitizer/ASAN/ASAN_malloc_linux.cc:144
    #1 0x555593131245 in main /home/chenbing/Code/test/overflow.c:6
    #2 0x7f1cd3ad4082 in __libc_start_main ../csu/libc-start.c:308

SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:790 in __interceptor_memcpy
Shadow bytes around the buggy address:      // 4)
  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 00[04]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
==2172878==ABORTING

1)告诉我们错误的原因是:heap-buffer-overflow,堆区内存溢出了,该内存的地址是:0x60200000001c。
2)描述了写入数据导致溢出的位置堆栈,
3)则是对应的内存分配位置堆栈,
4)还是shadow内存快照。其中fa表示堆区内存的red zone,fd表示已经释放的堆区内存。

ASAN 的基本原理

ASAN的内存检测方法与Valgrind的AddrCheck工具很像,都是使用shadow内存来记录应用程序的每个字节是否可以被安全的访问,在访问内存时都对其映射的shadow内存进行检查。

但是,ASAN使用一个更具效率的shadow内存映射机制和更加紧凑的内存编码来实现,并且除了堆内存外还能检测栈和全局对象中的错误访问,且比AddrCheck快一个数量级。

ASAN由两部分组成:代码插桩模块和运行时库。

  • 代码插桩模块会修改代码使其在访问内存时检查每块内存访问状态,称为shadow 状态,以及在内存两侧创建redzone的内存区域。
  • 运行时库则提供一组接口用来替代malloc和free以及相关的函数,使得在分配堆空间时在其周围创建redzone,并在内存出错时报告错误。
    首先,我们先介绍一下什么是shadow 内存 和 redzone。

shadow 内存

在ASAN中malloc函数返回的内存地址通常至少是8个字节对齐,比如malloc(15)将分配得到2块大小为8字节的内存,在这个场景中,第二块8字节内存的前5个字节是可以访问,但剩下的3个字节是不能访问的。
所谓的shadow 内存就是在应用程序的虚拟地址空间中预留一段地址空间,用来存储映射应用程序访问的内存块中哪些字节可以被使用的信息,这些信息就是shadow 状态。其中每1个字节的shadow 内存,映射到8个字节的应用程序内存,因此,shadow状态可能有3种:

0: 表示映射的8个字节均可以使用
k(1<=k<=7): 表示表示映射的8个字节中只有前k个字节可以使用
负值: 表示映射的8个字节均不可使用,且不同的值表示所映射不同的内存类型(堆、栈、全局对象或已释放内存)

ASAN使用带有比例和偏移量的直接映射将应用程序地址转换为其对应的shadow内存地址:
shadow_address = (addr >> 3) + offset
假设max - 1是虚拟地址空间中的最大有效地址,则offset的值应选择为在启动时不被占用的从offset到offset+Max/8的区域。

在 32 位 linux 系统中,虚拟地址空间为:0x00000000-0xffffffff,offset = 0x20000000(2^29)。
在 64 位系统中,ofsset = 0x0000100000000000(2^44)。
在某些情况下(例如,在 Linux 上使用 -fPIE/-pie 编译器标志)可以使用零偏移来进一步简化检测。
以下是 32 位 linux 系统中的地址空间分布
0x1 0000 0000 --------------- | HIGH | | MEMORY | 0x4000 0000 --------------- | HIGH SHADOW | 0x2800 0000 --------------- | BAD REGION | 0x2400 0000 --------------- | LOW SHADOW | 0x2000 0000 --------------- | LOW MEMORY | 0x0000 0000 ---------------
虚拟地址空间被划分为高低两部分,每个部分的内存地址映射到相应的shadow 内存。注意:将shadow 内存中的地址进行映射会得到Bad 区域中的地址,Bad 区域是被页面保护标记为不可访问的地址空间。
shadow映射方式可以推导为(addr >> scale) + offset的形式,其中scale是的取值范围是1~7,当 scale=N时,shadow 内存占用虚拟地址空间的1/2^N, red-zone的最小大小为2^N字节(保证malloc()的对齐要求)。shadow 内存中的每个字节描述了2N个内存字节的状态并有2N + 1个不同的值。

redzone

ASAN会在应用程序使用的堆、栈、全局对象的内存周围分配额外内存,这个额外的内存叫做redzone,redzone会被shadow 内存标记为不可使用状态,当应用程序访问redzone内存时说明已经溢出访问了,此时,ASAN检测redzone的shadow 状态后就会报告相应错误。readzone越大,检测内存下溢和上溢的范围越大。具体的分配策略将在下面涉及。

代码插桩

ASAN 会在应用程序访问内存的位置进行插桩,对于访问完整8字节内存的位置,插入以下代码检查内存对应的 shadow 内存,以此判断是否访问异常:

ShadowAddr = (Addr >> 3) + Offset;

if (*ShadowAddr != 0)
  ReportAndCrash(Addr);

由于应用程序访问8字节的内存,因此,其映射的shadow 内存的存储值必须是0,表示该8字节内存完全可用,否则,报错。

应用程序对 1、2、或者 4 字节内存的访问要复杂一些,如果访问的内存块对应的shadow 内存的存储值如果不是负数,且不为0,或者将要访问内存块超过了shadow 内存表示的可用范围,意味着本次将访问到不可使用的内存:

ShadowAddr = (Addr >> 3) + Offset;
k = *ShadowAddr;
if (k != 0 && ((Addr & 7) + AccessSize > k))
  ReportAndCrash(Addr);

需要注意的是,ASAN对源代码的插桩时机是在LLVM对代码编译优化之后,也就意味着ASAN只能检测 LLVM 优化后幸存下来的内存访问,例如:被 LLVM 优化掉的对栈对象进行访问的代码将不会被ASAN所识别。

同时,ASAN也不会对 LLVM 生成的内存访问代码进行插桩,例如:寄存器溢出检查等等。

另外,即使错误报告代码ReportAndCrash(Addr)只会被调用一次,但由于会在代码中的许多位置进行插入,因此,错误报告代码也必须相当紧凑。

目前 ASAN 使用了一个简单的函数调用来处理错误报告,当然还有另一个选择是插入一个硬件异常。

运行时库

在应用程序启动时,将映射整个shadow 内存,因此程序的其他部分不能使用它。BAD 区域也是受保护的,应用程序也不能访问。

在 linux 操作系统中,shadow 内存区域不会被占用,因此,映射总是成功的。但在 MacOS 中可能需要禁用地址空间布局(ASLR)。

另外,根据 GOOGLE 工程师介绍,shadow 内存区域的布局也适用于 windows 操作系统。

启用 ASAN 时,源代码中的 malloc 和 free 函数将会被替换为运行时库中的 malloc 和 free 函数。

malloc 分配的内存区域被组织为为一个与对象大小相对应的空闲列表数组。当对应于所请求内存大小的空闲列表为空时,从操作系统(例如,使用mmap)分配带有redzone的内存区域。n个内存块,将分配n+1个redzone:

| redzone-1 | memory-1 | redzone-2 | memory-2 | redzone-3 |

free 函数会将整个内存区域置成不可使用并将其放入隔离区,这样该区域就不会马上被 malloc 分配给应用程序。

目前,隔离区是使用一个 FIFO 队列实现的,它在任何时候都拥有一定数量的内存。

默认情况下,malloc 和 free 记录当前调用堆栈,以便提供更多信息的错误报告。 malloc 调用堆栈存储在左侧 redzone 中(redzone 越大,可以存储的帧数越多),而 free 调用堆栈存储在内存区域本身的开头。

到这里你应该已经明白了对于动态分配的内存,ASAN是怎么实现检测的,但你可能会产生疑惑:动态分配是通过 malloc 函数分配redzone来支持错误检测,那栈对象和全局对象这类没有malloc分类内存的对象是怎么实现的呢?其实原理也很简单:

对于全局变量,redzone 在编译时创建,redzone 的地址在应用程序启动时传递给运行时库。 运行时库函数会将redzone 设置为不可使用并记录地址以供进一步错误报告。
对于栈对象,redzone 是在运行时创建和置为不可使用。 目前,使用32字节的 redzone。例如以下代码片段:void foo() { char a[10]; }
经 ASAN 处理后的代码大致如下:void foo() { char rz1[32] char arr[10]; char rz2[32-10+32]; unsigned * shadow = (unsigned*)(((long)rz1>>8)+Offset); // 将 redzone 设置为不可使用 shadow[0] = 0xffffffff; // rz1 shadow[1] = 0xffff0200; // arr and rz2 shadow[2] = 0xffffffff; // rz2 // 将所有内存设置成可以使用 shadow[0] = shadow[1] = shadow[2] = 0; }

总结

ASAN 使用shadow 内存和redzone来提供准确和即时的错误检测。

传统观点认为,shadow 内存和redzone要么通过多级映射方案产生高开销,要么占用大量的程序内存。但,ASAN的使用的shadow映射机制和shadow 状态编码减少了对内存空间占用。

最后,如果你觉得ASAN插桩代码和检测的对你某些的代码来说太慢了,那么可以使用编译器标志来禁用特定函数的,使ASAN跳过对代码中某个函数的插桩和检测, 跳过分析函数的编译器指令是:

__attribute__((no_sanitize_address))
posted @ 2025-08-11 07:39  LdreamerD  阅读(88)  评论(0)    收藏  举报