一、KASAN工具使用
KASAN工具:Kernel Address SANitizer(KASAN)是一种动态内存安全错误检测工具,主要功能是检查内存越界访问和使用已释放内存的问题。
1.1 KASAN宏控开关
KASAN有三种模式:1.通用KASAN;2.基于软件标签的KASAN;3.基于硬件标签的KASAN
宏控开关 | 说明 | |
通用KASAN | CONFIG_KASAN_GENERIC | 启用通用KASAN,类似于用户空 间的ASan。这种模式在许多CPU架构上都被支持,但它有明显的性能和内存开销。 |
软件标签KASAN | CONFIG_KASAN_SW_TAGS | 类似于用户空间HWASan,这种模式只支持arm64,但其适度的内存开销允许在内存受限的设备上用真实的工作负载进行测试。 |
硬件标签KASAN | CONFIG_KASAN_HW_TAGS | 现场内存错误检测器或作为安全缓解的模式,这种模式只在支持MTE(内存标签扩展)的arm64 CPU上工作,但它的内存和性能开销很低,因此可以在生产中使用。 注:ARMv8.5-A 以及更高版本的 ARM 架构支持 MTE 功能,可以通过检查 CPUID 寄存器中的宏观架构标识符 (MIDR_EL1) 来确定 CPU 是否支持 MTE |
打开宏控开关就可以使能KASAN.
1.2 编译器依赖
软件KASAN模式使用编译时工具在每个内存访问之前插入有效性检查,因此需要一个 提供支持的编译器版本。基于硬件标签的模式依靠硬件来执行这些检查,但仍然需要一个支持内存标签指令的编译器版本。
编译器依赖 | |
通用KASAN | 需要GCC 8.3.0版本或更高版本,或者内核支持的任何Clang版本。 |
软件标签KASAN | 需要GCC 11+或者内核支持的任何Clang版本。 |
硬件标签KASAN | KASAN需要GCC 10+或Clang 12+。 |
1.3 检测的内存类型
检测的内存类型 | |
通用KASAN | 支持在所有的slab、page_alloc、vmap、vmalloc、堆栈和全局内存中查找错误。 |
软件标签KASAN | 支持slab、page_alloc、vmalloc和堆栈内存。 |
硬件标签KASAN | 支持slab、page_alloc和不可执行的vmalloc内存。 |
注:对于slab,通用KASAN和软KASAN模式都支持SLUB和SLAB分配器,而基于硬件标签的 KASAN只支持SLUB。
二、KASAN实现原理
2.1 linux kernel内存分配器
kernel中存在多种内存分配器,常用的包括Buddy、SLUB 、SLAB 、CMA、SLOB等,而KASAN基于内存分配器实现,因此需要对内存分配器做一些了解。
内存分配器 | 适用场景 | 接口函数 |
SLUB | 是当前 Linux 内核中最常用的内存分配器。它适用于大多数通用的内存分配需求,具有较好的性能和灵活性。因此,对于大多数情况下的内存分配操作,SLUB 是首选的分配器。 | 1)kmalloc(size, flags): 分配指定大小的内存块。 2)kmem_cache_alloc(cache, flags): 在给定的缓存 cache 中分配一个对象。 3)kcalloc(n, size, flags): 分配 n 个元素,并将所分配的内存区域初始化为 0。 |
SLAB | 相比SLUB分配器,SLAB 分配器对于某些特殊场景,如需要对齐要求、追踪统计信息等的内存分配操作,可能会更为合适。 | 1)kmem_cache_alloc(cache, flags): 在给定的缓存 cache 中分配一个对象。 2)kmalloc(size, flags): 分配指定大小的内存块。 |
Buddy | 适用于需要页级别连续内存分配的情况,例如页面缓存、物理页面映射等。它将物理内存划分为固定大小的块,并提供对连续块的分配和释放。 | alloc_pages(gfp_mask, order): 分配连续的页面。 |
CMA | 用于为需要大块连续内存的设备驱动程序提供内存分配支持,如 DMA 操作。 | dma_alloc_coherent(dev, size, dma_handle, flag): 在 CMA 区域分配一块连续的内存。 |
SLOB | SLOB 分配器适用于嵌入式系统和资源受限环境中,它实现简单且占用较大的内存管理。因此,在资源非常有限的系统中,可以考虑使用 SLOB 分配器。 | slob_alloc(size, align, boundary, usage): 分配指定大小的内存块。 |
2.2 kasan实现原理
kasan可以检测栈内存、堆内存的异常。
栈内存:全局变量、局部变量
堆内存:通过内存分配器(buddy、slub等)进行堆内存申请与释放的时候调用kasan的相关函数,对shadow memory做标记及检测。
2.2.1 kasan如何检测
检测原理:假设内存是从地址8~15一共8 bytes。对应的shadow memory值为5,假如现在访问11(11&7=3 3<5)地址,那么就是可以访问,假如想要访问地址13(13&7=5 5>=5),那么就不能访问,就检测出了问题。
相关源码如下:
static __always_inline bool memory_is_poisoned_1(unsigned long addr)
{
/* 将地址转换成影子内存(每8byte有对应的1byte影子内存,后面分析具体映射关系) */
s8 shadow_value = *(s8 *)kasan_mem_to_shadow((void *)addr);
/* 如果shadow_value不为0,比如说是负数或者1-7的值,
那么就需要进行判断,看看对应要访问的字节的影子内存对应是否能够访问 */
if (unlikely(shadow_value)) {
/* KASAN_SHADOW_MASK 的值为7 */
#define KASAN_SHADOW_MASK (KASAN_SHADOW_SCALE_SIZE - 1)
/* 这里把虚拟地址 &7 目的就是为了看访问的地址(实际上已经是地址+size)
是否大于剩余可访问的字节数,注意这里就是kasan的最根本的原理 */
s8 last_accessible_byte = addr & KASAN_SHADOW_MASK;
return unlikely(last_accessible_byte >= shadow_value);
}
/* shadow 值为 0,8个字节都能被访问,其中一个字节肯定能访问,
返回false说明kasan没有检测出问题 */
return false;
}
static __always_inline unsigned long bytes_is_nonzero(const u8 *start,
size_t size)
{
while (size) {
/* 这里如果对应的影子地址的值非0,就需要进行权限的判断了 */
if (unlikely(*start))
return (unsigned long)start;
start++;
size--;
}
return 0;
}
static __always_inline unsigned long memory_is_nonzero(const void *start, const void *end)
{
unsigned int words;
unsigned long ret;
unsigned int prefix = (unsigned long)start % 8;
if (end - start <= 16)
return bytes_is_nonzero(start, end - start);
/* 如果影子地址差了16个以上(对应16*8=128 即size大于128) */
if (prefix) {
prefix = 8 - prefix;
/* 将start按8对齐,先把未对齐的前 prefix 长度权限先校验 */
ret = bytes_is_nonzero(start, prefix);
if (unlikely(ret))
return ret;
start += prefix; /* start补齐成8的倍数 */
}
/* 在计算end到start有多少个8字节影子地址(即对应words倍的128长度的实际内存) */
words = (end - start) / 8;
while (words) {
if (unlikely(*(u64 *)start))
return bytes_is_nonzero(start, 8);
/* 再次进行权限判断,如果有一个不为0,则说明有问题 */
start += 8;
words--;
}
/* 最后,将剩余长度的影子地址进行权限判断,同样的有一个不为0,就可能有问题 */
return bytes_is_nonzero(start, (end - start) % 8);
}
static __always_inline bool memory_is_poisoned_n(unsigned long addr,
size_t size)
{
unsigned long ret;
/* 判断 内存对应的影子内存中,起始和结束 shadow 值是否都为 0
注意:这里影子内存起始就是直接转换来的,而结束比较有意思,
找的永远是对应地址对应长度的影子地址的下一个影子地址 */
ret = memory_is_nonzero(kasan_mem_to_shadow((void *)addr),
kasan_mem_to_shadow((void *)addr + size - 1) + 1);
/* 根据前面的判断,如果ret不为0(可能的值为负数或1-7),
就说明内存权限可能有问题,需要进一步判断 */
if (unlikely(ret)) {
/* 只判断起始地址,连续size长度的最后一字节所在影子内存所在位置的权限值 */
unsigned long last_byte = addr + size - 1;
s8 *last_shadow = (s8 *)kasan_mem_to_shadow((void *)last_byte);
/* 如果ret!=last_shadow 可能是因为在连续的内存检测过程中,
就已经检测到了一个非法权限,那么肯定就是有问题的 */
/* ||后面的检测方案和 memory_is_poisoned_1 实现是相同的 */
if (unlikely(ret != (unsigned long)last_shadow ||
((long)(last_byte & KASAN_SHADOW_MASK) >= *last_shadow)))
return true;
}
return false;
}
以上是kasan基于影子内存和对应权限值是如何检测出问题的原理。
kasan是如何维护和标记影子内存所对应的权限值的(详见2.2.2)?以及,kasan的影子内存是如何及映射的(详见2.2.3)?
2.2.2 影子内存标记
内存与释放内存时调用kasan的相关函数。
2.2.2.1 buddy内存分配器检测
Buddy 系统在 free 和 alloc 的时间点上插入了权限设置,所以 buddy 能检测出 use-after-free 类型的错误。调用的函数为:kasan_alloc_pages和kasan_free_pages
alloc 调用流程:
free调用流程:
/*
* 该函数会为从“addr”开始的“size”字节的阴影内存添加tag。
内存地址应该对齐到KASAN_SHADOW_SCALE_SIZE */
void kasan_poison_shadow(const void *address, size_t size, u8 value)
{
void *shadow_start, *shadow_end;
address = reset_tag(address); //实际就是调用了__tag_reset
shadow_start = kasan_mem_to_shadow(address);
shadow_end = kasan_mem_to_shadow(address + size);
/* 将影子内存中对应的权限值设置成value,
对于alloc来说,实际上就是设置成0
对于free来说,实际上就是设置了0xFF
*/
__memset(shadow_start, value, shadow_end - shadow_start);
}
void kasan_unpoison_shadow(const void *address, size_t size)
{
u8 tag = get_tag(address); //实际上就是调用了__tag_get,也就是tag = 0 [CONFIG_KASAN_SW_TAGS 没开】
address = reset_tag(address);
kasan_poison_shadow(address, size, tag);
/* 如果size不是8的倍数,对最后一个影子内存的权限值设置为当前 siez & 7 的大小 */
/* 对于buddy,申请的都是整页,不会走下面,这个函数slab也会调用,会走到底下 */
if (size & KASAN_SHADOW_MASK) {
u8 *shadow = (u8 *)kasan_mem_to_shadow(address + size);
if (IS_ENABLED(CONFIG_KASAN_SW_TAGS))
*shadow = tag; /* 这里走不进来 */
else
*shadow = size & KASAN_SHADOW_MASK;
}
}
void kasan_alloc_pages(struct page *page, unsigned int order)
{
u8 tag;
unsigned long i;
if (unlikely(PageHighMem(page)))
return;
tag = random_tag();
for (i = 0; i < (1 << order); i++)
page_kasan_tag_set(page + i, tag);
/* 在这里设置对应影子内存的值 */
kasan_unpoison_shadow(page_address(page), PAGE_SIZE << order);
}
void kasan_free_pages(struct page *page, unsigned int order)
{
#define KASAN_FREE_PAGE 0xFF
if (likely(!PageHighMem(page)))
kasan_poison_shadow(page_address(page),
PAGE_SIZE << order,
KASAN_FREE_PAGE);
}
2.2.2.2 SLUB内存分配器检测
TBD
2.2.2.3 全局变量检测
开启kasan后,编译器会自动识别全局变量,进行初始化,最终调用__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. */
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
};
static void register_global(struct kasan_global *global)
{
/* 按照全局变量的大小向8取整,假设size=4,那么对齐大小为8 */
size_t aligned_size = round_up(global->size, KASAN_SHADOW_SCALE_SIZE);
/* 全局量的起始地址+大小 设置初值,那么这里就是设置0x4 */
kasan_unpoison_shadow(global->beg, global->size);
#define KASAN_GLOBAL_REDZONE 0xF9
/* 对地址+对齐为起始地址 假设地址=0,那么这里起始地址就是8,到红区结束填充0Xf9 */
kasan_poison_shadow(global->beg + aligned_size,
global->size_with_redzone - aligned_size,
KASAN_GLOBAL_REDZONE);
}
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);
2.2.2.4 栈内存检测
TBD
2.2.2.5 影子内存标记总结
类型 | 影子内存标记 | 检测类型 |
buddy | 初始化:0 释放:0xff | use_after_free |
slub | TBD | TBD |
global | 初始化:0xf9 | global-out-of-bounds |
stack | TBD | TBD |
2.2.3 影子内存映射
TBD