1、什么是KASAN
KASAN 即Kernel Address SANitizer,是一种动态内存错误检测工具,旨在发现内存越界、释放后使用、重复释放以及栈溢出等错误。 KASAN是内核分析内存问题强有力的工具。
KASAN 进行内存检测是在代码编译时,通过在内存访问代码之前插入有效性检查,因此需要特定的编译器版本。对于 GCC,它需要 4.9.2 或更高版本来提供基本支持,需要 5.0 或更高版本来检测堆栈和全局变量的越界访问以及内联检测模式。使用 Clang,它需要 7.0.0 或更高版本,并且还不支持检测全局变量的越界访问。
目前通用 KASAN 支持 x86_64、arm64(linux 4.4版本合入)、xtensa、s390 和 riscv 架构。
基础知识:
- 伙伴系统buddy基础知识:buddy 子系统基本原理
- slub基础知识:图解slub
- KASAN检测的原理可参考:KASAN实现原理
2、搭建qemu环境,调试linux内核
不熟悉如何搭建qemu环境的话,可参考:virtualbox + ubuntu + qemu + busybox + gdb 调试linux内核
3、使能内核KASAN配置
#打开内核配置页面
cd ~/study/linux-4.19.157/
make menuconfig
-
按如下路径选择,并选中KASan: runtime memory debugger
Kernel hacking -> Memory debugging -> KASan: runtime memory debugger -
在KASan: runtime memory debugger中选中KASan: extra checks
-
在Instrumentation type中选择Outline Instrumentation,从CONFIG_KASAN_OUTLINE和CONFIG_KASAN_INLINE之间选择,前者产生较小的二进制文件,后者要快1.1~2倍。
-
打开Module for testing kasan for bug detection,用于在~/study/linux-4.19.157/lib中生成测试用的c文件,编译后可生成ko文件。
同时,也可以打开SLUB_DEBUG,因为有段时间KASAN是依赖SLUB_DEBUG的,就是在Kconfig中使用了depends on。不过最新的代码已经不需要依赖了,可以看下提交。但是我建议你打开该选项,因为log可以输出更多有用的信息。
可查看.config文件中对应的配置是否均打开:
CONFIG_SLUB_DEBUG=y
CONFIG_SLUB=y
CONFIG_SLUB_DEBUG_ON=y
CONFIG_HAVE_ARCH_KASAN=y
CONFIG_KASAN=y
CONFIG_KASAN_EXTRA=y
CONFIG_KASAN_OUTLINE=y
CONFIG_TEST_KASAN=m
4、编译内核
# 编译成功后,源码根目录下会生成带调试信息的 vmlinux 文件,
# 内核文件在 arch/x86/boot 目录下,文件名为 bzImage,即 vmlinuz。
make -j4
#查找生成的test_kasan.ko文件,用于测试,源码在test_kasan.c
idle@linux:~/study/linux-4.19.157/lib$ ls -l test_kasan*
-rw-rw-r-- 1 idle idle 12587 11月 11 2020 test_kasan.c
-rw-rw-r-- 1 idle idle 300112 3月 5 14:05 test_kasan.ko
-rw-rw-r-- 1 idle idle 613 3月 5 13:52 test_kasan.mod.c
-rw-rw-r-- 1 idle idle 74256 3月 5 14:01 test_kasan.mod.o
-rw-rw-r-- 1 idle idle 227304 3月 5 12:16 test_kasan.o
5、制作内核启动的文件系统initrd-busybox.img
#用编译出来的test_kasan.ko制作内核启动的文件系统initrd-busybox.img
cd ~/study/rootfs
cp ~/study/linux-4.19.157/lib/test_kasan.ko ./
find . -print0 | cpio --null -ov --format=newc | pigz -9 > ../initrd-busybox.img
上述命令执行成功后,会在 ~/study/ 目录下生成新的 initrd-busybox.img,即 initrd。不熟悉的话可阅读:4.编译busybox
6、启动qemu
这次使用qemu-system-x86_64启动,可不带-S -s选项,即不使用gdb调试,直接启动。
#输入如下命令启动qemu
#-m:指定RAM大小(默认128)
#-kernel:指定的内核镜像
#-initrd:设置刚刚利用 busybox 创建的initrd-busybox.img,作为内核启动的文件系统
#-append:附加选项,指定no kaslr可以关闭随机偏移
#--nographic和console=ttyS0一起使用,启动的界面就变成当前终端
#-s:相当于-gdb tcp::1234的简写,可以直接通过主机的gdb远程连接
#-S 启动gdb调试,在gdb中使用'c'开始执行
qemu-system-x86_64 -kernel ~/study/linux-4.19.157/arch/x86/boot/bzImage -initrd ~/study/initrd-busybox.img -append "console=ttyS0 nokaslr" -nographic
使用上述命令会出现如下警告,然后卡住不能继续运行:
此时只能打开另一个terminal,kill掉qemu-system-x86_64这个进程:
ps -aux | grep qemu
kill -9 xxxx
出现这个问题的原因是:qemu 分配的默认 RAM 是 128M,此时是要大量的RAM来运行,我尝试设置1700才开始正常运行。
# -m 1700表示使用1700M RAM
qemu-system-x86_64 -m 1700 -kernel ~/study/linux-4.19.157/arch/x86/boot/bzImage -initrd ~/study/initrd-busybox.img -append "console=ttyS0 nokaslr" -nographic
7、运行test_kasan.ko
qemu启动成功后,在根目录下会出现我们拷贝过来的test_kasan.ko文件。
#运行ko文件
insmod test_kasan.ko
接着会打印大量的KASAN检测的report,截取一部分如下。关于KASAN report的报告解析,可查看文章:KASAN实现原理。
/ # insmod test_kasan.ko
[ 20.319090] test_kasan: module verification failed: signature and/or required key missing - tainting kernel
//发生越界访问位置
[ 20.347103] kasan test: kmalloc_oob_right out-of-bounds to right
[ 20.348189] ==================================================================
//越界写1个字节,写的地址是0xffff888047231623,当前进程是comm是insmod,pid是128
[ 20.349479] BUG: KASAN: slab-out-of-bounds in kmalloc_oob_right+0x99/0xa8 [test_kasan]
[ 20.350690] Write of size 1 at addr ffff888047231623 by task insmod/128
[ 20.351534]
[ 20.352020] CPU: 0 PID: 128 Comm: insmod Tainted: G E 4.19.157 #2
[ 20.356108] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS Ubuntu-1.8.2-1ubuntu1 04/01/2014
//kasan_report的Call trace,方便定位出问题的函数调用关系
[ 20.357552] Call Trace:
[ 20.358066] dump_stack+0x96/0xd5
[ 20.358572] print_address_description+0x70/0x290
[ 20.359207] kasan_report+0x291/0x390
[ 20.359736] ? kmalloc_oob_right+0x99/0xa8 [test_kasan]
[ 20.360476] __asan_report_store1_noabort+0x1c/0x20
[ 20.361173] kmalloc_oob_right+0x99/0xa8 [test_kasan]
[ 20.361886] ? kmalloc_pagealloc_uaf+0x5e/0x7b [test_kasan]
[ 20.362252] kmalloc_tests_init+0x11/0x90a [test_kasan]
[ 20.362577] ? kmalloc_pagealloc_oob_right+0x89/0x89 [test_kasan]
[ 20.363047] do_one_initcall+0xb6/0x33f
[ 20.363391] ? perf_trace_initcall_level+0x450/0x450
[ 20.365348] ? kasan_unpoison_shadow+0x36/0x50
[ 20.365647] ? kasan_kmalloc+0xad/0xe0
[ 20.365801] ? kasan_unpoison_shadow+0x36/0x50
[ 20.366222] ? __asan_register_globals+0x87/0xa0
[ 20.366822] do_init_module+0x1d7/0x5d1
[ 20.367142] load_module+0x6dc2/0x9050
[ 20.367450] ? module_frob_arch_sections+0x20/0x20
[ 20.369608] __do_sys_init_module+0x246/0x270
[ 20.369958] ? __do_sys_init_module+0x246/0x270
[ 20.370331] ? load_module+0x9050/0x9050
[ 20.370623] __x64_sys_init_module+0x73/0xb0
[ 20.370943] do_syscall_64+0xa5/0x2a0
[ 20.371227] ? page_fault+0x8/0x30
[ 20.371489] entry_SYSCALL_64_after_hwframe+0x44/0xa9
[ 20.371945] RIP: 0033:0x4c2599
[ 20.372649] Code: 00 f3 c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 40 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 0f 83 fb 66 01 00 c3 66 2e 0f 1f 84 00 00 00 00
[ 20.377448] RSP: 002b:00007ffff3d0f708 EFLAGS: 00000246 ORIG_RAX: 00000000000000af
[ 20.381262] RAX: ffffffffffffffda RBX: 00007ffff3d0fab0 RCX: 00000000004c2599
[ 20.381588] RDX: 0000000000685bdb RSI: 0000000000049450 RDI: 00007f18ea847010
[ 20.382237] RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000001
[ 20.382713] R10: 000000000202a8f0 R11: 0000000000000246 R12: 00007ffff3d0fab8
[ 20.383221] R13: 0000000000685bdb R14: 0000000000000000 R15: 0000000000000000
[ 20.383733]
//该object分配的调用栈,并指出分配内存的进程pid是128
[ 20.384003] Allocated by task 128:
[ 20.384439] save_stack+0x46/0xd0
[ 20.384813] kasan_kmalloc+0xad/0xe0
[ 20.385073] kmem_cache_alloc_trace+0x10c/0x210
[ 20.385362] kmalloc_oob_right+0x51/0xa8 [test_kasan]
[ 20.385677] kmalloc_tests_init+0x11/0x90a [test_kasan]
[ 20.385998] do_one_initcall+0xb6/0x33f
[ 20.386278] do_init_module+0x1d7/0x5d1
[ 20.386537] load_module+0x6dc2/0x9050
[ 20.386840] __do_sys_init_module+0x246/0x270
[ 20.387134] __x64_sys_init_module+0x73/0xb0
[ 20.387414] do_syscall_64+0xa5/0x2a0
[ 20.387708] entry_SYSCALL_64_after_hwframe+0x44/0xa9
[ 20.388069]
//上次释放该object的调用栈,并指出释放该内存的进程pid是0,stack看不到
[ 20.393159] Freed by task 0:
[ 20.393370] (stack is not available)
[ 20.393653]
//指出slub相关的信息,从“kmalloc-128”的kmem_cache分配的object。
//object起始地址是0xffff8880472315a8
//访问出问题的地址位于object起始地址偏移123 bytes的位置
[ 20.393790] The buggy address belongs to the object at ffff8880472315a8
[ 20.393790] which belongs to the cache kmalloc-128 of size 128
[ 20.394554] The buggy address is located 123 bytes inside of
[ 20.394554] 128-byte region [ffff8880472315a8, ffff888047231628)
[ 20.395234] The buggy address belongs to the page:
[ 20.395664] page:ffffea00011c8c40 count:1 mapcount:0 mapping:ffff88804b403240 index:0xffff8880472313c8
[ 20.396447] flags: 0xfffffc0000100(slab)
[ 20.397001] raw: 000fffffc0000100 ffff88804b400850 ffff88804b400850 ffff88804b403240
[ 20.397665] raw: ffff8880472313c8 0000000000080007 00000001ffffffff 0000000000000000
[ 20.398120] page dumped because: kasan: bad access detected
[ 20.398613]
//出问题地址对应的shadow memory的值,可以确定申请内存的实际大小是123 bytes。
[ 20.398788] Memory state around the buggy address:
[ 20.399236] ffff888047231500: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[ 20.399887] ffff888047231580: fc fc fc fc fc 00 00 00 00 00 00 00 00 00 00 00
[ 20.400386] >ffff888047231600: 00 00 00 00 03 fc fc fc fc fc fc fc fc fc fc fc
[ 20.400882] ^
[ 20.401195] ffff888047231680: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[ 20.401653] ffff888047231700: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc f
8、KASAN shadow memory各数值的含义
KASAN的原理是利用额外的内存标记可用内存的状态。这部分额外的内存被称作shadow memory(影子区)。KASAN将内核空间虚拟地址的1/8内存用作shadow memory。内核空间虚拟地址中连续8 bytes内存(8 bytes align)使用1 byte 特殊的magic num在shadow memory进行标记。那么这些特殊的magic num分别代表什么含义呢?
| shadow memory的magic num | 内核空间虚拟内存 |
|---|---|
| 0 | 8 bytes内存都可以访问 |
| N(1 =< N <= 7) | 连续N(1 =< N <= 7) bytes可以访问 |
| 负数 | 8 bytes内存访问都是invalid |
| 0xFF | page was freed |
| 0xFE | redzone for kmalloc_large allocations |
| 0xFC | redzone inside slub object 第一次创建slab缓存池的时候,整个slab对应的shadow memory都填充0xFC |
| 0xFB | object was freed (kmem_cache_free/kfree) |
| 0xFA | redzone for global variable |
例如上述log:
[ 20.398788] Memory state around the buggy address:
[ 20.399236] ffff888047231500: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[ 20.399887] ffff888047231580: fc fc fc fc fc 00 00 00 00 00 00 00 00 00 00 00
[ 20.400386] >ffff888047231600: 00 00 00 00 03 fc fc fc fc fc fc fc fc fc fc fc
[ 20.400882] ^
[ 20.401195] ffff888047231680: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[ 20.401653] ffff888047231700: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
- fc表示redzone区,不能访问;
- 00表示可以访问,总共有15个00表示15*8=120个byte可以访问;
- 03表示接下来3gebytes可以访问。
则总共有123个byte可以访问。实际源码在~/study/linux-4.19.157/lib/test_kasan.c中,的确是分配了123bytes的空间,却访问了ptr[123] = 'x',导致访问越界。
static noinline void __init kmalloc_oob_right(void)
{
char *ptr;
size_t size = 123;
pr_info("out-of-bounds to right\n");
ptr = kmalloc(size, GFP_KERNEL);
if (!ptr) {
pr_err("Allocation failed\n");
return;
}
ptr[size] = 'x';
kfree(ptr);
}