Address Sanitizer(投毒?投毒!)
本文最后更新于:2024年1月6日 早上
0x0: 写在所有之前
笔者在Fuzz101项目中,遇到了ASAN(Address Sanitizer)这个内存错误检测工具,感觉挺巧妙的。
这边就浅浅的记录一下
0x1: What is it?
ASAN(Address Sanitizer)是google开发的一个针对C/C++的高效率的内存错误检测工具
它可以检测以下类型的漏洞
- stack buffer overflow
- heap buffer overflow
- global buffer overflow
- use after free
- use after return
- use after scope
- memory leak
- initialization order bugs
同时,ASAN还支持x86、x68_64、mips、arm、powerpc等多种架构
可以说,好用的一批
那它是怎么做到的嘞
因为是内存检测工具,所以要做到全面的检查,就要对每次的内存读/写及其他操作进行监测
笔者刚开始的时候觉得ASAN可能是canary保护的plusplus版本,后来才知道ASAN通过一个编译器检测模块和一个劫持内存操作函数(例如malloc/free)的run-time库来对内存进行监测。
1 |
|
Shadow Memory
影子内存,位于虚拟地址空间的中间(因为堆栈在两头),是用来记录主程序的内存是否可用的内存区域,这是ASAN的特有产物。
主程序的内存按照8字节对齐,而8个字节在影子内存中对应的区域为一个字节,所以主程序内存 :影子内存=8 :1。
影子内存无法在主程序中被读写,只有通过编译器相关代码才可访问
在每次对内存进行读写操作时,都会读取对应的影子内存检查其合法性
影子内存的计算公式如下:
1 |
|
Sanitizer 投毒
我觉得可能用大家都玩过的游戏——扫雷进行类比,会更好理解。
如果我们想要标记一个区域已经被使用,那么就在这块内存对应的Shadow Memory埋雷,一旦再次对此区域进行读写操作,就会触发那颗“雷“,从而造成crash。
1 |
|
当然,这只是举个例子。不同的漏洞所对应的”扫雷“策略也不同。
每个影子内存对应的可能值有9个:
- 当其对应的内存8个字节都未被投毒,value=0
- 当其对应的内存8个字节都被投毒,value=负数
- 当其对应得内存有k个字节被投毒,value=8-k
1 |
|
Report Error
ReportError
可以被实现为一个调用(这是默认方式),但还有一些其他稍微更有效率和/或更紧凑的解决方案。在某个时候,默认行为是:
将失败地址复制到 %rax(%eax)。
执行 ud2 指令(生成 SIGILL 信号)。
在 ud2 后的一个字节指令中编码访问类型和大小。总体上,这三条指令需要 5-6 字节的字节码。
也可以只使用单个指令(例如 ud2),但这将需要在运行时库中拥有一个完整的反汇编器(或其他一些技巧)。
实现
通过IDA进行分析可以很直观的看到ASAN的行为
小写一个demo
1 |
|
编译
1 |
|
这里面其实可以很清楚的看到mem memory和shadow memory之间的换算以及Report Error的调用
至于汇编层面的,笔者在这边贴出官方文档中的example
1 |
|
0x2: 报错查看
md,写了半天,才发现github里有官方给的demo,草了
🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡
stack overflow
demo:
1 |
|
当输入的内容较少时,会正常出现stack overflow的报错
图一框一:
- 漏洞类型——stack overflow
- 报错地址,pc、sp、bp等寄存器的值
- 在线程T0栈地址0x7ffff0291120进行read操作时,检测到错误
- 错误代码位于stack.c的14行
图一框二
- 漏洞地址0x7ffff0291120位于T0线程的栈中偏移80处,并指出漏洞存在的变量
图二
- shadow memory 展示
- 因为1字节的shadow memory对应8字节的mem memory,所以
f1 f1 f1 f1
与f2 f2
之间的区域为stack1
对应的shadow memory,stack2
同理 - 因为造成漏洞的时
return stack2[0x10];
,所以第一个f3被标识,表示此处投毒发现漏洞
此图的下半部分则为各种符号代表的含义
因为demo中使用了get,尝试输入大量数据造成栈溢出
但好像并不能输出log信息
那就进gdb康康
可以看到在最后调用__asan_report时,函数参数已经指向了dirty data,那就说明原本正常运行时ASAN多开辟出来的栈空间被恶意操作时会导致功能crash
笔者后来又试试了只有gets函数的demo,发现就算用ASAN编译也只会报出stack smash
铸币笔者认为,ASAN更倾向于在编译的时候就标注出漏洞,当执行到这个漏洞时引发报错,就像在fuzz中,这可能算是个代码覆盖率的问题,执行到漏洞存在分支便crash。而对于gets这种依赖用户输入的危险函数,ASAN在编译的时候并不能精确定义其危险性,故并没有进行检测。
笔者的说法可能有失偏颇,各位大师傅们如果有什么想法可以在评论区留言呜呜呜。
heap overflow
和stack overflow类似,就不细讲了
demo
1 |
|
ASAN_log
global buffer overflow
同上
demo:
1 |
|
但是ASAN_log很奇怪
特别是明明gloabl才0x10个字节大小,shadow memory前面那么大一块区域
且当我将当demo 换成return global[-1]
时,直接显示正常了。
???
如果有师傅知道是什么情况能不能联系一下铸币笔者呜呜
跪谢
use after free
demo:
1 |
|
ASAN_log
memory leak
内存泄露其实就是内存分配后没有释放,导致内存空间中有数据残留
demo:
没有free掉堆块
1 |
|
ASAN_log
stack use after scope
stack-use-after-scope指的是超出定义域外对局部变量操作
demo:
1 |
|
先用volatile指定一下指针p每次操作前都重新读一下值
ASAN_log
stack use after return
和stack-use-after-scope类似
1 |
|
但是ASAN默认是不检测这个错误的,所以要使用ASAN_OPTIONS=detect_stack_use_after_return=1
开启
ASAN_log
0x3: 一些小bug
一:
可以看到,针对溢出类型的漏洞时,ASAN在shadow memory ‘s left or right投毒的区域是有限的,若溢出的范围超出投毒的范围或者恰好落在别的可用内存对应的shadow memory,则不会报错
二:
众所周知,例如在x86_64进行堆块分配时,一个chunk的前8个字节是可以被前一chunk使用的,但这却会被ASAN检测出heap overflow
三:
当内存分配未8字节对齐时
可以看到这边有一个shadow memory为04
,结合上文所提到的shadow memory的9钟可能值,可知此处有4个字节被投毒
0x4: 最后的最后
花了两天时间大致了解了一下ASAN,总的来说这个工具还是很好用的,虽然有些小问题,但这很大一部分原因是语言导致的。
其实官方的文档写的超级详细,各位大师傅如果想深入了解ASAN可以直接去google的仓库