前言
参考文章:
本所涉及到的相关代码可以在我的仓库内找到:how2ASAN/README.md at main · Loora1N/how2ASAN (github.com)
ASAN简介
ASAN(Address Sanitizer)是针对 C/C++ 的快速内存错误检测工具,在运行时检测 C/C++ 代码中的多种内存错误。ASAN 早先是 LLVM 中的特性,后被集成到 GCC 4.8 中,在 4.9 版本中加入了对 ARM 平台的支持。
ASAN 目前支持的平台有 X86/X86_64/ARM/ARM64,对于 ARM64 平台,Android 官方推荐使用 HWASan (HWAddress Sanitizer)。目前支持一下类型的内存错误:
- use-after-free
- use-after-return
- use-after-scope
- heap-buffer-overflow
- stack-buffer-overflow
- global-buffer-overflow
- Memory leaks
- double-free
- Initialization order bugs
可以通过编译时,使用clang
和-fsanitize=address
参数开启此功能
ASAN原理
ASAN模块主要分为两个部分:
- instrument 静态插桩模块,对栈上对象、全局对象、动态分配的对象分配 redzone,以及针对这些内存做访问检测
- runtime 运行时库,替换
malloc
,free
,memcpy
,memset
等实现、提供报错函数
插桩
ASAN会在涉及内存读写的地方进行插桩,比如如下代码(插桩前):
1 | *addr = ...; |
插桩后:
1 | if (IsPoisoned(addr)) { |
即通过IsPoisoned()
函数来确定地址是否合法,而这个函数的实现依赖于shadow memory技术。
shadow memory
在ASAN中,会将8字节内存映射1字节内存的方式创建shadow memory。用shadow memory中每个字节的值来代表实际内存中,所对应的8个字节的状态。shadow memory也会利用memmap映射到进程的虚拟内存中,实际换算关系如下:
1 | shadow_addr = (real_addr > 3) + offset; |
- real_addr为实际访问地址,如堆栈地址
- shadow_addr 指映射的shadow memory的存储地址
- offset为一个偏移,是为了防止shadow memory与用户正常使用的地址出现重叠
具体实现可以看这里AddressSanitizer.cpp (github.com)的getShadowMapping()
函数。
shadow memory的取值大致可以分为以下3种情况:
- 0x00,代表该8字节可正常读写
- 小于8的正整数n,代表该8个字节中只有前n个字节可读写
- 负数,代表该区域不可读写。
具体不同的数值有不同的含义,可以参考下图查看,再每次ASAN报错时也会输出此图的内容
redzone & posion
在上一节的图中,我们可以看到很多负数值对应着redzone,也就是不可读写的区域。ASAN将一些区域标记为redzone和posion状态,在读写操作前利用IsPosioned(addr)
函数,检查addr所对应的shadow memory是否为redzone,以此达到溢出检测的效果。
栈空间
每创建一个新的栈帧,ASAN都会为栈额外分配一些redzone空间,分布在栈顶,局部变量之间,以及栈底。假设有如下主函数:
1 | int main() { |
那么实际的栈内存和shadow memory下图所示,栈空间redzone创建会以32byte对齐:
这图直接偷的知乎大佬,水印也留着,在本文开头有原文链接,侵删
因为考虑到栈帧是存在重复利用的情况,因而在函数return之前,会将该栈帧的全部shadow memory清零,防止因为存留的redzone标记而影响到其他函数的调用。
堆空间
ASAN为了能够检测堆区域的的溢出、UAF等问题,也使用redzone进行标记。ASAN劫持了malloc和free,不使用ptmalloc进行内存管理,gdb调试时可以明显看到堆区域的不同寻常。堆块使用的分配方式,反而极像Kernel所使用的slab机制,即根据需要的size直接分配几种固定大小的chunk。
假设我们调用malloc(0x13)
,其内存的效果图如下:
同上,侵删
可以看到,ASAN在我们申请的地址之前,增加了固定16byte的redzone,然后以16byte对齐。
需注意的事,这里只有left redzone,且取代了ptmalloc的0x10大小的chunk header。但根据我在测试时发现,堆空间的redzone似乎在初始化时会全部标记为0xfa
,即Heap left redzone。然后在malloc时,将用户使用空间取消posion。因而下个chunk的left redzone会充当上个chunk的right redzone,而最后一个chunk的right zone则是由初始化时设定的redzone进行限制。
我使用的示例代码如下:
1 |
|
可以在这里看到how2ASAN/heap-buffer-overflow at main · Loora1N/how2ASAN (github.com)
报错的shadow memory为下图: