AddressSanitizer算法及源码解析

AddressSanitizer简介

AddressSanitizer是Google用于检测内存各种buffer overflow(Heap buffer overflow, Stack buffer overflow, Global buffer overflow)的一个非常有用的工具。该工具是一个LLVM的Pass,现已集成至llvm中,要是用它可以通过-fsanitizer=address选项使用它。AddressSanitizer的源码位于/lib/Transforms/Instrumentation/AddressSanitizer.cpp中,Runtime-library的源码在llvm的另一个项目compiler-rt的/lib/asan文件夹中。

AddressSanitizer算法

具体的算法可以参考WIKI,在此对AddressSanitizer算法做一个简短的介绍。AddressSanitizer主要包括两部分:插桩(Instrumentation)和动态运行库(Run-time library)。插桩主要是针对在llvm编译器级别对访问内存的操作(store,load,alloca等),将它们进行处理。动态运行库主要提供一些运行时的复杂的功能(比如poison/unpoison shadow memory)以及将malloc,free等系统调用函数hook住。其实该算法的思路很简单,如果想防住Buffer Overflow漏洞,只需要在每块内存区域右端(或两端,能防overflow和underflow)加一块区域(RedZone),使RedZone的区域的影子内存(Shadow Memory)设置为不可写即可。具体的示意图如下图所示。

RedZone

内存映射

AddressSanitizer保护的主要原理是对程序中的虚拟内存提供粗粒度的影子内存(没8个字节的内存对应一个字节的影子内存),为了减少overhead,就采用了直接内存映射策略,所采用的具体策略如下:Shadow=(Mem >> 3) + offset。每8个字节的内存对应一个字节的影子内存,影子内存中每个字节存取一个数字k,如果k=0,则表示该影子内存对应的8个字节的内存都能访问,如果0<k<7,表示前k个字节可以访问,如果k为负数,不同的数字表示不同的错误(e.g. Stack buffer overflow, Heap buffer overflow)。具体的映射策略如下图所示。
mapping图片

图1: 虚拟地址映射图

typicalMapping

插桩

为了防止buffer overflow,需要将原来分配的内存两边分配额外的内存Redzone,并将这两边的内存加锁,设为不能访问状态,这样可以有效的防止buffer overflow(但不能杜绝buffer overflow)。一下是在栈中插桩的一个例子。

未插桩的代码:

1
2
3
4
5
void foo() {
char a[8];
...
return;
}

插桩后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
void foo() {
char redzone1[32]; // 32-byte aligned
char a[8]; // 32-byte aligned
char redzone2[24];
char redzone3[32]; // 32-byte aligned
int *shadow_base = MemToShadow(redzone1);
shadow_base[0] = 0xffffffff; // poison redzone1
shadow_base[1] = 0xffffff00; // poison redzone2, unpoison 'a'
shadow_base[2] = 0xffffffff; // poison redzone3
...
shadow_base[0] = shadow_base[1] = shadow_base[2] = 0; // unpoison all
return;
}

动态运行库

在动态运行库中将malloc/free函数进行了替换。在malloc函数中额外的分配了Redzone区域的内存,将与Redzone区域对应的影子内存加锁,主要的内存区域对应的影子内存不加锁。

free函数将所有分配的内存区域加锁,并放到了隔离区域的队列中(保证在一定的时间内不会再被malloc函数分配)。

AddressSanitizer源码分析

AddressSanitizer主要有三种层面的变量:Stack Variable(局部变量),Global Variable, Heap Variable。由于每种变量的生命周期(life time)不同,所以对不同种类的变量处理也是不同的。下面分别从Global Variable,Stack Variable,Heap Variable三个层次来分析AddressSanitizer源码的逻辑结构。

Global Variable

Global Variable存放在程序的数据段。在该算法的实现过程中,处理GlobalVariale的是AddressSanitizerModule类,该类继承自llvm的ModulePass,所以我们先看一下AddressSanitizerModule类的runOnModule(Module &M)方法的处理过程,该过程首先进行一些初始化,然后我们可以看到对Global的插桩方法InstrumentGlobals()方法。

runOnMoudle

图2: RunOnModule

在InstrumentGlobals()方法中,主要是分成两步:首先,重新声明一个GlobalVariable,这个GlobalVariable包含以前的GlobalVariable和一个RedZone;然后,调用runtime-library将新声明的这个GlobalVariable的RedZone区域加锁。我们先来看第一步的具体实现,如图3所示。

instrumentGlobal

图3: 生成包含RedZone的新的GlobalVariable

下面,我们首先看一下一个Struct结构,该结构记录GlobalVariable存储的首地址,数据的大小,Redzone的大小,Module的名字等信息,便于在Runtime-library中使用。该结构在AddressSanitizerModule和runtime-library中都有相应的定义:

poisonGlobal
poisonGlobalRuntime

然后我们可以看到对GlobalVariable进行插桩来实现RedZone的Poison和整个GlobalVariable的Poison操作。

instrumentGlobal_actural
register_unregisterGlobal

具体的Poison RedZone和Poison GlobalVariable的实现在Runtime-library中:
RegisterGlobal
UnRegisterGlobal

Stack Variable

Stack Variable保存在栈区,在栈中的数据我们需要控制好变量的声明周期(lifetime),当调用一个函数时,会开辟一个栈,栈中的数据会有相应的redzone和shadow memory,并将redzone的shadow memory Poison,当函数结束(正常返回,异常),栈被销毁,需要将数据和redzone清空,其相应的shadow memory也要UnPoison掉。

对于Stack Variable,AddressSanitizer算法中实现了AddressSanitizer类,该类是继承了llvm的FunctionPass,该Pass能够处理每一个函数,在处理每个函数的时候,处理每一个load,store等能够访问内存的指令,在这些指令执行前进行插桩,看其访问的内存是不是被poison。

下面我们主要看一下AddressSanitizer::runOnFunction(Module &M)函数中主要的插桩过程。

stackInstrument

在每次访问内存时,都会查看影子内存的值,看其是否是0,如果是0则表示都能访问具体的插桩在instrumentMop函数中,

instrumentMop

其中具体的处理过程在instrumentAddress函数中:

instrumentAddress

Heap Variable

Heap Variable保存在堆区,其分配的函数是malloc函数,该部分的主要代码在runtime-library中,该库中主要是先将malloc的库函数hook住,然后自己定义malloc函数,定义分配策略。

hookFunction

具体的分配策略定义在compiler-rt/lib/asan/asan-allocator.cc文件中,感兴趣可以看一下。