1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 原理

Linux 内核划出 32TB 虚拟地址区间作为分配内存的 shadow 区域,以实现 KASAN 功能。

354a8d6b-081d-406d-b580-c501102bd49e

KASAN 基本原理,首先是通过 1/8 虚拟内存(上图中的 32TB kasan 区),也称 shadow memory,对系统虚拟地址空间进行标记:每连续的 8 字节,对应一个 shadow memory 字节,如果该 shadow memory 字节值为 0,则表示连续的 8 字节可以访问,shadow memory 字节值 1~7 则表示有 1~7 字节可以访问,如下图所示(图片借自博客 https://blog.csdn.net/gy794627991/article/details/132139699):

519aa607-1596-49ab-aba5-1e26d6c1bd15

然后通过编译器选项 -fsanitize=kernel-address这对编译器的版本有要求),指示编译器在所有读写访问处插入 __asan_loadN__asan_storeN 调用,然后内核实现这些接口,检查对应读写地址的 shadow memory 字节,看是否可以访问,不能访问则报 BUG。

值得一提的是,编译器插入 __asan_{load,store}N 是基于默认的 CONFIG_KASAN_OUTLINE=y 配置,Linux 内核提供 CONFIG_KASAN_INLINE=y 配置项,此时编译器不会插入 __asan_{load,store}N 调用,而是直接插入内存 shadow 标记检测代码。本文讨论的是基于默认的 CONFIG_KASAN_OUTLINE=y 配置的情形。

3. 实现

本文以 ARM64 + Linux 4.14.111 分析 Linux 内核的 KASAN 功能。通过配置项 CONFIG_KASAN=y 开启 Linux 内核的 KASAN 功能。

ARM64 架构下,对 KASAN 的具体实现,本文主要讨论 2 个核心要点:

  • KASAN shadow 内存的分配
  • KASAN 对内存分配时标记和访问时检测

3.1 KASAN shadow 内存的分配

在 Linux 内核启动阶段,首先通过 kasan_early_init()整个 KASAN shadow 内存虚拟地址区间 [KASAN_SHADOW_START,KASAN_SHADOW_END] 映射到同一页面 kasan_zero_page[PAGE_SIZE],然后在 kasan_init() 为支持 KASAN 功能虚拟地址区间的分配 shadow 物理内存

void __init kasan_init(void)
{
	u64 kimg_shadow_start, kimg_shadow_end;
	u64 mod_shadow_start, mod_shadow_end;
	struct memblock_region *reg;
	int i;

	/* 计算 内核镜像区间 的 kasan shadow memory 的范围 */
	kimg_shadow_start = (u64)kasan_mem_to_shadow(_text);
	kimg_shadow_end = (u64)kasan_mem_to_shadow(_end);

	/* 计算 module 区间 的 kasan shadow memory 的范围 */
	mod_shadow_start = (u64)kasan_mem_to_shadow((void *)MODULES_VADDR);
	mod_shadow_end = (u64)kasan_mem_to_shadow((void *)MODULES_END);

	/*
	 * We are going to perform proper setup of shadow memory.
	 * At first we should unmap early shadow (clear_pgds() call bellow).
	 * However, instrumented code couldn't execute without shadow memory.
	 * tmp_pg_dir used to keep early shadow mapped until full shadow
	 * setup will be finished.
	 */
	/*
	 * 接下来要配置 内核 PGD 页表 swapper_pg_dir[] 中的 kasan shadow memory 
	 * 部分, 先切换到临时 PGD 页表 tmp_pg_dir[], 然后再对 swapper_pg_dir[] 
	 * 进行配置, 配置完成后再切回到 swapper_pg_dir[].
	 */
	memcpy(tmp_pg_dir, swapper_pg_dir, sizeof(tmp_pg_dir));
	dsb(ishst);
	cpu_replace_ttbr1(lm_alias(tmp_pg_dir));

	/* 清除 PGD 页表 swapper_pg_dir[] 中 kasan shadow memory 的页表项 */
	clear_pgds(KASAN_SHADOW_START, KASAN_SHADOW_END);

	/*
	 * 建立 内核镜像区间 的 kasan memory 区间的 PGD,PUD,PMD 这 3 级页表映射,
	 * 并为 kasan memory 区间分配 物理内存.
	 */
	vmemmap_populate(kimg_shadow_start, kimg_shadow_end,
			 pfn_to_nid(virt_to_pfn(lm_alias(_text))));

	/*
	 * vmemmap_populate() has populated the shadow region that covers the
	 * kernel image with SWAPPER_BLOCK_SIZE mappings, so we have to round
	 * the start and end addresses to SWAPPER_BLOCK_SIZE as well, to prevent
	 * kasan_populate_zero_shadow() from replacing the page table entries
	 * (PMD or PTE) at the edges of the shadow region for the kernel
	 * image.
	 */
	kimg_shadow_start = round_down(kimg_shadow_start, SWAPPER_BLOCK_SIZE);
	kimg_shadow_end = round_up(kimg_shadow_end, SWAPPER_BLOCK_SIZE);

	/*
	 * 映射 多个 不使用 kasan 功能的 kasan shadow 子区间 到 kasan_zero_page[]:
	 * 即 这些子区间 不使用 kasan 功能.
	 */
	kasan_populate_zero_shadow((void *)KASAN_SHADOW_START,
				   (void *)mod_shadow_start); /* 整个 kasan shadow 内存区间自身禁用 kasan 功能 */
	kasan_populate_zero_shadow((void *)kimg_shadow_end, /* [fixmap + PCI I/O, vmemmap] 区间 禁用 kasan 功能 */
				   kasan_mem_to_shadow((void *)PAGE_OFFSET));

	if (kimg_shadow_start > mod_shadow_end)
		kasan_populate_zero_shadow((void *)mod_shadow_end,
					   (void *)kimg_shadow_start);

	/* 建立 RAM 线性映射区间的 kasan shadow 子区间 页表映射, 并分配物理内存 */
	for_each_memblock(memory, reg) {
		void *start = (void *)__phys_to_virt(reg->base);
		void *end = (void *)__phys_to_virt(reg->base + reg->size);

		if (start >= end)
			break;

		vmemmap_populate((unsigned long)kasan_mem_to_shadow(start),
				(unsigned long)kasan_mem_to_shadow(end),
				pfn_to_nid(virt_to_pfn(start)));
	}

	/*
	 * KAsan may reuse the contents of kasan_zero_pte directly, so we
	 * should make sure that it maps the zero page read-only.
	 */
	/* 所有的 kasan 目前都映射到 kasan_zero_page[] 页面, 且以 只读 方式映射 */
	for (i = 0; i < PTRS_PER_PTE; i++)
		set_pte(&kasan_zero_pte[i],
			pfn_pte(sym_to_pfn(kasan_zero_page), PAGE_KERNEL_RO));

	memset(kasan_zero_page, 0, PAGE_SIZE);
	/*
	 * 已经配置好了 swapper_pg_dir[] 内核页表 的 kasan shadow 区间
	 * 映射(全部映射到了 kasan_zero_page[] 只读页面), 切换回 swapper_pg_dir[] 
	 * 内核页表.
	 */
	cpu_replace_ttbr1(lm_alias(swapper_pg_dir));

	/* At this point kasan is fully initialized. Enable error messages */
	init_task.kasan_depth = 0;
	pr_info("KernelAddressSanitizer initialized\n");
}

3.2 KASAN 对内存分配时标记和访问时检测

KASAN 支持对以下几种内存进行标记和访问检测:

  • 全局变量
  • buddy 页面
  • slab 空间
  • 栈空间

3.2.1 全局变量的标记和检测

3.2.1.1 全局变量的标记

Linux 内核开启 KASAN 功能后,编译器会为全局变量生成一段初始化代码,类似如下的片段:

2eb128ff-2db3-4967-a155-574087f9e194

可以看到这段代码调用了 __asan_register_globals() 函数,然后在下面路径中被调用:

kernel_init_freeable()
	do_basic_setup()
		do_ctors()
			_sub_I_65535_1()
				__asan_register_globals()

那么 __asan_register_globals() 函数做了些什么工作呢?来看一下:

/* The layout of struct dictated by compiler */
struct kasan_global {
	/* 全局变量的起始地址 */
	const void *beg;		/* Address of the beginning of the global variable. */
	/* 全局变量占据空间大小 */
	size_t size;			/* Size of the global variable. */
	/* 全局变量的大小 + red zone 大小, 然后对齐到 32 字节 */
	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
};

/* @globals 由编译器构建 */
void __asan_register_globals(struct kasan_global *globals, size_t size)
{
	int i;

	for (i = 0; i < size; i++)
		register_global(&globals[i]);
}
EXPORT_SYMBOL(__asan_register_globals);

#define KASAN_GLOBAL_REDZONE    0xFA  /* redzone for global variable */

/* 初始化 全局变量自身 和 其 redzone 的 kasan shadow */
static void register_global(struct kasan_global *global)
{
	size_t aligned_size = round_up(global->size, KASAN_SHADOW_SCALE_SIZE);

	/* 全局变量自身空间 的 shadow 区填充 */
	kasan_unpoison_shadow(global->beg, global->size);

	/* 全局变量 redzone 的 shadow 区填充为 0xFA */
	kasan_poison_shadow(global->beg + aligned_size,
		global->size_with_redzone - aligned_size,
		KASAN_GLOBAL_REDZONE);
}
/*
 * - size 对齐到 8 字节部分的 kasan shadow 清 0
 * - 如果 size 有未对齐到 8 字节的部分, 则剩余部分字节数
 *   写入对应的 kasan shadow
 */
void kasan_unpoison_shadow(const void *address, size_t size)
{
	kasan_poison_shadow(address, size, 0);

	if (size & KASAN_SHADOW_MASK) { /* 未对齐到 8 字节的剩余字节 */
		u8 *shadow = (u8 *)kasan_mem_to_shadow(address + size);
		*shadow = size & KASAN_SHADOW_MASK; /* shadow 写入未对齐的字节数 */
	}
}

/*
 * Poisons the shadow memory for 'size' bytes starting from 'addr'.
 * Memory addresses should be aligned to KASAN_SHADOW_SCALE_SIZE.
 */
/* 将 [address, address+size] 对应的 kasan shadow 设为 value */
static void kasan_poison_shadow(const void *address, size_t size, u8 value)
{
	void *shadow_start, *shadow_end;

	shadow_start = kasan_mem_to_shadow(address);
	shadow_end = kasan_mem_to_shadow(address + size);

	memset(shadow_start, value, shadow_end - shadow_start);
}

kasan_mem_to_shadow() 映射被访问虚拟地址 addr 到其对应的 shadow 内存虚拟地址

#define KASAN_SHADOW_SCALE_SHIFT 3

/*
 * KASAN_SHADOW_START: beginning of the kernel virtual addresses.
 * KASAN_SHADOW_END: KASAN_SHADOW_START + 1/8 of kernel virtual addresses.
 */
#define KASAN_SHADOW_START      (VA_START)
#define KASAN_SHADOW_END        (KASAN_SHADOW_START + KASAN_SHADOW_SIZE)

/*
 * This value is used to map an address to the corresponding shadow
 * address by the following formula:
 *     shadow_addr = (address >> 3) + KASAN_SHADOW_OFFSET;
 *
 * (1 << 61) shadow addresses - [KASAN_SHADOW_OFFSET,KASAN_SHADOW_END]
 * cover all 64-bits of virtual addresses. So KASAN_SHADOW_OFFSET
 * should satisfy the following equation:
 *      KASAN_SHADOW_OFFSET = KASAN_SHADOW_END - (1ULL << 61)
 */
#define KASAN_SHADOW_OFFSET     (KASAN_SHADOW_END - (1ULL << (64 - 3)))

/* 返回 @addr 对应的 kasan shadow address */
static inline void *kasan_mem_to_shadow(const void *addr)
{
	return (void *)((unsigned long)addr >> KASAN_SHADOW_SCALE_SHIFT)
		+ KASAN_SHADOW_OFFSET;
}

小结一下,对全局变量的标记工作包括:

  • 全局变量自身空间,对齐到 8 字节的部分,对应的 shadow 字节空间均标记为 0,未对齐到 8 字节的部分的对应 shadow 字节填充为未对齐到 8 字节的字节数;
  • 编译器为全局变量尾部扩展的 redzone 对应的 shadow 字节填充为 0xFA (KASAN_GLOBAL_REDZONE)。
3.2.1.1 全局变量的访问检测

用链接 linux kernel 内存踩踏之KASAN(一) 的代码进行全局变量 OOB 问题测试:

// kasan_test.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/workqueue.h>
#include <linux/jiffies.h>
#include <asm/page.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/vmalloc.h>


#include "../../mm/kasan/kasan.h"

int global_kasan_value[2] = {996, 007};

struct kasan_test_type {
    int type;
};

static struct kasan_test_type *gptr = NULL;

enum kasan_test_case {
    slab_out_of_bounds = 0,
    page_out_of_bounds = 1,
    global_out_of_bounds = 2,
    stack_out_of_bounds = 3,
    use_after_free = 4,
    vmalloc_out_of_bounds = 5,
    alloca_out_of_bounds = 6,
    slab_use_after_free = 7,
    slab_repeate_free = 8,
};

static void kmalloc_oob_right(size_t size, int write_offset)
{
    char *ptr;

    ptr = kmalloc(size, GFP_KERNEL);
    pr_info("%s %llx\n", __func__, (unsigned long long)ptr);

    ptr[size - 1 + write_offset] = 'y';

    kfree(ptr);
}

static void kmalloc_use_after_free(size_t size)
{
    char *ptr;

    ptr = kmalloc(size, GFP_KERNEL);
    pr_info("%s %llx\n", __func__, (unsigned long long)ptr);

    ptr[0] = 'x';

    kfree(ptr);
    
    ptr[0] = 'y';
}

static void kmalloc_repeate_free(size_t size)
{
    char *ptr;

    ptr = kmalloc(size, GFP_KERNEL);
    pr_info("%s %llx\n", __func__, (unsigned long long)ptr);

    ptr[0] = 'x';

    kfree(ptr);
    kfree(ptr);
}

static void global_oob_left(void)
{
	pr_info("global arr oob access %d\n", global_kasan_value[2]);
}

static void pagealloc_oob_right(size_t order)
{
    char *ptr;
    struct page *pages;

    size_t size = (1UL << (PAGE_SHIFT + order));

    pages = alloc_pages(GFP_KERNEL, order);
    ptr = page_address(pages);
    pr_info("%s %llx\n", __func__, (unsigned long long)ptr);
    ptr[0] = ptr[size];
    free_pages((unsigned long)ptr, order);
}

static void pagealloc_uaf(size_t order)
{
	char *ptr;
	struct page *pages;

	pages = alloc_pages(GFP_KERNEL, order);
	ptr = page_address(pages);
	pr_info("%s %llx", __func__, (unsigned long long)ptr);
	free_pages((unsigned long)ptr, order);

	pr_info("%s %d\n", __func__, ptr[0]);
}

static void vmalloc_oob(size_t size)
{
    char *v_ptr;

    v_ptr = vmalloc(size);

    OPTIMIZER_HIDE_VAR(v_ptr);

    pr_info("%s %llx", __func__, (unsigned long long)v_ptr);
    /* Make sure in-bounds accesses are valid. */
    v_ptr[0] = 0;
    v_ptr[size - 1] = 0;

    /* trigger oob access */
    pr_info("%s %d\n", __func__, v_ptr[size]);

    vfree(v_ptr);
}

static void kasan_stack_oob(void)
{
	char stack_array[10];
	/* See comment in kasan_global_oob_right. */
	char *volatile array = stack_array;
	char *p = &array[ARRAY_SIZE(stack_array) + 4];
	pr_info("%s  %d\n", __func__, *p);
}

static void kasan_test_case(int type)
{
    bool multishot = kasan_save_enable_multi_shot();

    switch (type) {
        case slab_out_of_bounds:
            kmalloc_oob_right(128, 2); //alloc 128 byte and overwrite 2 offset
            break;

        case page_out_of_bounds:
            pagealloc_oob_right(0);
            break;

        case global_out_of_bounds:
            global_oob_left();
            break;

        case stack_out_of_bounds:
            kasan_stack_oob();
            break;

        case use_after_free:
            pagealloc_uaf(0);
            break;

        case vmalloc_out_of_bounds:
            vmalloc_oob(2048);
            break;

	case slab_use_after_free:
		kmalloc_use_after_free(18);
		break;

	case slab_repeate_free:
		kmalloc_repeate_free(88);
		break;

        default :
            pr_info("undef error type %d\n", type);
            break;
    }

    kasan_restore_multi_shot(multishot);
    pr_info("%s type %d\n", __func__, type);
}

static ssize_t kasan_testcase_write(struct file *filp, const char __user *buf,
                   size_t len, loff_t *off)
{
    char *kbuf;
    int ntcase;

    kbuf = kmalloc(len + 1, GFP_KERNEL);
    if (copy_from_user(kbuf, buf, len) != 0) {
        pr_info("copy the buff failed \n");
        goto done;
    }

    ntcase = simple_strtoul(kbuf, NULL, 0);

    kasan_test_case(ntcase);
done:
    return len;
}

static struct file_operations kasan_fops = {
    .owner  =   THIS_MODULE,
    .write  =   kasan_testcase_write,
    .llseek =   noop_llseek,
};

static struct miscdevice kasan_misc = {
    .minor  = MISC_DYNAMIC_MINOR,
    .name   = "kasan_test",
    .fops   = &kasan_fops,
};

static int __init kasan_start(void) 
{
    int ret;

    ret = misc_register(&kasan_misc);
    if (ret < 0) {
        printk(KERN_EMERG " kasan test register failed %d\n", ret);
        return ret;
    }
    gptr = kzalloc(sizeof(struct kasan_test_type), GFP_KERNEL);

    printk(KERN_INFO "kasan test register\n");
    return 0;
}

static void __exit kasan_end(void) 
{ 
    misc_deregister(&kasan_misc);
} 

MODULE_LICENSE("GPL");
MODULE_AUTHOR("geek");
MODULE_DESCRIPTION("A simple kasan test driver!");
MODULE_VERSION("0.1");
 
module_init(kasan_start);
module_exit(kasan_end);

编译运行,测试触发下面代码段的运行:

int global_kasan_value[2] = {996, 007};

static void global_oob_left(void)
{
	pr_info("global arr oob access %d\n", global_kasan_value[2]);
}

报错日志:

# insmod kasan_test.ko
root@qemu-ubuntu:~# echo 2 > /dev/kasan_test
[   83.940491] ==================================================================
[   83.942045] BUG: KASAN: global-out-of-bounds in kasan_testcase_write+0x37c/0x3c0 [kasan_test]
[   83.942486] Read of size 4 at addr ffff200000ea2008 by task bash/1388
[   83.942673] 
[   83.944585] 
[   83.944782] The buggy address belongs to the variable:
[   83.944944]  global_kasan_value+0x8/0xfffffffffffff000 [kasan_test]
[   83.945163] 
[   83.945284] Memory state around the buggy address:
[   83.945753]  ffff200000ea1f00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   83.945954]  ffff200000ea1f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   83.946154] >ffff200000ea2000: 00 fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
[   83.946341]                       ^
[   83.946427]  ffff200000ea2080: 00 00 fa fa fa fa fa fa 00 00 00 00 00 00 00 00
[   83.946556]  ffff200000ea2100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   83.946701] ==================================================================

看到了吧?访问到全局变量的 OOB 问题被爆出来了,怎么做到的?反汇编 kasan_test.ko,可以看到编译器在写 global_kasan_value 的代码前插入了 __asan_store1_noabort() 调用:

867b436b-6935-4b35-9414-de7d21c579fc

正是 __asan_store1_noabort() 对访问地址的 shadow 字节进行检测:

void __asan_store1(unsigned long addr)
{
	check_memory_region_inline(addr, 1, true, _RET_IP_);
}
__alias(__asan_store1) void __asan_store1_noabort(unsigned long); // __asan_store1() 的别名
static __always_inline void check_memory_region_inline(unsigned long addr,
						size_t size, bool write,
						unsigned long ret_ip)
{
	if (unlikely(size == 0))
		return;

	if (unlikely((void *)addr <
		kasan_shadow_to_mem((void *)KASAN_SHADOW_START))) { /* 非法地址 */
		kasan_report(addr, size, write, ret_ip); /* 报告错误 */
		return;
	}

	if (likely(!memory_is_poisoned(addr, size)))
		return; /* 可以正常访问地址 addr */

	/* 报告错误 */
	kasan_report(addr, size, write, ret_ip);
}

static __always_inline bool memory_is_poisoned(unsigned long addr, size_t 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);
}

static __always_inline bool memory_is_poisoned_1(unsigned long addr)
{
	s8 shadow_value = *(s8 *)kasan_mem_to_shadow((void *)addr); /* 读取 @addr 对应的 kasan shadow 字节内容 */

	/*
	 * shadow_value 可能是:
	 * - 某块内存末尾未对齐到 8 字节, shadow_value 存储未对齐到 8 的字节数
	 * - poisoned 字节: KASAN_FREE_PAGE, ...
	 */
	if (unlikely(shadow_value)) {
		/* 试图访问 @addr 未对齐到 8 部分, 在 8 字节内的偏移位置 */
		s8 last_accessible_byte = addr & KASAN_SHADOW_MASK;
		/*
		 * last_accessible_byte >= shadow_value 成立的两种可能:
		 * - @addr 想访问 8 字节内的 第 last_accessible_byte 个字节 
		 *   超出了 可访问字节 最大位置, 表示出现了 OOB BUG.
		 * - shadow_value 存储的是 poisoned 字节, 如 KASAN_GLOBAL_REDZONE
		 *   等, 这些系统定义的 poisoned 字节全都是小于 0 的, 因为
		 *   它们的最高位都为 1, 对于 s8 类型则为小于 0 的数.
		 */
		return unlikely(last_accessible_byte >= shadow_value);
	}

	return false; /* shadow_value == 0 表示从 @addr 连续的 8 字节均可访问 */
}

这里只贴出了 1 字节访问的检测,对于 2,4,8,16, 以及更多字节写访问检测,读取可自行阅读源码中的 memory_is_poisoned_2_4_8(), memory_is_poisoned_16(), memory_is_poisoned_n() 函数,自行分析。

这里的例子是写访问检测,读访问检测的类似,编译器在读访问之前插入 __asan_loadN_noabort() 调用,以 1 字节的读访问为例,来看一下:

void __asan_load1(unsigned long addr)
{
	check_memory_region_inline(addr, 1, false, _RET_IP_);
}
__alias(__asan_load1) void __asan_load1_noabort(unsigned long); // __asan_load1() 的别名

check_memory_region_inline() 已经分析过了,就不再赘述。另外,kasan_report() 的内容本文不做展开,读者可自行阅读相关源码。

3.2.2 buddy 页面的标记和检测

3.2.2.1 buddy 页面的标记

首先,BOOT 期间在内存管理系统初始化过程中,释放内存页面给 buddy 时,调用 kasan_free_pages() 将对应页面的 shadow 内存字节填充为 0xFF,表示所有这些释放页面空间不可访问(未从 buddy 分配自然不可用):

#define KASAN_FREE_PAGE         0xFF  /* page was freed */

void kasan_free_pages(struct page *page, unsigned int order)
{
	if (likely(!PageHighMem(page)))
		/* 已释放 page 的 kasan shadow 标记为 0xFF */
		kasan_poison_shadow(page_address(page),
				PAGE_SIZE << order,
				KASAN_FREE_PAGE);
}

然后在分配页面是调用 kasan_alloc_pages() 将页面的 shadow 内存字节清 0,表示对应页面空间可用:

prep_new_page()
	post_alloc_hook()
		kasan_alloc_pages()

void kasan_alloc_pages(struct page *page, unsigned int order)
{
	if (likely(!PageHighMem(page)))
		/*
		 * - size 对齐到 8 字节部分的 kasan shadow 清 0
		 * - 如果 size 有未对齐到 8 字节的部分, 则剩余部分字节数
		 *   写入对应的 kasan shadow
		 */
		kasan_unpoison_shadow(page_address(page), PAGE_SIZE << order);
}

释放页面时同样调用 kasan_free_pages(),标记释放的页面空间不再可用。

3.2.2.1 buddy 页面的访问检测

这和 2.2.1.1 全局变量的访问检测 并无不同,都是编译器在页面空间读写访问前,插入 __asan_{load,store}N_abort 调用,在此不再赘述。

这里给出 buddy 页面访问 OOB 报错的例子信息,对应的出错代码段如下:

static void pagealloc_oob_right(size_t order)
{
    char *ptr;
    struct page *pages;

    size_t size = (1UL << (PAGE_SHIFT + order));

    pages = alloc_pages(GFP_KERNEL, order);
    ptr = page_address(pages);
    pr_info("%s %llx\n", __func__, (unsigned long long)ptr);
    ptr[0] = ptr[size];
    free_pages((unsigned long)ptr, order);
}

报错日志:

# insmod kasan_test.ko
# echo 1 > /dev/kasan_test
[   51.847460] ==================================================================
[   51.848464] BUG: KASAN: use-after-free in kasan_testcase_write+0x200/0x3c0 [kasan_test]
[   51.848591] Read of size 1 at addr ffff800031eb3000 by task bash/1386
[   51.848681] 
[   51.849727] 
[   51.849839] Allocated by task 1334:
[   51.850066] 
[   51.850108] Freed by task 1335:
[   51.850230] 
[   51.850276] The buggy address belongs to the object at ffff800031eb3000
[   51.850276]  which belongs to the cache cred_jar(79:systemd-random-seed.service) of size 168
[   51.850476] The buggy address is located 0 bytes inside of
[   51.850476]  168-byte region [ffff800031eb3000, ffff800031eb30a8)
[   51.850661] The buggy address belongs to the page:
[   51.850833] page:ffff7e0000c7acc0 count:1 mapcount:0 mapping:          (null) index:0xffff800031eb3300
[   51.851036] flags: 0xfffc00000000100(slab)
[   51.851298] raw: 0fffc00000000100 0000000000000000 ffff800031eb3300 000000018010000e
[   51.851423] raw: dead000000000100 dead000000000200 ffff80003138f000 ffff8000347acd80
[   51.851540] page dumped because: kasan: bad access detected
[   51.851621] page->mem_cgroup:ffff8000347acd80
[   51.851752] 
[   51.851810] Memory state around the buggy address:
[   51.851999]  ffff800031eb2f00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   51.852114]  ffff800031eb2f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   51.852208] >ffff800031eb3000: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[   51.852329]                    ^
[   51.852388]  ffff800031eb3080: fb fb fb fb fb fc fc fc fc fc fc fc fc fc fc fc
[   51.852480]  ffff800031eb3100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   51.852582] ==================================================================

你可能会奇怪,这里不应该是页面访问的 OOB 错误吗?怎么爆出了 UAF?原因是越界的地址是紧跟页面之后第一个字节的地址 ptr[0] = ptr[size];,其对应的 shadow 不对应页面本身的 shadow 区域,从而产生了误报。可见,KASAN 存在一定的缺陷。

3.2.3 slab 空间的标记和检测

3.2.3.1 slab 空间的标记
3.2.3.1.1 分配时的标记

前面有提到过,KASAN 会将 buddy 分配器分配的页面会标记为可用状态,所以,首先,slab 从 buddy 获取页面后,KASAN 会先将获取的页面标记为不可用状态,因为 slab 是按 object 进行分配的:

kmalloc()
	...
	slab_alloc_node()
		...
		allocate_slab()
			...
			page = alloc_slab_page(s, alloc_gfp, node, oo);
			...
			kasan_poison_slab(page);
			...

/* slab 页面空间 的 kasan shadow 填充为 KASAN_KMALLOC_REDZONE (0xFC) */
void kasan_poison_slab(struct page *page)
{
	kasan_poison_shadow(page_address(page),
			PAGE_SIZE << compound_order(page),
			KASAN_KMALLOC_REDZONE);
}

然后,在分配 object 的时候,KASAN 将 object 对应空间标记为可用,同时标记 object 的 redzone 空间,这可以放置越界访问(OOB):

kmalloc()
	...
	slab_alloc_node()
		...
		slab_post_alloc_hook()

static inline void slab_post_alloc_hook(struct kmem_cache *s, gfp_t flags,
					size_t size, void **p)
{
	size_t i;

	flags &= gfp_allowed_mask;
	for (i = 0; i < size; i++) {
		void *object = p[i];

		...
		kasan_slab_alloc(s, object, flags);
	}

	...
}

void kasan_slab_alloc(struct kmem_cache *cache, void *object, gfp_t flags)
{
	kasan_kmalloc(cache, object, cache->object_size, flags);
}

#define KASAN_KMALLOC_REDZONE   0xFC  /* redzone inside slub object */

void kasan_kmalloc(struct kmem_cache *cache, const void *object, size_t size,
		   gfp_t flags)
{
	unsigned long redzone_start;
	unsigned long redzone_end;

	...

	if (unlikely(object == NULL))
		return;

	redzone_start = round_up((unsigned long)(object + size),
				KASAN_SHADOW_SCALE_SIZE);
	redzone_end = round_up((unsigned long)object + cache->object_size,
				KASAN_SHADOW_SCALE_SIZE);

	kasan_unpoison_shadow(object, size); /* kasan 标记 size 大小的 object 可用 */
	kasan_poison_shadow((void *)redzone_start, redzone_end - redzone_start,
		KASAN_KMALLOC_REDZONE); /* kasan 标记 redzone 空间: 读写都会导致 kasan BUG report */

	if (cache->flags & SLAB_KASAN)
		set_track(&get_alloc_info(cache, object)->alloc_track, flags);
}
EXPORT_SYMBOL(kasan_kmalloc);

slab 分配时大空间时,还有一种路径走 kmalloc_large(),KASAN 标记过程会有些不同,本文对此不作展开,读者可自行研究。

3.2.3.1.2 释放时的标记

释放时 KASAN 的标记动作,可以防止 UAF重复释放(double free)的问题:

kfree()
	slab_free()
		slab_free_freelist_hook()
			slab_free_hook()
				kasan_slab_free()

bool kasan_slab_free(struct kmem_cache *cache, void *object)
{
	s8 shadow_byte;

	/* RCU slabs could be legally used after free within the RCU period */
	if (unlikely(cache->flags & SLAB_TYPESAFE_BY_RCU))
		return false;

	shadow_byte = READ_ONCE(*(s8 *)kasan_mem_to_shadow(object));
	if (shadow_byte < 0 || shadow_byte >= KASAN_SHADOW_SCALE_SIZE) {
		kasan_report_double_free(cache, object,
				__builtin_return_address(1));
		return true;
	}

	kasan_poison_slab_free(cache, object);

	if (unlikely(!(cache->flags & SLAB_KASAN)))
		return false;

	set_track(&get_alloc_info(cache, object)->free_track, GFP_NOWAIT);
	quarantine_put(get_free_info(cache, object), cache);
	return true;
}

#define KASAN_KMALLOC_FREE      0xFB  /* object was freed (kmem_cache_free/kfree) */

static void kasan_poison_slab_free(struct kmem_cache *cache, void *object)
{
	unsigned long size = cache->object_size;
	unsigned long rounded_up_size = round_up(size, KASAN_SHADOW_SCALE_SIZE);

	/* RCU slabs could be legally used after free within the RCU period */
	if (unlikely(cache->flags & SLAB_TYPESAFE_BY_RCU))
		return;

	kasan_poison_shadow(object, rounded_up_size, KASAN_KMALLOC_FREE);
}
3.2.3.2 slab 空间的访问检测

这和 2.2.1.1 全局变量的访问检测 并无多大不同,都是编译器在页面空间读写访问前,插入 __asan_{load,store}N_abort 调用,在此不再赘述。

分别看下 slab OOB,UAF,重复释放(double free)3 种情形的示例。

首先看 OOB,触发代码:

static void kmalloc_oob_right(size_t size, int write_offset)
{
    char *ptr;

    ptr = kmalloc(size, GFP_KERNEL);
    pr_info("%s %llx\n", __func__, (unsigned long long)ptr);

    ptr[size - 1 + write_offset] = 'y';

    kfree(ptr);
}

报错日志:

# echo 0 > /dev/kasan_test
[  182.350903] ==================================================================
[  182.351236] BUG: KASAN: slab-out-of-bounds in kasan_testcase_write+0x2ac/0x4c8 [kasan_test]
[  182.351446] Write of size 1 at addr ffff8000325aa881 by task bash/1386
[  182.351594] 
[  182.352740] 
[  182.353024] Allocated by task 1386:
[  182.353580] 
[  182.353834] Freed by task 0:
[  182.354568] 
[  182.354763] The buggy address belongs to the object at ffff8000325aa800
[  182.354763]  which belongs to the cache kmalloc-128 of size 128
[  182.355252] The buggy address is located 1 bytes to the right of
[  182.355252]  128-byte region [ffff8000325aa800, ffff8000325aa880)
[  182.355509] The buggy address belongs to the page:
[  182.355652] page:ffff7e0000c96a80 count:1 mapcount:0 mapping:          (null) index:0x0
[  182.355848] flags: 0xfffc00000000100(slab)
[  182.355983] raw: 0fffc00000000100 0000000000000000 0000000000000000 0000000180100010
[  182.356158] raw: ffff7e0000cdcd00 0000000d00000002 ffff800035003c00 0000000000000000
[  182.356310] page dumped because: kasan: bad access detected
[  182.356425] 
[  182.356475] Memory state around the buggy address:
[  182.356589]  ffff8000325aa780: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[  182.356737]  ffff8000325aa800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[  182.356879] >ffff8000325aa880: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[  182.357011]                    ^
[  182.357091]  ffff8000325aa900: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[  182.357224]  ffff8000325aa980: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[  182.357347] ==================================================================

接着看 UAF,触发代码:

static void kmalloc_use_after_free(size_t size)
{
    char *ptr;

    ptr = kmalloc(size, GFP_KERNEL);
    pr_info("%s %llx\n", __func__, (unsigned long long)ptr);

    ptr[0] = 'x';

    kfree(ptr);

    ptr[0] = 'y';
}

报错日志:

# echo 7 > /dev/kasan_test 
[   81.257559] ==================================================================
[   81.259832] BUG: KASAN: use-after-free in kasan_testcase_write+0x468/0x4c8 [kasan_test]
[   81.261441] Write of size 1 at addr ffff800034487c00 by task bash/1386
[   81.262126] 
[   81.263600] 
[   81.263954] Allocated by task 1386:
[   81.264686] 
[   81.265004] Freed by task 1386:
[   81.265367] 
[   81.265481] The buggy address belongs to the object at ffff800034487c00
[   81.265481]  which belongs to the cache kmalloc-128 of size 128
[   81.266581] The buggy address is located 0 bytes inside of
[   81.266581]  128-byte region [ffff800034487c00, ffff800034487c80)
[   81.267555] The buggy address belongs to the page:
[   81.268264] page:ffff7e0000d121c0 count:1 mapcount:0 mapping:          (null) index:0x0
[   81.268873] flags: 0xfffc00000000100(slab)
[   81.269416] raw: 0fffc00000000100 0000000000000000 0000000000000000 0000000180100010
[   81.270147] raw: ffff7e0000c63440 0000000b0000000b ffff800035003c00 0000000000000000
[   81.270856] page dumped because: kasan: bad access detected
[   81.271324] 
[   81.271563] Memory state around the buggy address:
[   81.272280]  ffff800034487b00: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[   81.272640]  ffff800034487b80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   81.273689] >ffff800034487c00: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[   81.274098]                    ^
[   81.274166]  ffff800034487c80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   81.274258]  ffff800034487d00: 03 fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   81.274362] ==================================================================

最后看重复释放(double free),触发代码:

static void kmalloc_double_free(size_t size)
{
    char *ptr;

    ptr = kmalloc(size, GFP_KERNEL);
    pr_info("%s %llx\n", __func__, (unsigned long long)ptr);

    ptr[0] = 'x';

    kfree(ptr);
    kfree(ptr);
}

报错日志:

# echo 8 > /dev/kasan_test 
[  358.378681] ==================================================================
[  358.378947] BUG: KASAN: double-free or invalid-free in           (null)
[  358.379107] 
[  358.380123] 
[  358.380376] Allocated by task 1386:
[  358.380905] 
[  358.381138] Freed by task 1386:
[  358.381849] 
[  358.382091] The buggy address belongs to the object at ffff800033f34900
[  358.382091]  which belongs to the cache kmalloc-128 of size 128
[  358.382722] The buggy address is located 0 bytes inside of
[  358.382722]  128-byte region [ffff800033f34900, ffff800033f34980)
[  358.382856] The buggy address belongs to the page:
[  358.382962] page:ffff7e0000cfcd00 count:1 mapcount:0 mapping:          (null) index:0x0
[  358.383083] flags: 0xfffc00000000100(slab)
[  358.383159] raw: 0fffc00000000100 0000000000000000 0000000000000000 0000000180100010
[  358.383263] raw: dead000000000100 dead000000000200 ffff800035003c00 0000000000000000
[  358.383355] page dumped because: kasan: bad access detected
[  358.383444] 
[  358.383477] Memory state around the buggy address:
[  358.383551]  ffff800033f34800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[  358.383643]  ffff800033f34880: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[  358.383734] >ffff800033f34900: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[  358.383818]                    ^
[  358.383872]  ffff800033f34980: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[  358.383961]  ffff800033f34a00: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[  358.384049] ==================================================================

3.2.4 栈空间的标记和检测

编译器在栈变量前后插入 redzone,然后编译器再插入对这两个 redzone 的 KASAN 标记代码。如下图所示:

image

栈空间的 OOB 包括以下几种情形:

/*
 * Stack redzone shadow values
 * (Those are compiler's ABI, don't change them)
 */
#define KASAN_STACK_LEFT        0xF1
#define KASAN_STACK_MID         0xF2
#define KASAN_STACK_RIGHT       0xF3
#define KASAN_STACK_PARTIAL     0xF4
#define KASAN_USE_AFTER_SCOPE   0xF8

OOB 触发代码:

static void kasan_stack_oob(void)
{
	char stack_array[10];
	/* See comment in kasan_global_oob_right. */
	char *volatile array = stack_array;
	char *p = &array[ARRAY_SIZE(stack_array) + 4];
	pr_info("%s  %d\n", __func__, *p);
}

报错日志:

# insmod kasan_test.ko
# echo 3 > /dev/kasan_test
[  556.754000] ==================================================================
[  556.755603] BUG: KASAN: stack-out-of-bounds in kasan_testcase_write+0x36c/0x4c8 [kasan_test]
[  556.756005] Read of size 1 at addr ffff800031bffc9e by task bash/1387
[  556.756163] 
[  556.758828] 
[  556.759118] The buggy address belongs to the page:
[  556.759879] page:ffff7e0000c6ffc0 count:0 mapcount:0 mapping:          (null) index:0x0
[  556.761106] flags: 0xfffc00000000000()
[  556.762048] raw: 0fffc00000000000 0000000000000000 0000000000000000 00000000ffffffff
[  556.762386] raw: 0000000000000000 ffff7e0000c6ffe0 0000000000000000 0000000000000000
[  556.762556] page dumped because: kasan: bad access detected
[  556.762682] 
[  556.762749] Memory state around the buggy address:
[  556.762994]  ffff800031bffb80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[  556.763284]  ffff800031bffc00: 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1 00 f2
[  556.763414] >ffff800031bffc80: f2 f2 00 02 f3 f3 00 00 00 00 00 00 00 00 00 00
[  556.763569]                             ^
[  556.763652]  ffff800031bffd00: 00 00 f1 f1 f1 f1 00 00 f2 f2 00 00 00 00 00 f2
[  556.763771]  ffff800031bffd80: f2 f2 f2 f2 00 00 00 00 00 f3 f3 f3 f3 f3 00 00
[  556.763900] ==================================================================

4. KASAN 的优缺点和发展

KASAN 通过在内存访问指令前插入钩子函数,去检测内存访问的合法性,其优点是可以做到实时检测,不像 slub debug 的检测需要手动触发,或者释放时发现问题,这存在检测上存在延迟;和 slub debug 一样,KASAN 也会带来性能上的开销,本文讨论的版本不适应于生产环境;另外,KASAN 也会带来内核代码体积的增大。

为了克服上面提到的这些缺点,在高版本的内核增加了 Software TAG(仅 ARM64 支持,需要利用指针高位,利用了 ARM64 TBI 特性)Hardware TAG(需要硬件支持,如 ARM64MTE),Hardware TAG 的方式带来的性能开销很小,可以用于生产环境。

另外,高版本内核也增加了对 VMALLOC 空间的 KASAN 支持。

5. 参考资料

[1] Kernel Address Sanitizer (KASAN)
[2] linux kernel 内存踩踏之KASAN(一)
[3] linux之kasan原理及解析
[4] KASAN(9)-全局变量和栈变量的实现

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐