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

目录

全局变量的实现原理
测试代码
全局变量下毒
栈区变量实现原理
局部变量下毒
总结
参考文档

kasan也能够定位全局变量和栈区局部变量的oob问题,本文分析kasan如何实现全局变量和栈区变量的oob问题

全局变量的实现原理

关于kasan如何实现全局变量定位oob问题,我借助ppt的一页内容,如下图

image.png

可以看到,默认情况下,对于全局变量,通过编译器插入构造器的方式插入的红区。这个不禁让我想起了《使用ASAN定位全局变量构造顺序问题》,关于全局变量构造顺序,可以转而了解此文章。本文主要说明kasan在内核上检测全局变量问题。

通过ppt的内容,我可以大概了解到其检测步骤如下

  1. 为全局变量生成构造函数
  2. 为全局变量按照32字节对齐
  3. 多出来的字节用来下毒红区
  4. 当代码触碰影子红区则报错

测试代码

为了演示此问题,我又贴上了一份检测全局变量的代码如下

static char global_array[10]; static noinline void kasan_global_oob(void) { char *volatile array = global_array; char *p = &array[ARRAY_SIZE(global_array) + 3]; *(volatile char *)p; return ; }

可以看到,这里申请了10字节的全局变量,而越界的地方是第13个字节,所以相关日志如下

[ 10.447271] Memory state around the buggy address: [ 10.447326] ffffffd00ea7ca00: 04 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 [ 10.447382] ffffffd00ea7ca80: f9 f9 f9 f9 00 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 [ 10.447438] >ffffffd00ea7cb00: f9 f9 f9 f9 f9 f9 f9 f9 00 02 f9 f9 f9 f9 f9 f9 [ 10.447484] ^ [ 10.447539] ffffffd00ea7cb80: 00 f9 f9 f9 f9 f9 f9 f9 00 00 00 00 f9 f9 f9 f9 [ 10.447595] ffffffd00ea7cc00: 00 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 00 00 00 00

因为ppt中提到是编译器的指令插桩,根据c语言全局变量的构造概念,我们可以反汇编o文件,从o文件中找答案(参考 《使用ASAN定位全局变量构造顺序问题》),结果如下

# objdump -d lib/test_kasan_kylin.o Disassembly of section .text.exit: 0000000000000000 <_sub_D_65535_0>: 0: a9bf7bfd stp x29, x30, [sp, #-16]! 4: 90000000 adrp x0, 0 <_sub_D_65535_0> 8: 91000000 add x0, x0, #0x0 c: 910003fd mov x29, sp 10: 91004000 add x0, x0, #0x10 14: d2800041 mov x1, #0x2 // #2 18: 94000000 bl 0 <__asan_unregister_globals> 1c: a8c17bfd ldp x29, x30, [sp], #16 20: d65f03c0 ret Disassembly of section .text.startup: 0000000000000000 <_sub_I_65535_1>: 0: a9bf7bfd stp x29, x30, [sp, #-16]! 4: 90000000 adrp x0, 0 <_sub_I_65535_1> 8: 91000000 add x0, x0, #0x0 c: 910003fd mov x29, sp 10: 91004000 add x0, x0, #0x10 14: d2800041 mov x1, #0x2 // #2 18: 94000000 bl 0 <__asan_register_globals> 1c: a8c17bfd ldp x29, x30, [sp], #16 20: d65f03c0 ret

可以看到,和标准c语言的全局变量一致,默认会在o文件生成构造函数,其构造调用__asan_register_globals,析构调用__asan_unregister_globals,我们翻阅一下内核代码,分析一下

全局变量下毒

首先分析代码流程如下

__asan_register_globals register_global

这里重点函数是register_global,代码如下

static void register_global(struct kasan_global *global) { size_t aligned_size = round_up(global->size, KASAN_GRANULE_SIZE); kasan_unpoison(global->beg, global->size, false); kasan_poison(global->beg + aligned_size, global->size_with_redzone - aligned_size, KASAN_GLOBAL_REDZONE, false); }

这个函数有如下几个重要信息

  • 被编译器生成的结构体struct kasan_global
  • 先对global->beg到global->beg+global->size区域解毒为0
  • 再将global->beg + aligned_size到global->size_with_redzone - aligned_size下毒为0xf9

所以一切重要的事情都在于结构体struct kasan_global,翻阅代码,其定义如下

struct kasan_global { const void *beg; /* Address of the beginning of the global variable. */ size_t size; /* Size of the global variable. */ size_t size_with_redzone; /* Size of the variable + size of the red zone. 32 bytes aligned */ const void *name; const void *module_name; /* Name of the module where the global variable is declared. */ unsigned long has_dynamic_init; /* This needed for C++ */ #if KASAN_ABI_VERSION >= 4 struct kasan_source_location *location; #endif #if KASAN_ABI_VERSION >= 5 char *odr_indicator; #endif };

这个注释还是非常详细的,我逐个解释一下

  1. beg: 全局变量地址
  2. size: 全局变量大小
  3. size_with_redzone: 加上红区的大小
  4. name: 全局变量名字
  5. module_name: 对于文件名字
  6. location: 结构体kasan_source_location指针

这里还需要kasan_source_location结构体,其定义如下

struct kasan_source_location { const char *filename; int line_no; int column_no; };

这个更显而易见了,如下

  1. 文件名
  2. 行号
  3. 列号

再根据kasan的报错堆栈,我们知道这个全局变量的地址是ffffffd00ea7cb40.

结合上面所有的信息,那么可以在编译器插桩的函数上加上一句打印,从而打印这个kasan_global结构体指针,打印如下

# git diff mm/kasan/generic.c diff --git a/mm/kasan/generic.c b/mm/kasan/generic.c index 53cbf28859b5..2a7e4d8ac0f7 100644 --- a/mm/kasan/generic.c +++ b/mm/kasan/generic.c @@ -219,6 +219,10 @@ void __asan_register_globals(struct kasan_global *globals, size_t size) { int i; + if(globals->beg == 0xffffffd00ea7cb40) { + printk("tf: kasan_global=%px \n", globals); + } + for (i = 0; i < size; i++) register_global(&globals[i]); }

此时重编译内核,运行得到日志如下

[ 6.335641] tf: kasan_global= ffffffd00e0b60e0

我们拿到了这个kasan_global结构体指针,借助crash可以打印信息如下

crash> struct kasan_global 0xffffffd00e0b60e0 struct kasan_global { beg = 0xffffffd00ea7cb40 <global_array>, size = 10, size_with_redzone = 64, name = 0xffffffd00c607110, module_name = 0xffffffd00c6070f8, has_dynamic_init = 0, location = 0xffffffd00e0b60d0, odr_indicator = 0x0 }

再打印location,如下

crash> struct kasan_source_location 0xffffffd00e0b60d0 struct kasan_source_location { filename = 0xffffffd00c6070f8 "lib/test_kasan_kylin.c", line_no = 16, column_no = 13 }

栈区变量实现原理

这里还是根据ppt的文档解析,如下

image.png

根据文档,可以知道默认栈分配的变量,编译器在编译的时候会直接插入红区变量,然后根据整体栈的布局设置红区内容。

所以根据ppt的内容,这部分工作量全部在编译器,无需代码实现。

局部变量下毒

我们知道局部变量都是编译器做的,那么解析这个步骤就可以直接反汇编即可,这里还是利用上面的测试代码,反汇编kasan_global_oob函数,如下

crash> dis kasan_global_oob 0xffffffd0094d0de0 <kasan_global_oob>: stp x29, x30, [sp,#-96]! 0xffffffd0094d0de4 <kasan_global_oob+4>: mov x1, #0xffd000000000 // #281268818280448 0xffffffd0094d0de8 <kasan_global_oob+8>: movk x1, #0xdfff, lsl #48 0xffffffd0094d0dec <kasan_global_oob+12>: add x0, sp, #0x20 0xffffffd0094d0df0 <kasan_global_oob+16>: mov x29, sp 0xffffffd0094d0df4 <kasan_global_oob+20>: mov x5, #0x8ab3 // #35507 0xffffffd0094d0df8 <kasan_global_oob+24>: stp x19, x20, [sp,#16] 0xffffffd0094d0dfc <kasan_global_oob+28>: lsr x19, x0, #3 0xffffffd0094d0e00 <kasan_global_oob+32>: add x4, x19, x1 0xffffffd0094d0e04 <kasan_global_oob+36>: adrp x3, 0xffffffd00c607000 0xffffffd0094d0e08 <kasan_global_oob+40>: adrp x2, 0xffffffd0094d0000 < kstrtos16_from_user+416> 0xffffffd0094d0e0c <kasan_global_oob+44>: add x3, x3, #0xe0 0xffffffd0094d0e10 <kasan_global_oob+48>: add x2, x2, #0xde0 0xffffffd0094d0e14 <kasan_global_oob+52>: movk x5, #0x41b5, lsl #16 0xffffffd0094d0e18 <kasan_global_oob+56>: stp x5, x3, [sp,#32] 0xffffffd0094d0e1c <kasan_global_oob+60>: mov w3, #0xf1f1f1f1 // #-235802127 0xffffffd0094d0e20 <kasan_global_oob+64>: str x2, [sp,#48] 0xffffffd0094d0e24 <kasan_global_oob+68>: mov w2, #0xf300 // #62208 0xffffffd0094d0e28 <kasan_global_oob+72>: str w3, [x19,x1] 0xffffffd0094d0e2c <kasan_global_oob+76>: movk w2, #0xf3f3, lsl #16 0xffffffd0094d0e30 <kasan_global_oob+80>: str w2, [x4,#4] 0xffffffd0094d0e34 <kasan_global_oob+84>: adrp x0, 0xffffffd00ea7c000 < bounce_bio_set+192> 0xffffffd0094d0e38 <kasan_global_oob+88>: add x0, x0, #0xb40 0xffffffd0094d0e3c <kasan_global_oob+92>: str x0, [sp,#64] 0xffffffd0094d0e40 <kasan_global_oob+96>: ldr x20, [sp,#64] 0xffffffd0094d0e44 <kasan_global_oob+100>: add x0, x20, #0xd 0xffffffd0094d0e48 <kasan_global_oob+104>: and w2, w0, #0x7 0xffffffd0094d0e4c <kasan_global_oob+108>: lsr x3, x0, #3 0xffffffd0094d0e50 <kasan_global_oob+112>: ldrsb w1, [x3,x1] 0xffffffd0094d0e54 <kasan_global_oob+116>: cmp w1, #0x0 0xffffffd0094d0e58 <kasan_global_oob+120>: ccmp w2, w1, #0x1, ne 0xffffffd0094d0e5c <kasan_global_oob+124>: b.ge 0xffffffd0094d0e7c <kasa n_global_oob+156> 0xffffffd0094d0e60 <kasan_global_oob+128>: mov x0, #0xffd000000000 // #281268818280448 0xffffffd0094d0e64 <kasan_global_oob+132>: ldrb w1, [x20,#13] 0xffffffd0094d0e68 <kasan_global_oob+136>: movk x0, #0xdfff, lsl #48 0xffffffd0094d0e6c <kasan_global_oob+140>: str xzr, [x19,x0] 0xffffffd0094d0e70 <kasan_global_oob+144>: ldp x19, x20, [sp,#16] 0xffffffd0094d0e74 <kasan_global_oob+148>: ldp x29, x30, [sp],#96 0xffffffd0094d0e78 <kasan_global_oob+152>: ret 0xffffffd0094d0e7c <kasan_global_oob+156>: bl 0xffffffd0086579c0 <__as an_report_load1_noabort> 0xffffffd0094d0e80 <kasan_global_oob+160>: b 0xffffffd0094d0e60 <kasa n_global_oob+128>

这里关于load/store插桩的汇编我们可以跳过分析,已经在《KASAN(4)-INLINE插桩分析》介绍过了,我们只关心栈区如何设置红区的。主要步骤如下

  1. 整个栈大小96字节
  2. 设置shadow offset
  3. 分配32字节作为栈左
  4. 获取影子内存地址
  5. 下毒栈左影子值
  6. 下毒可访问区
  7. 下毒栈右

上面关于汇编的分析同时也忽略了全局变量判断oob的指令。可以看到,通过汇编可以知道其在栈上的前32字节下毒了0xf1,中间8字节是可访问区域,栈后面24字节作为栈右下毒0xf3。组合起来的值是f1 f1 f1 f1 00 f3 f3 f3

总结

到这里,全局变量和局部变量的下毒流程已经分析完毕了。

对于全局变量,其依赖gcc给代码插桩,默认让所有的全局变量执行构造函数,而这个构造函数固定名字为__asan_register_globals,同时初始化了结构体kasan_global用于传递这个全局变量的基本信息。在内核中需要实现__asan_register_globals这个钩子,并正常给影子内存下毒。当下次load/store访问变量的时候,继续由插桩指令判断影子内存。从而判断访问是否oob。

对于局部变量,其全部来自gcc插桩,对栈区访问的变量进行栈左和栈右的预留栈区和下毒。当下次load/store访问变量的时候,继续由插桩指令判断影子内存。从而判断访问是否oob。

参考文档

谷歌三篇关于asan的文章讲解还是比较权威的,可以阅读一下

https://docs.google.com/presentation/d/10V_msbtEap9dNerKvTrRAzvfzYdrQFC8e2NYHCZYJDE/edit?slide=id.g8b31c59a9b_0_0#slide=id.g8b31c59a9b_0_0
https://docs.google.com/presentation/d/1IpICtHR1T3oHka858cx1dSNRu2XcT79-RCRPgzCuiRk/edit?slide=id.gee201659dd_0_308#slide=id.gee201659dd_0_308
https://docs.google.com/presentation/d/1qA8fqRDHKX_WM_ZdDN37EQQZwSTNJ4FFws82tbUSKxY/edit?pli=1&slide=id.g154fcb2683b_1_151#slide=id.g154fcb2683b_1_151