Address Sanitizer结果分析
2022-05-04 13:03:57

ASan结果分析

ASan简介

Address Sanitizer(ASan):是一个快速的C/C++内存错误检查器

该工具由一个编译器检测模块和一个提供内存操作函数(malloc/free)替代run-time库

该工具适用于x86,ARM,MIPS,PowerPC64架构,支持Linux,Darwin(OS X和IOS模拟器),FreeBSD,Android

ASan可以检测:

漏洞类型
UAF
Heap buffer overflow
Stack buffer overflow
Global buffer overflow
Use after return
Use after scpoe
Initialization order bugs
Memory leak

demo分析

来自https://hollk.blog.csdn.net/?type=blog

stack buffer overflow

int main(int argc, char **argv) {
  int closure[100];
  closure[1] = 0;
  return closure[argc + 100];  // overflow
}

编译:

clang -O -g -fsanitize=address stackdemo.cpp -o stackdemo

定义了一个int类型的,长度为100的数组==》int为4字节==》closure数组占400个字节

在最后返回closure[argc + 100]出现溢出==》argc+100超出了栈空间的大小

运行编译后的程序==》ASan显示检测日志:

初次分析日志内容:

==2045857==表示检测到问题的进程号为2045867

ERROR后面为检测到的漏洞类型:stack-buffer-overflow

漏洞所在地址为0x7fffffffdf34

漏洞触发时寄存器:

PC:0x0000004c32f4

bp:0x7fffffffdd70

sp:0x7fffffffdd68

描述主要采用READ操作,在线程T0的0x7fffffffdf34栈地址处读取大小为4的数据

#0,1,2为函数栈

漏洞地址0x7fffffffdf34位于T0线程的栈中的栈帧偏移量为436处,并指出被溢出的变量

其余部分为总结和警告

shaow字节图:

其中的一个字节表示源程序内存中的8个字节

该图为对shadow字节图解释:

ASan会对程序中的内存空间右侧或者两侧添加一个不可写区域redzone

f1为左侧增加区域

f3为右侧增加区域

在shadow图中f1与f3中的00有50个字节,每一个字节又代表源程序中的8个字节==》

对应内存中50*8 = 400个字节==》closure[100]开辟的空间大小

在00后第一个f3为**[f3]**:

表示该字节被溢出==》

在运行程序时没有添加任何参数==》argc为1(stackdemo参加计数)==》

所以返回为closure[101]==》溢出4个字节==》[f3]

==》日志分析总结:

被溢出变量:closure

溢出点所在地址:0x7fffffffdf34

溢出点距离变量起始点偏移:436

global-buffer-overflow

溢出全局变量部分的内存

int hollk[100] = {-1};
int main(int argc, char **argv) {
  return hollk[argc + 100];  // overflow
}

编译:

clang -O -g -fsanitize=address glo.cpp -o glo

创建了一个int型数组closure[100]==》400字节

argc作为输入参数计数,即使运行程序时不添加额外的参数,argc也会将glo.cpp作为一个计数==》

return返回closure[argc+100]==》closure[101]==》超过closure数组的内存空间

编译后运行:

根据ASan日志分析:

溢出点位于:0x0000004f6d34

寄存器:

pc:0x0000004c3142
bp:0x7ffd2860b3b0
sp:0x7ffd2860b3a8

存在溢出点的全局变量为closure,溢出4个字节

shadow中f9为全局变量右侧添加的redzone,溢出4字节==》[f9]

stack-use-after-scope

局部变量在脱离作用域后再次被使用

volatile int *p = 0;

int main() {
  {
    int closure = 0;
    p = &closure;
  }
  *p = 5;
  return 0;
}

编译:

clang -O -g -fsanitize=address clo.cpp -o clo

使用volatile关键字创建int指针p,赋值为0

创建一个int类型的变量closure,并赋值为0==》局部变量

取closure变量地址赋值给指针p==》main函数结束,修改p指针指向的位置的值为5==》

closure变量中的值变为5==》因为closure是一个局部变量,超过了main函数的作用范围后发生了变化==》

触发stack-use-after-scope漏洞

volatile:提醒编译器该处创建的指针p是会发生变化的,编译后每一次读取或存储p指针,都将会重新进行读操作

例:

A,B两个线程同时读取p指针0x7ffffab0中的值==》

此时A读取的的是地址0x7ffffab0中的值0,B读取的也是0x7ffffab0处的0==》

使用该关键字

如果A将p指针修改为0x7ffffcd0并将其中的值修改为1==》

B将重新读取指针p的地址0x7ffffcd0,再读取地址内的值1

未使用该关键字

如果A将p指针修改为0x7ffffcd0并将其中的值修改为1==》

B将读取0x7ffffab0处的0

编译后运行:

ASan日志分析:

漏洞出现地址:0x7ffc238179a0

寄存器:

pc 0x0000004c3201 
bp 0x7ffc23817970 
sp 0x7ffc23817968

出现漏洞的变量closure

[f8]代表closure变量所在位置出现漏洞

stack-use-after-return

与stack-use-after-scope类似,都是局部变量脱离了作用域再次进行操作

int *ptr;
__attribute__((noinline))
void FunctionA() {
  int closure[100];
  ptr = &closure[0];
}

int main(int argc, char **argv) {
  FunctionA();
  return ptr[argc];
}

编译:

clang -O -g -fsanitize=address clo.cpp -o clo

首先定义了一个全局变量ptr,定义函数属性为noinline==》声明函数为非内敛函数

内敛函数与非内敛函数对比:

//非内敛
int funA(int a,int b){
    return a+b;
}

int main(){
    funA(1,1);
    return 0;
}


//内敛
int main(){
    int a = 1;
    int b = 1;
    int c = a + b;
    return 0;
}

==》在funA函数内敛后,编译器会保持原有执行流程的情况下,将funA函数中的执行流程重组进main函数中==》demo中指定为非内敛属性==》在编译中保持FunctionA函数独立于main函数,成调用关系==》

FunctionA函数中定义了一个int类型的数组closure,并将closure数组起始地址赋值给全局变量指针ptr==》

进入main函数,调用FunctionA函数,返回ptr[argc]

注意点:closure是FunctionA函数中的局部变量,却在main函数中返回了closure数组中下标为argc位置的数据

==》在main函数中使用了closure数组属于脱离本身的作用域==》stack-use-after-return

编译后运行:

由于ASan默认是不检测stack-use-after-return,需要开启检测选项

ASAN_OPTIONS=detect_stack_use_after_return=1

ASan日志分析:

漏洞出现地址:0x7fb4a70e8024

寄存器:

pc 0x0000004c32dc
bp 0x7ffeae659470
sp 0x7ffeae659468

f5==》表示出现stack after return内存空间的范围

由于偏移量为36==》第五个f5的位置出现中括号(到达漏洞位置)

heap-buffer-overflow

int main(int argc, char **argv) {
  int *closure = new int[100];
  closure[0] = 0;
  int res = closure[argc + 100];
  delete [] closure;
  return res;
}

编译:

gcc -O -g -fsanitize=address clo.cpp -o clo

在C++中使用new来创建堆块(底层由malloc实现),使用delete释放堆块

首先开辟一个存放整数的空间(int),整数的初值为100==》返回一个指向该存储空间的地址==》

closure = malloc(400)

将堆块中的第一个四字节空间赋值为0==》将argc+100处的内容赋值给res变量==》

释放closure堆块并返回res的值==》

argc+100超过了堆块400字节内存空间范围==》heap-buffer-overflow

编译后运行:

ASan日志分析:

漏洞出现地址:0x7ffc238179a0

寄存器:

pc 0x0000004c3201 
bp 0x7ffc23817970 
sp 0x7ffc23817968

shadow解析:fa为closure堆块两侧的redzone空间==》中间的00==》内存空间的400字节

[fa]==》溢出了argc字节

heap-use-after-free

释放的堆块没有置空,并且已经释放该指针又进行调用

int main(int argc, char **argv) {
  int *closure = new int[100];
  delete [] closure;
  return closure[argc];
}

编译:

gcc -O -g -fsanitize=address clo.cpp -o clo

创建一个400字节大小的堆块,并将malloc指针赋给closure

释放堆块,返回closure堆块中argc数组下标对应的内容

编译后运行:

ASan日志分析:

漏洞出现地址:0x614000000044

寄存器:

pc 0x560f6b5b621f
bp 0x7ffd264ddd60
sp 0x7ffd264ddd50

shadow解析:紫色的fd表示被释放堆块区域

return返回时被释放的堆块的第argc数组下标中的内容==》[fd]在第一个

memory leaks

内存泄漏漏洞:

创建内存空间之后没有释放,导致数据在内存空间中没有消除,可以通过一些手段读取这段数据内容

#include <stdlib.h>

void *closure;

int main() {
  closure = malloc(8);
  closure = 0;
  return 0;
}

编译:

gcc -g -fsanitize=address clo.cpp -o clo

创建一个8字节大小的堆块

在没有将堆块释放的情况下将堆块的malloc指针置空==》

8字节的内存空间只要进程不结束,会一直停留在堆区==》memory leaks

编译后运行:

ASan日志展示了泄露时的函数栈,泄漏的字节长度

initialization-order-fiasco

由变量初始化顺序导致

//a.cc
#include <stdio.h>
extern int extern_global;
int __attribute__((noinline)) read_extern_global() {
  return extern_global;
}
int x = read_extern_global() + 1;
int main() {
  printf("%d\n", x);
  return 0;
}

首先使用extern定义了一个int型全局变量extern_global==》

extern:扩大作用域==》即使extern_global是在a.cc中进行定义的,但是如果是a.cc和b.cc联合编译==》

b.cc也可以使用extern_global变量

接下来定义一个费内敛函数read_extern_global()==》返回extern_global的值

定义一个变量x==》x为read_extern_global()的返回值extern_global的值+1

在main函数中打印X的值

注意点:在编译的时候会优先初始化read_extern_global()函数的返回值,并计算出x的值等待main()进行调用

//b.cc
int foo() { return -2; }
int extern_global = foo();

定义了一个foo()函数,函数直接返回-2,定义extern_global变量为foo()函数的返回值

编译运行ab:

clang++ a.cc b.cc -o a_b

==》

先编译a.cc==》优先初始化read_extern_global()==》继续初始化x = 0 + 1

编译运行ba:

clang++ b.cc a.cc -o ba

==》

先编译b.cc==》优先初始化foo()==》初始化x = -2 + 1

====》

若结果作为其他程序的参数==》

例:read()==》出现整数溢出:

read(0,buf,0xffffff)

若buf空间不足==》堆溢出/栈溢出

ASan分析:

联合编译后运行:

clang++ -g -fsanitize=address a.cc b.cc -o clo

ASan日志分析:

漏洞出现地址:0x000000e20e80

寄存器:

pc 0x0000004c6bc8
bp 0x7ffe376c2cb0
sp 0x7ffe376c2ca8