如何使用qemu玩转KASAN

1,434 阅读10分钟

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 架构。

基础知识:

2、搭建qemu环境,调试linux内核

不熟悉如何搭建qemu环境的话,可参考:virtualbox + ubuntu + qemu + busybox + gdb 调试linux内核

3、使能内核KASAN配置

#打开内核配置页面 
cd ~/study/linux-4.19.157/ 
make menuconfig
  1. 按如下路径选择,并选中KASan: runtime memory debugger Kernel hacking -> Memory debugging -> KASan: runtime memory debugger

  2. 在KASan: runtime memory debugger中选中KASan: extra checks

  3. 在Instrumentation type中选择Outline Instrumentation,从CONFIG_KASAN_OUTLINE和CONFIG_KASAN_INLINE之间选择,前者产生较小的二进制文件,后者要快1.1~2倍。

  4. 打开Module for testing kasan for bug detection,用于在~/study/linux-4.19.157/lib中生成测试用的c文件,编译后可生成ko文件。

1646496840.png

1646496843.png

1646496846.png

同时,也可以打开SLUB_DEBUG,因为有段时间KASAN是依赖SLUB_DEBUG的,就是在Kconfig中使用了depends on。不过最新的代码已经不需要依赖了,可以看下提交。但是我建议你打开该选项,因为log可以输出更多有用的信息。

1646496928.png

1646496930.png

可查看.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 1111  2020 test_kasan.c
-rw-rw-r-- 1 idle idle 300112 35 14:05 test_kasan.ko
-rw-rw-r-- 1 idle idle    613 35 13:52 test_kasan.mod.c
-rw-rw-r-- 1 idle idle  74256 35 14:01 test_kasan.mod.o
-rw-rw-r-- 1 idle idle 227304 35 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 ./

1646497084.png

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

使用上述命令会出现如下警告,然后卡住不能继续运行:

1646497185.png

此时只能打开另一个terminal,kill掉qemu-system-x86_64这个进程:

ps -aux | grep qemu 
kill -9 xxxx

1646497187.png

出现这个问题的原因是: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文件。

1646497190.png

#运行ko文件 
insmod test_kasan.ko

接着会打印大量的KASAN检测的report,截取一部分如下。关于KASAN report的报告解析,可查看文章:KASAN实现原理

test_kasan源码与log文件

/ # 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

内核空间虚拟内存

08 bytes内存都可以访问
N(1 =< N <= 7)连续N(1 =< N <= 7) bytes可以访问
负数8 bytes内存访问都是invalid
0xFFpage was freed
0xFEredzone for kmalloc_large allocations
0xFCredzone inside slub object
第一次创建slab缓存池的时候,整个slab对应的shadow memory都填充0xFC
0xFBobject was freed (kmem_cache_free/kfree)
0xFAredzone 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);
}

9、参考文档:

Linux内存管理内存检测技术(slub_debug/kmemleak/kasan)