Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

Address Sanitizer in macOS

$
0
0
Address Sanitizer in macOS

2016-08-19 12:12:46
来源:安全客 作者:安全客

阅读:292次
点赞(0)
收藏



分享到:








Address Sanitizer in macOS

简介

asan原理

asan实现

参考

简介

前几天, Keen Team的@marcograss在其博客上发布了一个使用Address Sanitizer(aka asan) 找到的堆溢出漏洞。在这里讨论一下asan的具体实现。

本文涉及的环境

macOS 10.11.6

Xcode 7.3.1 (确保使用苹果官方的clang, 其与开源clang生成的IR有些不同)

clang-703.0.31

什么是asan

asan是一个基于LLVM、由Google开发的快速查找内存错误的探测器。 在编译的时候插入相关的辅助和检测代码, 以此完成探测的工作。 后面将更加详细地介绍asan的结构。 asan很早就合并到了LLVM开源分支中, 并在Xcode7.0中正式合并到苹果的编译器中, 作为开发者调试c++代码并及时发现相应的内存错误的手段之一。

目前可以检测源代码中的几乎全部的栈内存和堆内存错误。

asan的使用

先写一个简单的含有栈溢出的程序:

// test.c
#include <stdio.h>
void f(char c)
{
printf("%c", c);
}
int main(void)
{
char a[32];
char b[32];
char c[50];
a[1]='1';
b[32]='a'; //stack overflow
c[100]='c'; //stack overflow
return 0;
}

使用如下的命令编译这个程序:

clang test.c -o test -fsanitize=address

运行程序, 将得到如下的结果:


Address Sanitizer in macOS

在asan给出的报告中, 可以得到如下的信息: 1. 当前错误是一个栈溢出的问题。 2. 当前栈上有三个对象, 在访问第二个对象的时候, 发生了栈溢出。


Address Sanitizer in macOS

3. asan在正常的栈对象周围放置了一部分的检测对象, 用来作为检测手段。

asan原理

在前一个小节中, 讨论过asan是一个基于LLVM的开源组件。 在这里, 将会详细地讨论asan的原理。

asan检测方式

C语言在做内存访问的时候, 没有任何安全可言。 这就导致了程序员在编写代码的时候, 会遇到很多奇奇怪怪的问题,同时也为软件安全带来了很多的挑战。

内存布局

通过上述的介绍, asan检测非法地址访问的方式是通过“影子内存”作为判断某个虚拟内存地址是否是“中毒”的。asan在内存分配的时候, 大致的内存分布如下:


Address Sanitizer in macOS

可以看到,一个进程的内存被划分为三种类型: (1) normal mmeory:待分配或者已经分配的内存; (2) shadow memory:内存索引区域; (3) bad memory(memory gap)。

使用先前编译出来的test再加上一些逆向工程,可以得到在真实环境下三种内存真实的大小:


Address Sanitizer in macOS
影子内存(Shadow Memory)

asan提供了一种有效的内存安全验证方式:使用辅助的内存追踪表来判断当前内存是否是合法有效的。这种内存追踪表被称作影子内存(shadow memory)。

这种机制有着显而易见的好处: 可以知道每个地址的状态。 每次访问地址前, 只要检测一下shadow memory, 就可以知道当前地址是否是合法的。 同样, 如果发生了溢出或者非法的访问, asan也会在第一时间知道发生溢出的地址。

非法内存在asan中被称作“中毒”;而合法内存则被称为“无毒”。asan中将8个字节表示为1个字节的索引值。如果索引值是0,代表8个字节是“无毒”的; 如果索引值是1,代表最后一个地址是“中毒”的(无法访问);如果8个字节都是“中毒”的, 那么这个索引值是一个负数。

访问内存操作的改变

在不同的C程序中, 访问内存一般是这样的:

*address=xxx

或者是

xxx=*address

在编译的时候, asan更改了访问代码, 将如上的操作变成了这样:

if (IsPoisoned(address))
{
ReportError(address, kAccessSize, kIsWrite);
}
*address = xxx; // or: xxx = *address;

经过如上的修改, 每次程序访问指针之前, 都将检查地址是否中毒, 如果中毒将抛出异常。

检测栈内存

asan相比其他的检测工具而言,栈内存检查是其拥有的一个很明显的优势。 这和asan工作的位置有关。为了更好的理解asan对栈的检测原理, 在这里需要介绍一下LLVM的工作原理。

LLVM工作原理

LLVM是Chris Lattner在2000年发起的新一代编译器项目。历经十几年的发展, LLVM已经成为成熟度很高的编译器项目。 苹果在其macOS平台引入了LLVM, 并作为其主力的编译器。

LLVM的工作流程如下所示:


Address Sanitizer in macOS

LLVM在结构上可以分为编译器前端和编译器后端。 前后端使用LLVM IR作为识别的语言。 前端主要将各种不同的语言(C/C++/Swift)转化成LLVM IR中间语言;后端使用一系列的Pass组件对LLVM IR进行一系列优化,最后将其转成对应架构的汇编语言(x86, x86/64, arm, arm64, etc)。

LLVM Pass组件

在LLVM的前端将LLVM IR代码提交给后端的时候, 后端将调用事先注册的Pass组件, 对获得的IR流进行一系列优化。不过这里可以这样说, 开发人员可以注册一系列pass组件, 在pass组件中插入一些辅助的代码到IR中, 这样可以改变原来代码的一些行为。asan中的栈检测正是基于这种方式实现的。

asan检测栈内存的方式

假设有如下的代码:

void foo() {
char a[8];
...
return;
}

如果开发者在编译程序的时候启用了asan, 那么asan将会把以上的代码更改为:

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;
}

asan在申请的栈数据周围用“下毒”的警戒区包围住。 当发生越界访问的时候, 该错误将会被asan侦测到。

检测堆内存

堆内存的检测将在另外一篇文章中描述。

asan实现


asan可以在源代码级别同时检测栈内存和堆内存,但是两种检测手段差别很大。栈内存的检查是基于LLVM Pass, 在编译阶段插入了检测代码; 而堆内存则是使用了动态替换malloc、free这些函数达到了检测的目的。因此, asan的实现分为两部分:栈实现和堆实现。本篇文章只讨论栈内存的检测手段。

asan的栈检测实现

因为asan在检查栈的时候, 使用了插入的检测代码手段, 具体来说是在程序的LLVM IR中插入了代码。 为了简化分析, 可以直接查看相应的LLVM IR代码, 以查看具体的实现和修改。

准备工作

在这里, 还是使用上面的test.c文件作为我们的测试代码。使用如下的命令, 我们可以得到使用asan前后test.c对应的LLVM IR(由于默认采用了-O0的方式编译本文出现的所有文件, 因此得到的IR可能不是标准的SSA形式, 在此特别说明)。

[不启动asan]: clang ./test -o ./test1.ll -emit-llvm -S
[启用asan]: clang ./test -o ./test2.ll emit-llvm -S -fsanitize=address 分析文件

为了更好地和asan生成的IR比较, 因此首先查看文件test1.ll, 文件内容如下:

; Function Attrs: nounwind ssp uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4
%a = alloca [32 x i8], align 16
%b = alloca [32 x i8], align 16
%c = alloca [50 x i8], align 16
store i32 0, i32* %1, align 4
%2 = getelementptr inbounds [32 x i8], [32 x i8]* %a, i64 0, i64 1
store i8 49, i8* %2, align 1
%3 = getelementptr inbounds [32 x i8], [32 x i8]* %b, i64 0, i64 32
store i8 97, i8* %3, align 16
%4 = getelementptr inbounds [50 x i8], [50 x i8]* %c, i64 0, i64 100
store i8 99, i8* %4, align 4
ret i32 0
}

可以看到IR几乎是test.c中的代码的直接翻译。

接下来, 可以查看test2.ll中的内容,由于生成的test2.ll文件比较大, 在当前计算机上生成了超过300行的代码。于是,分段查看当前的代码:

define i32 @main() #0 {
%1 = alloca i32, align 4
%2 = load i32, i32* @__asan_option_detect_stack_use_after_return
%3 = icmp ne i32 %2, 0
br i1 %3, label %4, label %6
; <label>:4 ; preds = %0
%5 = call i64 @__asan_stack_malloc_2(i64 256)
br label %6
; <label>:6 ; preds = %0, %4
%7 = phi i64 [ 0, %0 ], [ %5, %4 ]
%8 = icmp eq i64 %7, 0
br i1 %8, label %9, label %11
; <label>:9 ; preds = %6
%MyAlloca = alloca i8, i64 256, align 32
%10 = ptrtoint i8* %MyAlloca to i64
br label %11 如果asan中启用了asan_option_detect_stack_use_after_return标志, 那么使用asan_stack_malloc_2分配栈内存;否则使用alloca分配栈内存。 在这里需要注意的是分配栈的大小是256个字节。在test1.ll中, 可以看到当前的代码在栈上分配内存有三个,分别是[32i8], [32i8]和[50 * i8]。 并且由上面部分可以知道asan将会在每个栈变量两侧插入相应的redzone作为检测变量是否溢出的依据。因此当前的栈结构应该是这样的:
Address Sanitizer in macOS

整个栈是32个字节对齐的, 一般来说是32个字节, 但是理论上也可能存在更大的对齐值。

; <label>:11 ; preds = %6, %9
%12 = phi i64 [ %7, %6 ], [ %10, %9 ]
%13 = add i64 %12, 32 ; 得到%a的地址
%14 = inttoptr i64 %13 to [32 x i8]* ; 得到%a的指针
%15 = add i64 %12, 96
%16 = inttoptr i64 %15 to [32 x i8]* ; 得到%b
%17 = add i64 %12, 160
%18 = inttoptr i64 %17 to [50 x i8]* ;得到%c
%19 = inttoptr i64 %12 to i64*
store i64 1102416563, i64* %19 ; rsp=0x41B58AB3
%20 = add i64 %12, 8 ; rsp+8
%21 = inttoptr i64 %20 to i64*
store i64 ptrtoint ([33 x i8]* @__asan_gen_ to i64), i64* %21 ;栈信息指针
%22 = add i64 %12, 16 ; rsp+16 = ptr main
%23 = inttoptr i64 %22 to i64*
store i64 ptrtoint (i32 ()* @main to i64), i64* %23
%24 = lshr i64 %12, 3 ;shadow memory[0]
%25 = or i64 %24, 17592186044416 ;imm=0x100000000000
%26 = add i64 %25, 0 ;redzone[0]
%27 = inttoptr i64 %26 to i64*
store i64 4059165169, i64* %27
%28 = add i64 %25, 8 ;redzone[1]
%29 = inttoptr i64 %28 to i64*
store i64 4076008178, i64* %29
%30 = add i64 %25, 16 ;redzone[2]
%31 = inttoptr i64 %30 to i64*
store i64 4076008178, i64* %31
%32 = add i64 %25, 24 ; redzone[3]
%33 = inttoptr i64 %32 to i64*
store i64 -868082074072776704, i64* %33
store i32 0, i32* %1, align 4
%34 = getelementptr inbounds [32 x i8], [32 x i8]* %14, i64 0, i64 1
%35 = ptrtoint i8* %34 to i64
%36 = lshr i64 %35, 3
%37 = or i64 %36, 17592186044416
%38 = inttoptr i64 %37 to i8*
%39 = load i8, i8* %38
%40 = icmp ne i8 %39, 0
br i1 %40, label %41, label %46, !prof !2 asan在这里重新定位了三个变量。其次做了如下三件事: (1) 在redzone[0]保存了三个指针信息, 地址从低到高分别为:[rsp+0x00] = 0x41B58AB3 (redzone[0]'s magic number)[rsp+0x08] = @asangen(@asangen保存了当前栈的基本信息)[rsp+0x10] = ptr @main (当前函数的函数指针) (2) 设置shadow memory的基本信息:如果是redzone[0], 那么使用0xF1F1F1F1进行填充, 以此作为检测手段;如果是redzone[1], 那么使用0xF2F2F2F2进行填充;redzone[3]则使用0xF3F3F3F3。 * 正常分配的内存则是使用0进行说明。 如果是0, 那么证明当前位置是有效的。 为了更好的理解这部分内容, 我将使用调试器查看shadow memory中具体的值:
Address Sanitizer in macOS

这里可以很清楚的看到shadow memory的内存布局, 也可以更好的理解asan的检测原理。

(3) 对访问内存的行为做了如下的改变:

%34 = getelementptr inbounds [32 x i8], [32 x i8]* %14, i64 0, i64 1
%35 = ptrtoint i8* %34 to i64
%36 = lshr i64 %35, 3 ;计算索引
%37 = or i64 %36, 17592186044416 ;在lowmemory区域
%38 = inttoptr i64 %37 to i8* ;取出当前位置的值
%39 = load i8, i8* %38
%40 = icmp ne i8 %39, 0 ;比较是否是合法的

asan在程序每次访问指针的时候, 都检测其对应的shadow memory是否是0。 如果是0, 那么认为指针是合法的; 否则直接报错。test2.ll中的其他部分大部分都是这样检测的。

结论

asan的栈的检测是一项功能强大的工具,可以很好的检查出栈指针越界的问题。程序开发人员或者漏洞查找人员可以多利用asan检查或者找出更多的“越界漏洞”, 以保证程序的安全性。

参考


libtidy global buffer overflow

LLVM IR

AddressSanitizerAlgorithm

asan in Xcode7

asan from google

Happy Hacking JudyZhu123

本文由 安全客 原创发布,如需转载请注明来源及本文地址。
本文地址:http://bobao.360.cn/learning/detail/2973.html

Viewing all articles
Browse latest Browse all 12749

Trending Articles