linux内核(5.4.81)——KASAN

 

KASAN 简述

  • KASAN是内核用于动态检测内存错误的工具, 简单来说, 数据区域可分为两种:可访问区域,不可访问区域(red_zone).KASAN存在影子内存(shadow memory), 他和正常内存的比例是1:8, 即1byte shadow memory可以代表8bytes 正常内存的可访问性.
  • 128TB(内核正常内存) : 16TB(影子内存) —- Documentation/x86/x86_64/mm.rst x86-64 内存布局显示如下:
 ffffec0000000000 |  -20    TB | fffffbffffffffff |   16 TB | KASAN shadow memory
  • 具体规则(+: byte可访问, -: byte不可访问)
    • 如果1byte shadow memory对应的8bytes 内存都可访问, 则*(shadow memory) == 0
      [0] -> [+, +, +, +, +, +, +, +]
      
    • 如果1byte shadow memory对应的8bytes 内存都不可访问, 则*(shadow memory)为负数
      [-1] -> [-, -, -, -, -, -, -, -]
      
    • 如果1byte shadow memory对应的8bytes 内存中有N bytes可访问, 则*(shadow memory) == N
      if N = 3
      [3] -> [+, +, +, -, -, -, -, -]
      
  • 实现原理
    • 代码插桩: 利用编译器特性进行代码插桩, 当程序对内存进行读取或写入(load/store)时插入kasan检测代码
    • kasan检测代码: asan_loadN(addr)/asan_storeN(addr) (后面会对源码做详细分析), 主要功能是检测addr所在位置的N bytes内存是否可用.

 

源码分析

kasan检测入口

  • 使用宏定义实现asan_load/asan_store, 关键函数为check_memory_region_inline
#define DEFINE_ASAN_LOAD_STORE(size)                    \
    void __asan_load##size(unsigned long addr)            \
    {                                \
        check_memory_region_inline(addr, size, false, _RET_IP_);\
    }                                \
    EXPORT_SYMBOL(__asan_load##size);                \
    __alias(__asan_load##size)                    \
    void __asan_load##size##_noabort(unsigned long);        \
    EXPORT_SYMBOL(__asan_load##size##_noabort);            \
    void __asan_store##size(unsigned long addr)            \
    {                                \
        check_memory_region_inline(addr, size, true, _RET_IP_);    \
    }                                \
    EXPORT_SYMBOL(__asan_store##size);                \
    __alias(__asan_store##size)                    \
    void __asan_store##size##_noabort(unsigned long);        \
    EXPORT_SYMBOL(__asan_store##size##_noabort)

check_memory_region_inline

  • 当size==0时, 对内存不做读写操作, 正常返回
  • addr必须大于KASAN_SHADOW_START对应的正常地址, 即addr必须在shadow映射的界限内
  • memory_is_poisoned作为核心函数判断内存是否可用
static __always_inline bool check_memory_region_inline(unsigned long addr,
                        size_t size, bool write,
                        unsigned long ret_ip)
{
    if (unlikely(size == 0))
        return true;

    if (unlikely((void *)addr <
        kasan_shadow_to_mem((void *)KASAN_SHADOW_START))) {
        kasan_report(addr, size, write, ret_ip);
        return false;
    }

    if (likely(!memory_is_poisoned(addr, size)))
        return true;

    kasan_report(addr, size, write, ret_ip);
    return false;
}

memory_is_poisoned

  • size 如果为常量, 则对1, 2-4-8, 16这三种情况分开讨论
  • size 不为常量进入memory_is_poisoned_n
static __always_inline bool memory_is_poisoned(unsigned long addr, size_t size)
{
    // 判断size是否为常量
    if (__builtin_constant_p(size)) {
        switch (size) {
        case 1:
            return memory_is_poisoned_1(addr);
        case 2:
        case 4:
        case 8:
            return memory_is_poisoned_2_4_8(addr, size);
        case 16:
            return memory_is_poisoned_16(addr);
        default:
            BUILD_BUG();
        }
    }

    return memory_is_poisoned_n(addr, size);
}

memory_is_poisoned_1

  • 通过kasan_mem_to_shadow获得addr对应的shadow_addr, ((s8 )shadow_addr)获得shadow_addr所对应的8bits内存值shadow_value
  • 因为1byte shadow对应8bytes, 所以可以将 8bytes对齐的addr -> 8bytes对齐的addr+8 设为一个内存组, 对应1byte shadow, 此处addr&7获得addr在该组中的偏移量last_accessible_byte
  • 比较偏移量与shadow_value(该组内可访问内存的byte 数), 如果last_accessible_byte <= shadow_value 显然addr可访问(当shadow_value==0时, 表示8bytes皆可访问)
static __always_inline bool memory_is_poisoned_1(unsigned long addr)
{
    s8 shadow_value = *(s8 *)kasan_mem_to_shadow((void *)addr);

    if (unlikely(shadow_value)) {
        s8 last_accessible_byte = addr & KASAN_SHADOW_MASK;
        return unlikely(last_accessible_byte >= shadow_value);
    }

    return false;
}

memory_is_poisoned_2_4_8

  • 与memory_is_poisoned_1的区别在于此处多考虑了一种情况(待store/read内存跨越两个内存组)
  • 对于跨越内存组的情况, 需要满足第一个内存组8bytes皆可访问(0), 第二个内存组shadow_value >= 组内偏移
  • 如果不跨越内存, 只判断末尾地址是否可store/load 1byte 内存
static __always_inline bool memory_is_poisoned_2_4_8(unsigned long addr,
                        unsigned long size)
{
    u8 *shadow_addr = (u8 *)kasan_mem_to_shadow((void *)addr);

    /*
     * Access crosses 8(shadow size)-byte boundary. Such access maps
     * into 2 shadow bytes, so we need to check them both.
     */
    if (unlikely(((addr + size - 1) & KASAN_SHADOW_MASK) < size - 1))
        return *shadow_addr || memory_is_poisoned_1(addr + size - 1);

    return memory_is_poisoned_1(addr + size - 1);
}

memory_is_poisoned_16

  • 只有addr本身为8bytes 对齐时才会只跨越两个内存组, 否则跨越三个内存组
  • 对于跨越三个内存组的情况, 需要满足前两个内存组内存皆可访问(16bit shadow_value == 0), 同时第三个内存组shadow_value >= 组内偏移
  • 如果只跨越两个内存组, 只需要16bit shadow_value == 0即可
static __always_inline bool memory_is_poisoned_16(unsigned long addr)
{
    u16 *shadow_addr = (u16 *)kasan_mem_to_shadow((void *)addr);

    /* Unaligned 16-bytes access maps into 3 shadow bytes. */
    if (unlikely(!IS_ALIGNED(addr, KASAN_SHADOW_SCALE_SIZE)))
        return *shadow_addr || memory_is_poisoned_1(addr + 15);

    return *shadow_addr;
}

memory_is_poisoned_n

  • memory_is_nonzero:
    • 首先定位与待访问内存块对应的shadow_mem_block, 检测shadow_mem_block中的shadow_value是否全为0, 如果全为0, 则内存块可访问, 从memory_is_poisoned_n返回
    • 如果shadow_value不全为0, 则找到第一个不为0的shadow_value对应的shadow_addr, return shadow_addr
  • 得到memory_is_nonzero中返回的shadow_addr, 如果shadow_addr == last_shadow(末尾地址对应的shadow_addr) 则内存块可访问, 从memory_is_poisoned_n返回
  • 否则判断末尾地址是否可store/load 1byte(比较末尾地址偏移与last_shadow的大小)
static __always_inline bool memory_is_poisoned_n(unsigned long addr,
                        size_t size)
{
    unsigned long ret;

    ret = memory_is_nonzero(kasan_mem_to_shadow((void *)addr),
            kasan_mem_to_shadow((void *)addr + size - 1) + 1);

    if (unlikely(ret)) {
        unsigned long last_byte = addr + size - 1;
        s8 *last_shadow = (s8 *)kasan_mem_to_shadow((void *)last_byte);

        if (unlikely(ret != (unsigned long)last_shadow ||
            ((long)(last_byte & KASAN_SHADOW_MASK) >= *last_shadow)))
            return true;
    }
    return false;
}

 

实例分析

buddy_kasan

kasan_alloc_pages: 标记shadow_mem为0, kasan_free_pages: 标记shadow_mem为不可访问

验证代码

  • 编写kasan驱动, 使用alloc_pages调用buddy分配内存, 查看分配后以及释放后的shadow_mem内存(local_addr[0] = ‘\x10’; 对buddy分配的内存块做store操作, 会触发__asan_store1代码插桩)
long kasan_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
{
    char *local_addr = NULL;
    struct page *local_pg = alloc_pages(GFP_KERNEL, 2);
    local_addr = page_address(local_pg);
    local_addr[0] = '\x10';
    __free_pages(local_pg, 2);

    printk(KERN_DEBUG "[+] modules kasan debug\n");

    return 0;
}
  • ida反汇编代码(存在_asan_store1_noabort(v5), 猜测成立)
__int64 __fastcall kasan_ioctl(file *filp, unsigned int cmd, unsigned __int64 arg)
{
  __int64 v3; // r13
  __int64 v4; // r12
  _BYTE *v5; // r12

  _fentry__(filp, cmd, arg);
  v3 = alloc_pages_current(3264LL, 2LL);
  _asan_load8_noabort(&vmemmap_base);
  v4 = v3 - vmemmap_base;
  _asan_load8_noabort(&page_offset_base);
  v5 = (_BYTE *)(page_offset_base + (v4 >> 6 << 12));
  _asan_store1_noabort(v5);
  *v5 = 0x10;
  printk(&unk_1C0, v5);
  _free_pages(v3, 2LL);
  printk(&unk_200, 2LL);
  return 0LL;
}

buddy_kasan 动态调试

  • 在call __asan_store1处下断点, 查看rdi内容
      gef➤  p $rdi
      $1 = 0xffff88805b034000
    
  • 计算shadow_mem = addr >> 3 + 0xdffffc0000000000, 所以0xffff88805b034000对应的shadow_mem: 0xffffed100b606800
      static inline void *kasan_mem_to_shadow(const void *addr)
      {
          return (void *)((unsigned long)addr >> KASAN_SHADOW_SCALE_SHIFT)
              + KASAN_SHADOW_OFFSET;
      }
    
  • 查看shadow_mem内容(shadow_value=0x0, 表示内存块可访问)
      gef➤  x/16xg 0xffffed100b606800
      0xffffed100b606800:     0x0000000000000000      0x0000000000000000
      0xffffed100b606810:     0x0000000000000000      0x0000000000000000
      0xffffed100b606820:     0x0000000000000000      0x0000000000000000
      0xffffed100b606830:     0x0000000000000000      0x0000000000000000
      0xffffed100b606840:     0x0000000000000000      0x0000000000000000
      0xffffed100b606850:     0x0000000000000000      0x0000000000000000
      0xffffed100b606860:     0x0000000000000000      0x0000000000000000
      0xffffed100b606870:     0x0000000000000000      0x0000000000000000
    
  • 程序执行__free_pages, 释放buddy内存块, 再次查看shadow_mem(0xff=-1, 内存块不可访问)
      gef➤  x/16xg 0xffffed100b606800
      0xffffed100b606800:     0xffffffffffffffff      0xffffffffffffffff
      0xffffed100b606810:     0xffffffffffffffff      0xffffffffffffffff
      0xffffed100b606820:     0xffffffffffffffff      0xffffffffffffffff
      0xffffed100b606830:     0xffffffffffffffff      0xffffffffffffffff
      0xffffed100b606840:     0xffffffffffffffff      0xffffffffffffffff
      0xffffed100b606850:     0xffffffffffffffff      0xffffffffffffffff
      0xffffed100b606860:     0xffffffffffffffff      0xffffffffffffffff
      0xffffed100b606870:     0xffffffffffffffff      0xffffffffffffffff
    
  • 计算shadow_mem边界(mem:shadow_mem=8:1), alloc_pages申请4页(0x1000 4)对应shadow_mem(0x1000 4/8=0x800), 发现0xff与0xfc的分界, 猜测成立
      gef➤  x/16xg 0xffffed100b606800+0x800-0x10
      0xffffed100b606ff0:     0xffffffffffffffff      0xffffffffffffffff
      0xffffed100b607000:     0xfcfcfcfcfcfcfcfc      0xfcfcfcfcfcfcfcfc
      0xffffed100b607010:     0xfcfcfcfcfcfcfcfc      0xfcfcfcfcfcfcfcfc
      0xffffed100b607020:     0xfcfcfcfcfcfcfcfc      0xfcfcfcfcfcfcfcfc
      0xffffed100b607030:     0xfcfcfcfcfcfcfcfc      0xfcfcfcfcfcfcfcfc
      0xffffed100b607040:     0xfcfcfcfcfcfcfcfc      0xfcfcfcfcfcfcfcfc
      0xffffed100b607050:     0xfcfcfcfcfcfcfcfc      0xfcfcfcfcfcfcfcfc
      0xffffed100b607060:     0xfcfcfcfcfcfcfcfc      0xfcfcfcfcfcfcfcfc
    
  • 由上述调试结果可知, buddy_kasan可有效的检测内存越界以及uaf

slub_kasan

提到slub_kasan不得不提及slub_debug, slub_debug是slub早期的内存检测机制, 想详细的了解可以看一下这篇文章 slub_debug原理, 或者我之前写的一篇文章点击此处

在这里我简单的描述一下slub_debug的原理:

  • 首先slub算法将page切割成一个一个的slub_obj, obj的布局大概是当obj空闲时, 复用头部空间用于存储free_list(指向下一个空闲obj)
  • 当slub_debug开启后, slub_obj的布局将发生非常大的改变如下, 第一行为初始布局, 第二行为开启slub_debug后的布局
  • slub_debug
  • 图中red_zone为右边界越界检测, red_left_pad为左边界越界检测, 这里对于red_left_pad着重说一下, 如果从第二幅图来看, 怎么也看不出左边界越界检测原理, 但是如果布局是第三幅图的话, 就非常明了了. 所以重点在于第二幅布局如何变成第三幅布局, slub的实现是, 每个obj在设计时仍然采用第二副布局, 但却在page开头开辟了一个red_left_pad, 这样就巧妙的完成了转换(至于为什么要经过这么一番转换, 只能说是历史遗留问题, 设计右边界越界检测时并没有考虑左边界越界)
  • 然后再说一下slub_debug的局限性, slub_debug虽然和kasan一样设计了red_zone但是, slub_debug的安全检测只在alloc/free时启动, 即如果一个越界内存块永远不被释放, 则安全漏洞很难被发现(为了解决这个问题, slub设计了slub_info可以主动的去触发安全检测, 但是和kasan相比, 在检测范围上仍然很局限 —- 只能检测slub内存问题, 同时还有一个非常重要的问题, slub_debug的red_zone是和填充数据位于同一内存块, 是可以被修改的, 有被劫持的风险)
  • 关于slub_debug就说这么多, 继续研究slub_kasan(slub_kasan的布局在slub_debug上面再加一个)

slub_debug

  • kasan_poison_slab: 当创建kmem_cache时将page对应的shadow_mem标记为KASAN_KMALLOC_REDZONE, 表示内存不可访问
      void kasan_poison_slab(struct page *page)
      {
          unsigned long i;
    
          for (i = 0; i < compound_nr(page); i++)
              page_kasan_tag_reset(page + i);
          kasan_poison_shadow(page_address(page), page_size(page),
                  KASAN_KMALLOC_REDZONE);
      }
    
  • kasan_kmalloc: 当使用kmalloc(x) 申请内存后, 有一部分内存可用, 填充red_zone, 修改shadow_mem(kasan_kmalloc和下面全局变量修改shadow_mem的原理类似, 可以滑到下面看看全局变量_kasan, 大致思路就是填充从obj_start+size到obj末尾内存为red_zone)

全局变量_kasan

asan_register_globals: 根据 struct kasan_global为全局变量填充shadow_mem, asan_unregister_globals实现为空

验证代码

  • 目标全局变量为global_var
      char global_var[34] = {'a', 'b', 'c', 'd'};
    
      long kasan_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
      {
          global_var[0] = 'x';
          global_var[34] = '*';
          return 0;
      }
    
  • ida反汇编代码
      __int64 __fastcall kasan_ioctl(file *filp, unsigned int cmd, unsigned __int64 arg)
      {
          __int64 result; // rax
    
          _fentry__(filp, cmd, arg);
          global_var[0] = 120;
          _asan_store1_noabort(&global_var[34]);
          result = 0LL;
          global_var[34] = 42;
          return result;
      }
    

全局变量 动态调试

  • 查看global_var内存(0xffffffffc000a000)与其对应的shadow_mem(0xfffffbfff8001400)发现, global_var存在34 bytes有效内存, 62 bytes为无效内存(red_zone)
      gef➤  disassemble
      Dump of assembler code for function kasan_ioctl:
      => 0xffffffffc0008000 <+0>:     data16 data16 data16 xchg ax,ax
      0xffffffffc0008005 <+5>:     push   rbp
      0xffffffffc0008006 <+6>:     mov    rdi,0xffffffffc000a022
      0xffffffffc000800d <+13>:    mov    BYTE PTR [rip+0x1fec],0x78        # 0xffffffffc000a000
      0xffffffffc0008014 <+20>:    mov    rbp,rsp
      0xffffffffc0008017 <+23>:    call   0xffffffff8145d790 <__asan_store1>
      0xffffffffc000801c <+28>:    xor    eax,eax
      0xffffffffc000801e <+30>:    mov    BYTE PTR [rip+0x1ffd],0x2a        # 0xffffffffc000a022
      0xffffffffc0008025 <+37>:    pop    rbp
      0xffffffffc0008026 <+38>:    ret
      End of assembler dump.
      gef➤  x/2xg 0xffffffffc000a000
      0xffffffffc000a000:     0x0000000064636261      0x0000000000000000
      gef➤  x/2xg 0xfffffbfff8001400
      0xfffffbfff8001400:     0xfafafa0200000000      0x00000000fafafafa
      gef➤
    
  • 进一步验证red_zone原理(ida显示驱动init_array段中存在指向__asan_register_globals的函数指针, 显然在驱动初始化阶段会调用该函数)
  • 分析__asan_register_globals源码发现一个用来修饰全局变量的结构体(存放于.data段), 结合ida反汇编结果后填充结构体如下:
      struct kasan_global {
          const void *beg=0xffffffffc000a000;        /* Address of the beginning of the global variable. */
          size_t size=0x22;            /* Size of the global variable. */
          size_t size_with_redzone=0x60;    /* Size of the variable + size of the red zone. 32 bytes aligned */
          const void *name="global_var";
          const void *module_name="/home/povcfe/code/modules/kasan/kasan.c";    /* Name of the module where the global variable is declared. */
          unsigned long has_dynamic_init=0;    /* This needed for C++ */
      #if KASAN_ABI_VERSION >= 4
          struct kasan_source_location *location;
      #endif
      #if KASAN_ABI_VERSION >= 5
          char *odr_indicator="/home/povcfe/code/modules/kasan/kasan.c";
      #endif
      };
    
  • size_with_redzone=0x60, 动调时发现redzone为62(0x3e) bytes, 显然此处size_with_redzone不可能被直接使用, 继续分析源码
  • __asan_register_globals->register_global(特别关注aligned_size, 这里会解决上诉问题)
      static void register_global(struct kasan_global *global)
      {
          size_t aligned_size = round_up(global->size, KASAN_SHADOW_SCALE_SIZE);
    
          // 设置有效内存对应的shadow_mem, 特别注意内存组内有效N byte不足8 bytes时, 需要设置shadow_value=N
          kasan_unpoison_shadow(global->beg, global->size);
    
          // 填充8 bytes对齐的redzone对应的shadow_mem为KASAN_GLOBAL_REDZONE
          kasan_poison_shadow(global->beg + aligned_size,
              global->size_with_redzone - aligned_size,
              KASAN_GLOBAL_REDZONE);
      }
    
  • 分析aligned_size原理(算数太差, 直接用c实现了一下, aligned_size=0x28)
      #define __round_mask(x, y) ((__typeof__(x))((y)-1))
      #define round_up(x, y) ((((x)-1) | __round_mask(x, y))+1)
    
      #include <stdio.h>
    
      size_t __round_mask(size_t x, size_t y)
      {
          return ((__typeof__(x))((y)-1));
      }
    
      size_t round_up(size_t x, size_t y)
      {
          return ((((x)-1) | __round_mask(x, y))+1);
      }
    
      int main()
      {
          size_t res = round_up(0x22, 7);
          printf("0x%lx", res);
      }
    
  • 故 global->size_with_redzone(0x60) = 0x28(0x22(size) + 0x6(red_zone)) + 0x38(red_zone), 即有效内存空间0x22 bytes, red_zone(0x6 + 0x38) bytes

栈变量_kasan

__kasan_unpoison_stack: 设置栈内有效内存shadow_mem

验证代码

  • 目标栈变量 stack_var
      long kasan_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
      {
          char stack_var[0x4] = {'a', 'b', 'c', 'd'};
          stack_var[0x0] = 'x';
    
          printk(KERN_DEBUG "[+] %s\n", stack_var);
          return 0;
      }
    
  • ida反汇编代码(注意到0xF1(KASAN_STACK_LEFT), 0xF3(KASAN_STACK_RIGHT)字段, 猜测stack_kasan的shadow_mem填充代码是直接通过编译器插入源代码中的)
      __int64 __fastcall kasan_ioctl(file *filp, unsigned int cmd, unsigned __int64 arg)
      {
          _DWORD *v3; // rbx
          _QWORD v5[4]; // [rsp-70h] [rbp-70h] BYREF
          int v6; // [rsp-50h] [rbp-50h] BYREF
          unsigned __int64 v7; // [rsp-18h] [rbp-18h]
    
          _fentry__(filp, cmd, arg);
          v3 = (_DWORD *)(((unsigned __int64)v5 >> 3) - 0x2000040000000000LL);
          v5[0] = 1102416563LL;
          v5[1] = "1 32 4 12 stack_var:15";
          v5[2] = kasan_ioctl;
          *v3 = 0xF1F1F1F1;
          v3[1] = 0xF3F3F304;
          v7 = __readgsqword(0x28u);
          v6 = 0x64636278;
          printk(&unk_220, &v6);
          *(_QWORD *)v3 = 0LL;
          return 0LL;
      }
    

栈变量 动态调试

  • 根据ida反汇编代码猜测栈变量存在左右边界(red_zone)且关于32 bytes对齐
  • stack_var(0xffff88805b02fcf0)与其shadow_mem(0xffffed100b605f9e)内容如下, 可知栈变量有效内存为4 bytes, red_zone_size: (8 3 + 8 – 4) + 8 3 = 52(左边界red_zone为32bytes, 右边界red_zone+size关于32 bytes对齐, 总red_zone大小与全局变量相同)
      gef➤  x/8xg 0xffff88805b02fcf0-0x20
      0xffff88805b02fcd0:     0x0000000041b58ab3      0xffffffffc000903c
      0xffff88805b02fce0:     0xffffffffc00080e1      0xffff88805e404500
      0xffff88805b02fcf0:     0xffff888064636261      0x0000000200000101
      0xffff88805b02fd00:     0x000000000000004c      0x0000000000000000
      gef➤  x/4xg 0xffffed100b605f9e-0x10
      0xffffed100b605f8e:     0x0000000000000000      0xf1f1f1f100000000
      0xffffed100b605f9e:     0x00000000f3f3f304      0x0000000000000000
      gef➤
    
  • 待函数返回(栈变量销毁时), 会将shadow_mem置0(见ida伪代码)
      *(_QWORD *)v3 = 0LL;
    
  • 在kasan源码中没有找到填充red_zone的代码, 且栈变量左边界red_zone对应的内存处存放了栈变量信息, 函数信息. 所以猜测栈变量填充red_zone是在编译阶段实现的, 且左边界填充内容为用于描述栈的结构体
(完)