前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >AddressSanitizer算法及源码解析

AddressSanitizer算法及源码解析

作者头像
Linux阅码场
发布2019-10-08 17:26:59
2.8K0
发布2019-10-08 17:26:59
举报
文章被收录于专栏:LINUX阅码场LINUX阅码场

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 (https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm),在此对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)设置为不可写即可。具体的示意图如下图所示。

内存映射

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)。具体的映射策略如下图所示。

图1: 虚拟地址映射图

插桩

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

未插桩的代码:

插桩后的代码:

动态运行库

在动态运行库中将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()方法。

图2: RunOnModule

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

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

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

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

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

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)函数中主要的插桩过程。

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

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

Heap Variable

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

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

本文来源: http://blog.binpang.me/2017/07/26/AddressSanitizer/

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-07-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linux阅码场 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • AddressSanitizer简介
  • AddressSanitizer算法
    • 内存映射
      • 插桩
        • 动态运行库
        • AddressSanitizer源码分析
          • Global Variable
            • Stack Variable
              • Heap Variable
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档