kasan原理
- 调试
- 2024-05-24
- 248热度
- 0评论
kasan是什么?(基本原理)
kasan是用于内存检测的工具,能够检测内存以下异常。
- buffer-overflow in heap,stack and globals
- use-after-free
- uninitialized-memory-read
- user-memory-access
如若要支持kasan需要多划分1/8的内存用于内存检测的管理,如上图所示。分配的内存地址都是按字节对齐的,这样做为了提高cpu的效率,本章默认是8字节对齐。
如何实现分配内存地址对齐了?首先在初始化内存时,会在前后舍弃一定的字节数,保证整个地址空间起始和结束地址是8字节对齐的,在分配内存时,分配的大小也按照字节对齐来分配,如分配3字节时,会自动补齐5字节,即实际分配到的空间是8字节,只是5字节对申请者不可见,这样就可以实现分配的内存地址是按8字节对齐的了。
因为内存分配都是按照8字节对齐的,而用户申请的空间并不是按照8字节来,正如上所说请求分配了3字节,实际分配到8字节,剩余5字节对用户不可见,当用户写到了该5字节的内存也是不合法,所以了对应内存的状态一共有以下9种,影子区1字节就可以记录这9种状态。
- 0字节可访问:说明这8字节内存都不可访问,影子区记录为-1。
- 前K字节可以访问:K值记录在影子区,如当K为2,表示0~1字节可访问。K取值为1~7。
- 全都可以访问:影子区写0。
从上可知,对访问内存合法性的可以根据影子区的值来判断。判断公式如下:
(1) 对于分配的内存是8字节情况
*a = ...
char *shadow = ( a>> 3) + offset;
if (*shadow)
report_error(a);
*a = ....
(2)对于分配内存小于8字节情况
*a = ...
char *shadow = (a >> 3) + Offset;
if (*shadow && *shadow < (a & 7) + N)
report_error(a);
*a = ...
示例如上图,依旧是用户申请了3个字节空间,shadow = 3。当客户从第2个空间访问2字节时,但是shadow < 1 + 2,条件不成立,因此合法。如果从2个空间访问3字节时,*shadow < 1 + 3,条件成立,即为非法。
通过上面的方法可以解决use-after-free的问题,但是还不能解决buffer-overflow的问题。解决buffer-overflow,就是在分配内存的前后填充redzone,这段填区也将进行影子区映射,那么当访问越界时就会检查到影子区的内存,提示非法,实现原理参考下图所示。
简单小结一下kasan的基本原理:
- 内存分配地址和空间都是8n字节对齐的,本章默认8字节对齐。
- 总内存被分为n份,每份大小为8字节内存,每份用1字节内存标识,形成一一映射关系,标识区称为影子区域。
- 当请求字节不是8的整数倍时,如分配了3字节,实际分配的也是8字节,仅是另外5字节对分配者非法。因此8字节内存的访问权限一共有9种状态,其当前的状态在分配内存时记录到映射的影子区域中。
- 在分配内存时,为了支持越界访问操作,会在请求内存的前后填充red zone,red zone一般前后各8字节,也有对应的影子区域,该值填充0xfe。
- 编译器会在指令访问(读写)内存时,进行自动插桩代码,检查要访问的目标内存对应的影子区,判断是否合法,当检查非法时就会报错。
kasan何时设置影子区
(1)malloc的时候会分配2* redsize + wantsize,并对内存进行映射填充值。 redzone映射区域填充0xfe,读写区若8字节对齐则全填充0x0;若不对齐,余下不足8字节的实际值填充到最后1字节的影子区域。内存实际分配的还是8字节对齐的,只是对应应用来说,可访问的不足8字节,剩下的当redzone。
(2)free的时候对齐对应的影子区填充0xff。
申请内存
malloc
_malloc_r
pvPortMalloc
__internal_malloc
kasan_malloc_small //addr = 0xc178938, size = 16
kasan_unpoison_shadow
kasan_poison_shadow(address, size, 0); //分配空间对应的地址设置为0
shadow_start = kasan_mem_to_shadow(address);
shadow_end = kasan_mem_to_shadow(address + size);
memset(shadow_start, value, shadow_end - shadow_start);
kasan_poison_shadow(left_rz, sizeof(debug_magic.redzone), KASAN_KMALLOC_REDZONE);
//设置内存左redzone
kasan_poison_shadow(right_rz, sizeof(debug_magic.redzone), KASAN_KMALLOC_REDZONE);
//设置内存右redzone
//实际分配内存左右预留一个空间,然后这预留的空间对应的shadow区域也要填充值,假设分配了16字节,实际分配了8+16+8=32字节空间。然后对应的shadow区域是1+2+1字节大小。在heap4中malloc函数中,会多分配16字节长度,如下。
_internal_malloc(size_t xWanteSize)
xWantSize +=2*xSlabDegbuMgicSize;
申请内存,前后的内存redzone对应的shadow区域写0xFE,表示表示红区。实际内存对应的shadow区域写0,表示内存已经被申请,表示可写。
释放内存
free
__internal_free
kasan_free_large
kasan_poison_shadow(page, size, KASAN_FREE_PAGE);
//设置0xff到对应的shadow区域,表示该内存已经释放。不能再写了。
kasan如何检查内存合法性
初始化
初始化部分kasan_early_init/kasan_init/do_ctors
kasan_early_init
kasan_shadow_init_nommu
for()
*(xxx) = 0
将所有的shadaow memory设置为0
kasan_init
kasan_init_nommu
kasan_init_report
kasan_flags |= KASAN_REPORT_INIT_FLAG; //设置kasan标志位
kasan_enable_report
kasan_flags |= KASAN_REPORT_SHOW_FLAG;
rt_malloc_small_sethook(rt_malloc_small_func_hook);
rt_malloc_small_hook = hook; //设置回调函数,heap4 __internal_malloc的时候调用
rt_free_small_sethook(rt_free_small_func_hook);
rt_free_small_hook = hook; //设置回调函数,heap4 __internal_free的时候调用
影子区映射
adress:0+CONFIG_ARCH_MEM_LENGTH -------------------
|XXXXXXXXXXXXXXXXXX|
adress:0+x -------------------- KASAN_SHADOW_START
| |
| |
| |
adress:0 ------------------- CONFIG_ARCH_START_ADDRESS
#define KASAN_SHADOW_SIZE (CONFIG_ARCH_MEM_LENGTH>>3) // 相当于除以8
#define KASAN_SHADOW_START (CONFIG_ARCH_START_ADDRESS + CONFIG_ARCH_MEM_LENGTH - KASAN_SHADOW_SIZE)
#define KASAN_SHADOW_OFFSET (KASAN_SHADOW_START - (CONFIG_ARCH_START_ADDRESS>>3))
static inline void *kasan_mem_to_shadow(const void *addr)
{
return (void *)((unsigned long)addr >> KASAN_SHADOW_SCALE_SHIFT)
+ KASAN_SHADOW_OFFSET;
实际= KASAN_SHADOW_START + addr >> KASAN_SHADOW_SCALE_SHIFT - CONFIG_ARCH_START_ADDRESS>>3
addr肯定比CONFIG_ARCH_START_ADDRESS要大,也就是说,shadow的开始地址要先减掉
arch_start之前的地址,这样避免浪费空间。
}
通过分配的地址查找对应shadow地址,内存分配是8字节对齐分配,如分配3字节,实际也是8字节空间。
__asan_load/store定义
#define DEFINE_ASAN_LOAD_STORE(size) \\
void __asan_load##size(unsigned long addr) \\
{ \\
KASAN_CHECK_ADDR_FILTER(addr);\\
check_memory_region_inline(addr, size, false, _RET_IP_);\\
} \\
__alias(__asan_load##size) \\
void __asan_load##size##_noabort(unsigned long); \\
void __asan_store##size(unsigned long addr) \\
{ \\
KASAN_CHECK_ADDR_FILTER(addr);\\
check_memory_region_inline(addr, size, true, _RET_IP_); \\
} \\
__alias(__asan_store##size) \\
void __asan_store##size##_noabort(unsigned long); \\
DEFINE_ASAN_LOAD_STORE(1);
DEFINE_ASAN_LOAD_STORE(2);
DEFINE_ASAN_LOAD_STORE(4);
DEFINE_ASAN_LOAD_STORE(8);
DEFINE_ASAN_LOAD_STORE(16);
上面的函数展开后得到
void __asan_load1(unsigned long addr)
{
KASAN_CHECK_ADDR_FILTER(addr);
check_memory_region_inline(addr, 1, false, _RET_IP_);
}
__alias(__asan_load1)
void __asan_load1_noabort(unsigned long);
void __asan_store1(unsigned long addr)
{
KASAN_CHECK_ADDR_FILTER(addr);
check_memory_region_inline(addr, 1, true, _RET_IP_);
}
__alias(__asan_store1)
void __asan_store1_noabort(unsigned long);
void __asan_load2(unsigned long addr)
{
KASAN_CHECK_ADDR_FILTER(addr);
check_memory_region_inline(addr, 2, false, _RET_IP_);
}
__alias(__asan_load2)
void __asan_load2_noabort(unsigned long);
void __asan_store2(unsigned long addr)
{
KASAN_CHECK_ADDR_FILTER(addr);
check_memory_region_inline(addr, 2, true, _RET_IP_);
}
__alias(__asan_store2)
void __asan_store2_noabort(unsigned long);
void __asan_load4(unsigned long addr)
{
KASAN_CHECK_ADDR_FILTER(addr);
check_memory_region_inline(addr, 4, false, _RET_IP_);
}
__alias(__asan_load4)
void __asan_load4_noabort(unsigned long);
void __asan_store4(unsigned long addr)
{
KASAN_CHECK_ADDR_FILTER(addr);
check_memory_region_inline(addr, 4, true, _RET_IP_);
}
__alias(__asan_store4)
void __asan_store4_noabort(unsigned long);
void __asan_load8(unsigned long addr)
{
KASAN_CHECK_ADDR_FILTER(addr);
check_memory_region_inline(addr, 8, false, _RET_IP_);
}
__alias(__asan_load8)
void __asan_load8_noabort(unsigned long);
void __asan_store8(unsigned long addr)
{
KASAN_CHECK_ADDR_FILTER(addr);
check_memory_region_inline(addr, 8, true, _RET_IP_);
}
__alias(__asan_store8)
void __asan_store8_noabort(unsigned long);
void __asan_load16(unsigned long addr)
{
KASAN_CHECK_ADDR_FILTER(addr);
check_memory_region_inline(addr, 16, false, _RET_IP_);
}
__alias(__asan_load16)
void __asan_load16_noabort(unsigned long);
void __asan_store16(unsigned long addr)
{
KASAN_CHECK_ADDR_FILTER(addr);
check_memory_region_inline(addr, 16, true, _RET_IP_);
}
__alias(__asan_store16)
void __asan_store16_noabort(unsigned long);
__alias是给函数起一个别名,从上可知,定义了如下几个函数的实现。
__asan_loadx
__asan_loadx
__asan_loadx_noabort
其中x为1/2/4/8/16
__asan_loadx
{
KASAN_CHECK_ADDR_FILTER(addr);
unsigned long t = (unsigned long)addr;
if ((t < CONFIG_ARCH_START_ADDRESS) || (t > (CONFIG_ARCH_START_ADDRESS + CONFIG_ARCH_MEM_LENGTH)))
return ;
上面代码的意思就是,只对特定范围的地址做检测,不再该范围的不检查,比如XIP的代码,没必要检测。
check_memory_region_inline(addr, X, true, _RET_IP_);
}
检测
1/2/4/8/16字节
以4字节的来做示例分析,其他的类似。
static __always_inline bool memory_is_poisoned_4(unsigned long addr)
{
uint8_t *shadow_addr = (uint8_t *)kasan_mem_to_shadow((void *)addr);
//获取影子区的值
//如果影子区的值不等于继续检查,可能是不对齐的情况,比如分配了3字节,有5字节属于redzone
if (unlikely(*shadow_addr))
{
//判断第4个字节是否可访问,如果不可访问说明非法。
if (memory_is_poisoned_1(addr + 3))
{
return true;
}
/*
* If single shadow byte covers 4-byte access, we don't
* need to do anything more. Otherwise, test the first
* shadow byte.
*/
//如果第4字节为合法,满足*shadow >= (addr & 7) + N 则合法,否则非法。
if (likely(((addr + 3) & KASAN_SHADOW_MASK) >= 3))
{
return false;
}
return unlikely(*(uint8_t *)shadow_addr);
}
return false;
}
static __always_inline bool memory_is_poisoned_1(unsigned long addr)
{
int8_t shadow_value = *(int8_t *)kasan_mem_to_shadow((void *)addr);
//获取影子区的值
//如果影子区的值不等于0,即进一步判断
if (unlikely(shadow_value))
{
//判断原理与前面算法一致: *shadow < (addr & 7) + N为非法,否则为合法。
int8_t last_accessible_byte = addr & KASAN_SHADOW_MASK;
return unlikely(last_accessible_byte >= shadow_value);
}
return false;
}
N字节
* 判断内存对应的影子内存中,起始和结束shadow值是否都为 0
结束地址是对应地址长度的影子地址的下一个影子地址 */
ret = memory_is_zero(kasan_mem_to_shadow((void *)addr),
kasan_mem_to_shadow((void *)addr + size - 1) + 1);
if (ret)
{
unsigned long last_byte = addr + size - 1;
int8_t *last_shadow = (int8_t *)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全局变量实现
(1)当使能使能了asan-globals=1参数后,编译器会自动为每个全局变量填充red_zone。 redzone的大小为63-(size-1) (2)填充redzone后,编译器会为每个变量自动生成一个xxx_name的构造函数,该构造函数会调用__asan_register_globals进行注册,将全局变量的内存区域与影子区域建立映射并填充影子区的值,填充方法与heap一致。
以上操作全是编译器自动行为,应用层最终调用do_ctors回调调用构造函数即可。访问时判断内存合法性与前面算法一致。
全局变量影子区初始化
do_ctors
*fn = &_ctors_start
for(;fn< _ctors_end;fn++)
(*fn)();
调用_ctors_start和_ctors_end直接的回调函数,用于初始化全局变量的shadow
上面的地址在链接脚本的
#if (defined(CONFIG_KASAN))
/* .ctors */
. = ALIGN(8);
__ctors_start__ = .;
KEEP(*(.ctors))
KEEP(*(SORT(.init_array.*)))
KEEP(*(.init_array))
__ctors_end__ = .;
#endif
先调用do_ctors,调用每个工具链为每个变量生成的构造函数,构造函数调用__asan_register_globals。
static void register_global(struct kasan_global *global)
{
KASAN_CHECK_ADDR_FILTER(global->beg);
size_t aligned_size = round_up(global->size, KASAN_SHADOW_SCALE_SIZE);
kasan_unpoison_shadow(global->beg, global->size);
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]);
}
}
kasan局部变量实现
- 左边填充32字节,右边填充63-(size-1)
- shadow区域计算公式: shadow = (addr >>3) +koffset。
- koffset由编译参数-fasan-shadow-offset=xxx指定。
- shadow区域
KSAN_STACK_可读写区 0x00
KASAN_STACK_LEFT 0xF1
KASAN_STACK_MID 0xF2 --只有一个变量且是32字节对齐的,不会填充
KASAN_STACK_RIGHT 0xF3
KASAN_STACK_PARTAL 0xF4
示例: (int *) (0xc18a6d0 >> 3 + 0xaf00000) = KASAN_STACK_LEFT
以上操作全是编译器自动行为,用户只需要使能编译参数即可。
void foo() {
char rz1[32]; // 32-byte aligned
char a[328];
char rz2[24];
char rz3[32];
int *shadow = (&rz1 >> 3) + kOffset; //计算变量映射shadow区的起始地址
shadow[0] = 0xffffffff; // poison rz1
shadow[11] = 0xffffff00; // poison rz2
shadow[12] = 0xffffffff; // poison rz3
<------------- CODE ------------->
shadow[0] = shadow[11] = shadow[12] = 0;
}
官方解释算法如上,当工具链接加上--param asan-stack=1,局部变量redzone填充以及shadow映射填充值全工具链自动完成。对应局部变量需要左右都需要填充redzone,所以可以划分为以下4个部分。
- left redzone: 32字节。
- 变量长度:实际的变量长度
- mid redzone:该区域有两个用途①是补齐变量长度,让其32字节对齐。②是当存在多个变量时,用于中间区域的隔离。当变量长度不是32字节对齐的,填充长度是变量长度+mid redzone长度能够32字节对齐。mid redzone= 63-(size-1)
- right redzone:32字节。
对于上面4个内存区域,也可以将shadow映射区划分为4个部分
- left redzone shadow: 填充0xF1
- 变量长度:都填充0x0;
- mid redzone: 填充0xF2
- right redzone: 填充0xF3;
注意: 当只有一个变量且该变量长度是32字节对齐,则没有mid redzone区域。
下面来段实验,定义了这么一段代码。
void test_asan_xxx(void)
{
char a[328] = {1};
memset(a,0x0,328);
}
下面是反汇编的结果
void test_asan_xxx(void)
{
c0efd34: 7121 addi sp,sp,-448 //开辟448空间内存
c0efd36: f1f1f7b7 lui a5,0xf1f1f //a5=0xf1f1f<<12
c0efd3a: fb22 sd s0,432(sp) //将s0存储到sp+432位置
c0efd3c: f726 sd s1,424(sp) //将s1存储到sp+424位置
c0efd3e: f34a sd s2,416(sp) //将s2存储到sp+416位置
c0efd40: ff06 sd ra,440(sp) //将ra存储到sp+440位置
//开辟一个栈空间为448字节,并且将a5=0xf1f1f000
//将s0,s1,s2,ra压入栈中。
c0efd42: 840a mv s0,sp //s0指向sp,即填充32字节的地址
c0efd44: 1f17879b addiw a5,a5,497 //a5 = 0xf1f1f1f1
c0efd48: 800d srli s0,s0,0x3 //s0 = s0 >> 3
c0efd4a: 0af00937 lui s2,0xaf00 //s2 = 0xaf00000,即koffset
c0efd4e: 4089578b srw a5,s2,s0,0 //*(s0>>0 + s2)=a5
//写KASAN_STACK_LEFT
//对变量的前32字节 shadow映射区填充0xf1f1f1f1(8字节映射1字节)
//int *shadow = (sp >> 3) + koffset;
//shadow区域是按照4字节对齐存储,对应的变量就是32字节。
// shadow[0] = 0xf1f1f1f1
c0efd52: f2f2f7b7 lui a5,0xf2f2f //a5 = 0xf2f2f000
c0efd56: 2007879b addiw a5,a5,512 //a5 = 0xf2f2f200
c0efd5a: 008904b3 add s1,s2,s0 //s1 = s2 +s0,即32字节映射区地址
c0efd5e: d4dc sw a5,44(s1) //从32字节映射区偏移44字节写入0xf2f2f200
//写KASAN_STACK_MID(包含数据区对齐32字节后剩余部分的shadow)
//因为数组长度是328字节,对应的shadow是41字节,shodow是每4字节存储,所以剩余1字节
//就随着REDZONE映射shadow区域一起填充,余下的3字节就填充f2。
//所以STACK_MID就是偏移44的位置。
c0efd60: f3f3f7b7 lui a5,0xf3f3f
c0efd64: 3f37879b addiw a5,a5,1011
c0efd68: d89c sw a5,48(s1) //0xc73150a地址内容=0xf3f3f3f3
//写KASAN_STACK_RIGHT
//最后余下32字节redzone,对应4字节填充f3
char a[328] = {1};
memset(a,0x0,328);
c0efd6a: 1008 addi a0,sp,32
c0efd6c: 14800613 li a2,328
c0efd70: 4581 li a1,0
c0efd72: acfc20ef jal ra,c0b2840 <memset>
{
c0efd76: 4089500b srw zero,s2,s0,0
}
c0efd7a: 70fa ld ra,440(sp)
c0efd7c: 745a ld s0,432(sp)
{
c0efd7e: 0204b623 sd zero,44(s1)
}
c0efd82: 791a ld s2,416(sp)
c0efd84: 74ba ld s1,424(sp)
c0efd86: 6139 addi sp,sp,448
c0efd88: 8082 ret
当定义两个变量时示例
void test_asan_xxx(void)
{
char a[328] = {1};
char b[144] = {1};
memset(a,0x0,328);
memset(b,0x0,128);
}
left值依旧是32字节没有变化,在a数组和b数组中间填充了48字节的mid redzone,其中mid[16]字节是为了对齐b数组让其32字节对齐的,而mid[32]是用于隔离a[328]和b[144]数组的。