【fuzz】ASAN原理解析

前言

参考文章:

本所涉及到的相关代码可以在我的仓库内找到: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 运行时库,替换 mallocfreememcpymemset等实现、提供报错函数

插桩

ASAN会在涉及内存读写的地方进行插桩,比如如下代码(插桩前):

*addr = ...;

插桩后:

if (IsPoisoned(addr)) {
  ReportError(addr, kAccessSize, kIsWrite);
}
*addr = ...; // or ... = *addr;

即通过IsPoisoned()函数来确定地址是否合法,而这个函数的实现依赖于shadow memory技术。

shadow memory

在ASAN中,会将8字节内存映射1字节内存的方式创建shadow memory。用shadow memory中每个字节的值来代表实际内存中,所对应的8个字节的状态。shadow memory也会利用memmap映射到进程的虚拟内存中,实际换算关系如下:

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报错时也会输出此图的内容

shadow memory取值

redzone & posion

在上一节的图中,我们可以看到很多负数值对应着redzone,也就是不可读写的区域。ASAN将一些区域标记为redzoneposion状态,在读写操作前利用IsPosioned(addr)函数,检查addr所对应的shadow memory是否为redzone,以此达到溢出检测的效果。

栈空间

每创建一个新的栈帧,ASAN都会为栈额外分配一些redzone空间,分布在栈顶,局部变量之间,以及栈底。假设有如下主函数:

int main() {
    char a[13];
    char b[13];

    a[-1] = 0;
    b[13] = 0;
}

那么实际的栈内存和shadow memory下图所示,栈空间redzone创建会以32byte对齐:

img

这图直接偷的知乎大佬,水印也留着,在本文开头有原文链接,侵删

因为考虑到栈帧是存在重复利用的情况,因而在函数return之前,会将该栈帧的全部shadow memory清零,防止因为存留的redzone标记而影响到其他函数的调用。

堆空间

ASAN为了能够检测堆区域的的溢出、UAF等问题,也使用redzone进行标记。ASAN劫持了malloc和free,不使用ptmalloc进行内存管理,gdb调试时可以明显看到堆区域的不同寻常。堆块使用的分配方式,反而极像Kernel所使用的slab机制,即根据需要的size直接分配几种固定大小的chunk。

假设我们调用malloc(0x13),其内存的效果图如下:

img

同上,侵删

可以看到,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进行限制。

我使用的示例代码如下:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char *p1 = malloc(13);
    char *p2 = malloc(16);
    p1[-1] = 'a'; //crash
    p1[13] = 'b'; //crash
    free(p1);
    free(p2);
    return 0;
}

可以在这里看到how2ASAN/heap-buffer-overflow at main · Loora1N/how2ASAN (github.com)

报错的shadow memory为下图:

image-20230829172316072