编辑
2025-10-01
记录知识
0

目录

代码
反汇编
总结

根据《KASAN(1)-简单实践》可以了解到OUTLINE模式按照函数插桩,也就是gcc会默认在编译的时候插入函数钩子,由内核实现这个函数钩子,本文主要分析outline模式下的kasan的实现。关于inline下的kasan的实现可以查看文章《KASAN(4)-INLINE插桩分析》

代码

为了实验一致性,测试outline的代码没有发生改变,如下

static noinline void kmalloc_oob_right(void) { char *ptr; size_t size = 123; ptr = kmalloc(size, GFP_KERNEL); ptr[size] = 'x'; kfree(ptr); return ; }

唯一改变的是内核的配置由inline设置为outline,如下

CONFIG_KASAN_OUTLINE=y # CONFIG_KASAN_INLINE is not set

反汇编

inline的分析中已经解析了,代码不是通过宏展开实现的,所以可以直接反汇编即可,这里方法很多,我使用crash的方式来反汇编,关于crash的文章可以参考《RK平台上使用crash进行live debug》及其相关文章

crash> dis kmalloc_oob_right 0xffffffd008c38bf0 <kmalloc_oob_right>: stp x29, x30, [sp,#-32]! 0xffffffd008c38bf4 <kmalloc_oob_right+4>: adrp x0, 0xffffffd00ae5f000 <idle_sched_class+32> 0xffffffd008c38bf8 <kmalloc_oob_right+8>: add x0, x0, #0x9f0 0xffffffd008c38bfc <kmalloc_oob_right+12>: mov x29, sp 0xffffffd008c38c00 <kmalloc_oob_right+16>: str x19, [sp,#16] 0xffffffd008c38c04 <kmalloc_oob_right+20>: bl 0xffffffd0083ee6b0 <__asan_load8> 0xffffffd008c38c08 <kmalloc_oob_right+24>: adrp x0, 0xffffffd00ae5f000 <idle_sched_class+32> 0xffffffd008c38c0c <kmalloc_oob_right+28>: mov x2, #0x7b // #123 0xffffffd008c38c10 <kmalloc_oob_right+32>: mov w1, #0xcc0 // #3264 0xffffffd008c38c14 <kmalloc_oob_right+36>: ldr x0, [x0,#2544] 0xffffffd008c38c18 <kmalloc_oob_right+40>: bl 0xffffffd0083e99b0 <kmem_cache_alloc_trace> 0xffffffd008c38c1c <kmalloc_oob_right+44>: mov x19, x0 0xffffffd008c38c20 <kmalloc_oob_right+48>: add x0, x0, #0x7b 0xffffffd008c38c24 <kmalloc_oob_right+52>: bl 0xffffffd0083ee2f4 <__asan_store1> 0xffffffd008c38c28 <kmalloc_oob_right+56>: mov w1, #0x78 // #120 0xffffffd008c38c2c <kmalloc_oob_right+60>: mov x0, x19 0xffffffd008c38c30 <kmalloc_oob_right+64>: strb w1, [x19,#123] 0xffffffd008c38c34 <kmalloc_oob_right+68>: bl 0xffffffd0083e7ac0 <kfree> 0xffffffd008c38c38 <kmalloc_oob_right+72>: ldr x19, [sp,#16] 0xffffffd008c38c3c <kmalloc_oob_right+76>: ldp x29, x30, [sp],#32 0xffffffd008c38c40 <kmalloc_oob_right+80>: ret

我们关心gcc为我们的插桩,熟悉调用约定aapcs的可以知道如下是插桩代码

0xffffffd008c38c1c <kmalloc_oob_right+44>: mov x19, x0 0xffffffd008c38c20 <kmalloc_oob_right+48>: add x0, x0, #0x7b 0xffffffd008c38c24 <kmalloc_oob_right+52>: bl 0xffffffd0083ee2f4 <__asan_store1> 0xffffffd008c38c28 <kmalloc_oob_right+56>: mov w1, #0x78 // #120 0xffffffd008c38c2c <kmalloc_oob_right+60>: mov x0, x19

可以看到,这里为__asan_store1构造参数之后直接就调用了此函数了。

根据之前的分析可以知道,inline是直接将汇编片段插入到待调试函数中,而outline是将函授钩子插入到待调试函数中,这个钩子由gcc插入,但由kernel实现,所以我们从内核查看其实现。内核实现位置如下

void __asan_store##size(unsigned long addr) \ { \ check_region_inline(addr, size, true, _RET_IP_); \ } \ EXPORT_SYMBOL(__asan_store##size); \ __alias(__asan_store##size) \

其实这个代码很明显了,我们关注check_region_inline的实现,代码如下

162 static __always_inline bool check_region_inline(unsigned long addr, 163 size_t size, bool write, 164 unsigned long ret_ip) 165 { 166 if (unlikely(size == 0)) 167 return true; 168 169 if (unlikely(addr + size < addr)) 170 return !kasan_report(addr, size, write, ret_ip); 171 172 if (unlikely((void *)addr < 173 kasan_shadow_to_mem((void *)KASAN_SHADOW_START))) { 174 return !kasan_report(addr, size, write, ret_ip); 175 } 176 177 if (likely(!memory_is_poisoned(addr, size))) 178 return true; 179 180 return !kasan_report(addr, size, write, ret_ip); 181 }
  • 166-167: 传入的size不能为0
  • 169-170: 传入的size不能小于0
  • 172-175: 传入的地址应该在影子区域包含的内存地址范围内
  • 177-178: 传入的地址和大小是否触碰到下毒区域
  • 180: 所有情况不应该都不存在,上报问题

补充一下,这里kasan_mem_to_shadow的函数就是之前asan提到的影子内存到物理内存的转换,这里再重复一遍,如下图

image.png

继续分析,可以看到,此函数是完全防御式的,真正的行为在memory_is_poisoned上,我们关注此函数,其实现如下

142 static __always_inline bool memory_is_poisoned(unsigned long addr, size_t size) 143 { 144 if (__builtin_constant_p(size)) { 145 switch (size) { 146 case 1: 147 return memory_is_poisoned_1(addr); 148 case 2: 149 case 4: 150 case 8: 151 return memory_is_poisoned_2_4_8(addr, size); 152 case 16: 153 return memory_is_poisoned_16(addr); 154 default: 155 BUILD_BUG(); 156 } 157 } 158 159 return memory_is_poisoned_n(addr, size); 160 }

我们先看memory_is_poisoned_1的实现如下

43 static __always_inline bool memory_is_poisoned_1(unsigned long addr) 44 { 45 s8 shadow_value = *(s8 *)kasan_mem_to_shadow((void *)addr); 46 47 if (unlikely(shadow_value)) { 48 s8 last_accessible_byte = addr & KASAN_GRANULE_MASK; 49 return unlikely(last_accessible_byte >= shadow_value); 50 } 51 52 return false; 53 }

这里计算了影子区域的值,如果值是0,或者值是1到7内,那么正常访问,如果大于下毒的值,就代表oob了。这里和inline分析的汇编行为一致。

55 static __always_inline bool memory_is_poisoned_2_4_8(unsigned long addr, 56 unsigned long size) 57 { 58 u8 *shadow_addr = (u8 *)kasan_mem_to_shadow((void *)addr); 59 60 /* 61 * Access crosses 8(shadow size)-byte boundary. Such access maps 62 * into 2 shadow bytes, so we need to check them both. 63 */ 64 if (unlikely(((addr + size - 1) & KASAN_GRANULE_MASK) < size - 1)) 65 return *shadow_addr || memory_is_poisoned_1(addr + size - 1); 66 67 return memory_is_poisoned_1(addr + size - 1); 68 }

memory_is_poisoned_2_4_8主要考虑了跨影子内存的情况,但是都是利用memory_is_poisoned_1来测试oob的情况

70 static __always_inline bool memory_is_poisoned_16(unsigned long addr) 71 { 72 u16 *shadow_addr = (u16 *)kasan_mem_to_shadow((void *)addr); 73 74 /* Unaligned 16-bytes access maps into 3 shadow bytes. */ 75 if (unlikely(!IS_ALIGNED(addr, KASAN_GRANULE_SIZE))) 76 return *shadow_addr || memory_is_poisoned_1(addr + 15); 77 78 return *shadow_addr; 79 }

memory_is_poisoned_16如果不跨边界,那么直接判断其值是否为0即可,如果跨边界,那么计算边界上的影子值即可。

123 static __always_inline bool memory_is_poisoned_n(unsigned long addr, 124 size_t size) 125 { 126 unsigned long ret; 127 128 ret = memory_is_nonzero(kasan_mem_to_shadow((void *)addr), 129 kasan_mem_to_shadow((void *)addr + size - 1) + 1); 130 131 if (unlikely(ret)) { 132 unsigned long last_byte = addr + size - 1; 133 s8 *last_shadow = (s8 *)kasan_mem_to_shadow((void *)last_byte); 134 135 if (unlikely(ret != (unsigned long)last_shadow || 136 ((long)(last_byte & KASAN_GRANULE_MASK) >= *last_shadow))) 137 return true; 138 } 139 return false; 140 }

如果都是0,那么就直接返回了,如果存在非零的情况,并且非零的情况不是最后或者最后一个字节位置大于影子值,那么肯定存在oob的情况,此时返回true。

到这里,outline的代码分析完毕了,回到测试中的问题,这里size传入是1,所以之间判断1的情况即可。

总结

到这里,我们通过outline的方式分析了kasan检测oob的方法,其流程如下

  1. gcc内部对load/store进行插桩,和inline不一样的是这次插桩是函数插桩
  2. 因为是函数插桩,所以需要内核实现函数
  3. 函数需要实现对影子内存的判断,判断是否命中下毒区域

可以看到,使用inline的方式,简单小巧精炼,而使用outline的方式,设计通用,利用堆栈所以使得二进制大小更小。
而我们实际调试过程中,二进制体积反而不重要,而少一次函数的调用,之间利用汇编插入的方式性能更高。

所以这也是推荐使用inline而不是使用outline的原因。

最终,《KASAN(4)-INLINE插桩分析》和《KASAN(5)-OUTLINE插桩分析》的两个分析给出了INLINE和OUTLINE的区别,以及从实践的角度说明了INLINE的OUTLINE的优劣势,以及为什么内核默认推荐使用INLINE。