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; // rz1shadow[1] = 0xffff0200; // arr and rz2shadow[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