Address Sanitizer

Dydong / 2024-07-08 / 原文

Address Sanitizer

Introduction

Address Sanitizer是一款内存检测器,它可以检测在堆栈,全局变量等地方的溢出。后来被整合到了GCC等编译器中,Address Sanitizer由两部分组成:一个Instrumentation模块和一个运行时库。Instrumentation模块修改代码来检查每个内存访问的影子状态,并在堆栈和全局对象周围创建有毒的红色区域,以检测溢出和下溢。运行时库替换malloc、free和相关函数,在分配的堆区域周围创建有毒的红区,延迟释放堆区域的重用,并进行错误报告。Address Sanitizer使用影子内存来记录应用程序内存的每个字节是否可以安全访问,并使用仪器检查每个应用程序负载或存储上的影子内存。


Shadow Memory

​ malloc函数返回以8字节为单位,一般前k个字节是可以寻址的,后8-k个字节不可以,那么就有9种状态。规则具体为:0表示所有都可以寻址,K(1≤K≤7)表示前K字节是可寻址的,负值表示整个都不可以寻址,不同的负值来区分不同类型的不可寻址内存(堆红区、堆栈红区、全局红区、释放内存)。那么就可以用1个字节来存储,这种8:1的放缩形式导致影子内存的空间减少。

​ 地址转化时,使用偏移量来进行计算,可以理解为映射规则为(Addr>>Scale)+Offset。offset的设置也有相应的要求,选取的 Offset 应该满足如下约束:影子内存的地址段, 也就是 Offset 到 (Offset+Max)/8 的地址段,不能被应用程序用到。

​ 如下图所示,因为每一个内存memory都会进行一次shadow memory的映射,所以把应用程序的内存给映射为shadow,把shadow部分给映射为bad区域,bad区域就是一个不可访问的区域。

​ 映射规则可以细分为直接映射和间接映射,直接映射的代表性的例子是 TaintTrace 和 LIFT。TaintTrace 按照 1:1 映射。缺点就是无法处理内存需求特别大的被检测程序 ,如果被检测程序使用了一半以上的地址空间,那就没有足够的地址空间来容纳影子内存了。相比来说,LIFT 使用 8:1 的比例设置影子内存。间接映射的代表是 valgrind 和 Dr.Memory。他们设置多个影子内存段,然后配合查表法来完成映射。


Instrumentation

​ 当检测一个8字节的内存访问时,Address Sanitizer计算相应影子字节的地址,加载该字节,并检查它是否为零:

ShadowAddr = (Addr >> 3) + Offset;	// 取地址中的字节
if (*ShadowAddr != 0)	ReportAndCrash(Addr);	// 如果不为0那么就报告

​ 当检测1、2或4字节访问时,检测稍微复杂一些:如果阴影值为正(即,8字节字中只有前k个字节是可寻址的),我们需要将地址的最后3位与k进行比较:

ShadowAddr = (Addr >> 3) + Offset;	// 取地址中的字节
k = *ShadowAddr;
if (k != 0 && ((Addr & 7) + AccessSize > k))	ReportAndCrash(Addr);

ReportAndCrash的实现方式为简单的函数调用或者硬件异常指令。

​ 本技术将Address Sanitizer Instrumentation通道放置在LLVM优化管道的最末端。只记录那些在LLVM优化器执行的所有标量和循环优化中幸存下来的内存访问。例如,对由LLVM优化掉的本地堆栈对象的内存访问将不会被检测。同时,我们不需要测量由LLVM代码生成器生成的内存访问(例如,寄存器溢出)。


Run-time Library

​ 允许库中malloc和free函数也被专门函数取代,将malloc申请的堆块附近加入红区,可以认为是无法寻址访问被poisoned的区域。如果想防住Buffer Overflow漏洞,只需要在每块内存区域右端(或两端,能防overflow和underflow)加一块区域(RedZone),使RedZone的区域的影子内存(Shadow Memory)设置为不可写即可。

​ 对于全局变量,红区是在编译时创建的,红区地址在应用程序启动时传递给运行时库。运行时库函数毒害红区并记录地址,以便进一步报告错误。对于堆栈对象,红区是在运行时创建和毒害的。举个例子:

void foo() {
	char a[10];
	<function body>
}

​ 在函数调用的时候会进行创建红区:

void foo() {
	char rz1[32];
	char arr[10];
	char rz2[32-10+32];
	unsigned *shadow =(unsigned*)(((long)rz1>>8)+Offset);
    // poison the redzones around arr.
    shadow[0] = 0xffffffff; // rz1
    shadow[1] = 0xffff0200; // arr and rz2
    shadow[2] = 0xffffffff; // rz2
    <function body>
    // un-poison all.
    shadow[0] = shadow[1] = shadow[2] = 0;
}

Example

如下图所示,我们拿Ubuntu22.04作为例子来演示Heap上的溢出,在编译的时候加入选项g++ -fsanitize=address -g main.cpp

#include <iostream>
#include <cstring>
char* memoryOverflowExample()
{
    char* ret = new char;
    strcpy(ret, "Hello World\r\n");
    return ret;
}
int main()
{
    char* ret = memoryOverflowExample();
    std::cout << ret << std::endl;
    delete ret;
}

当运行的时候,首先他会现实调用的函数栈,然后会显示shadow memory上的状态,中括号为申请空间的映射,其中的定义为上述定义所示,然后旁边被红区包围,因为它是堆上的申请,所以旁边显示为Heap left redzone。

它所能检测出来的错误:


Reference article

AddressSanitizer: A Fast Address Sanity Checker | USENIX

https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm