Linux内核KASAN实现原理详解

1. 前言
kasan是一个动态检测内存错误的工具。kasan可以检测全局变量、栈、堆分配的内存发生越界访问等问题。功能比slub debug齐全并且支持实时检测。越界访问的严重性和危害性通过我之前的文章(slub debug技术)应该有所了解。正是由于slub debug缺陷,因此我们需要一种更加强大的检测工具。难道你不想吗?kasan就是其中一种。kasan的使用真的很简单。但是我是一个追求刨根问底的人。仅仅止步于使用的层面,我是不愿意的,只有更清楚的了解实现原理才能更加熟练的使用工具。不止是kasan,其他方面我也是这么认为。但是,说实话,写这篇文章是有点底气不足的。因为从我查阅的资料来说,国内没有一篇文章说kasan的工作原理,国外也是没有什么文章关注kasan的原理。大家好像都在说how to use。由于本人水平有限,就根据现有的资料以及自己阅读代码揣摩其中的意思。本文章作为抛准引玉,如果有不合理的地方还请指正。
注:文章代码分析基于linux-4.15.0-rc3。
2. 简介
kerneladdresssanitizer(kasan)是一个动态检测内存错误的工具。它为找到use-after-free和out-of-bounds问题提供了一个快速和全面的解决方案。kasan使用编译时检测每个内存访问,因此您需要gcc 4.9.2或更高版本。检测堆栈或全局变量的越界访问需要gcc 5.0或更高版本。目前kasan仅支持x86_64和arm64架构(linux 4.4版本合入)。你使用arm64架构,那么就需要保证linux版本在4.4以上。当然了,如果你使用的linux也有可能打过kasan的补丁。例如,使用高通平台做手机的厂商使用linux 3.18同样支持kasan。
3. 如何使用
使用kasan工具是比较简单的,只需要添加kernel以下配置项。
config_slub_debug=y
config_kasan=y
为什么这里必须打开slub_debug呢?是因为有段时间kasan是依赖slubu_debug的,什么意思呢?就是在kconfig中使用了depends on,明白了吧。不过最新的代码已经不需要依赖了,可以看下提交。但是我建议你打开该选项,因为log可以输出更多有用的信息。重新编译kernel即可,编译之后你会发现boot.img(android环境)大小大了一倍左右。所以说,影响效率不是没有道理的。不过我们可以作为产品发布前的最后检查,也可以排查越界访问等问题。我们可以查看内核日志内容是否包含kasan检查出的bugs信息。
4. kasan是如何实现检测的?
kasan的原理是利用额外的内存标记可用内存的状态。这部分额外的内存被称作shadow memory(影子区)。kasan将1/8的内存用作shadow memory。使用特殊的magic num填充shadow memory,在每一次load/store(load/store检查指令由编译器插入)内存的时候检测对应的shadow memory确定操作是否valid。连续8 bytes内存(8 bytes align)使用1 byte shadow memory标记。如果8 bytes内存都可以访问,则shadow memory的值为0;如果连续n(1 =< n > 3) + kasan_shadow_offse);
if (*shadow)
    report_bug();
*addr = 0;
红色区域类似是编译器插入的指令。既然是访问8 bytes,必须要保证对应的shadow mempry的值必须是0,否则肯定是有问题。那么如果访问的是1,2 or 4 bytes该如何检查呢?也很简单,我们只需要修改一下if判断条件即可。修改如下:
if (*shadow && *shadow > 3)  + kasan_shadow_offse。为了将[0xffff_0000_0000_0000, 0xffff_ffff_ffff_ffff]和[0xffff_0000_0000_0000, 0xffff_1fff_ffff_ffff]对应起来,因此计算kasan_shadow_offse的值为0xdfff_2000_0000_0000。我们将kasan区域放大,如下图所示。
kasan区域仅仅是分配的虚拟地址,在访问的时候必须建立和物理地址的映射才可以访问。上图就是kasan建立的映射布局。左边是系统启动初期建立的映射。在kasan_early_init()函数中,将所有的kasan区域映射到kasan_zero_page物理页面。因此系统启动初期,kasan并不能工作。右侧是在kasan_init()函数中建立的映射关系,kasan_init()函数执行结束就预示着kasan的正常工作。我们将不需要address sanitizer功能的区域同样还是映射到kasan_zero_page物理页面,并且是readonly。我们主要是检测kernel和物理内存是否存在uaf或者oob问题。所以建立kernel和linear mapping(仅仅是所有的物理地址建立的映射区域)区域对应的shadow memory建立真实的映射关系。moudle区域对应的shadow memory的映射关系也是需要创建的,但是映射关系建立是动态的,他在module加载的时候才会去创建映射关系。
4.4. 伙伴系统分配的内存的shadow memory值如何填充?
既然shadow memory已经建立映射,接下来的事情就是探究各种内存分配器向shadow memory填充什么数据了。首先看一下伙伴系统allocate page(s)函数填充shadow memory情况。
假设我们从buddy system分配4 pages。系统首先从order=2的链表中摘下一块内存,然后根据shadow memory address和memory address之间的对应的关系找对应的shadow memory。这里shadow memory的大小将会是2kb,系统会全部填充0代表内存可以访问。我们对分配的内存的任意地址内存进行访问的时候,首先都会找到对应的shadow memory,然后根据shadow memory value判断访问内存操作是否valid。
如果释放pages,情况又是如何呢?
同样的,当释放pages的时候,会填充shadow memory的值为0xff。如果释放之后,依然访问内存的话,此时kasan根据shadow memory的值是0xff就可以断,这是一个use-after-free问题。
4.5. slub分配对象的内存的shadow memory值如何填充?
当我们打开kasan的时候,slub allocator管理的object layout将会放生一定的变化。如下图所示。
在打开slub_debug的时候,object就增加很多内存,kasan打开之后,在此基础上又加了一截。为什么这里必须打开slub_debug呢?是因为有段时间kasan是依赖slubu_debug的,什么意思呢?就是在kconfig中使用了depends on,明白了吧。不过最新的代码已经不需要依赖了,可以看下提交。
当我们第一次创建slab缓存池的时候,系统会调用kasan_poison_slab()函数初始化shadow memory为下图的模样。整个slab对应的shadow memory都填充0xfc。
上述步骤虽然填充了0xfc,但是接下来初始化object的时候,会改变一些shadow memory的值。我们先看一下kmalloc(20)的情况。我们知道kmalloc()就是基于slub allocator实现的,所以会从kmalloc-32的kmem_cache中分配一个32 bytes object。
首先调用kmalloc(20)函数会匹配到kmalloc-32的kmem_cache,因此实际分配的object大小是32 bytes。kasan同样会标记剩下的12 bytes的shadow memory为不可访问状态。根据object的地址,计算shadow memory的地址,并开始填充数值。由于kmalloc()返回的object的size是32 bytes,由于kmalloc(20)只申请了20 bytes,剩下的12 bytes不能使用。kasan必须标记shadow memory这种情况。object对应的4 bytes shadow memory分别填充00 00 04 fc。00代表8个连续的字节可以访问。04代表前4个字节可以访问。作为越界访问的检测的方法。总共加在一起是正好是20 bytes可访问。0xfc是redzone标记。如果访问了redzone区域kasan就会检测out-of-bounds的发生。
当申请使用之后,现在调用kfree()释放之后的shadow memory情况是怎样的呢?看下图。
根据object首地址找到对应的shadow memory,32 bytes object对应4 bytes的shadow memory,现在填充0xfb标记内存是释放的状态。此时如果继续访问object,那么根据shadow memory的状态值既可以确定是use-after-free问题。
4.6. 全局变量的shadow memory值如何填充?
前面的分析都是基于内存分配器的,redzone都会随着内存分配器一起分配。那么global variables如何检测呢?global variable的redzone在哪里呢?这就需要编译器下手了。编译器会帮我们填充redzone区域。例如我们定义一个全局变量a,编译器会帮我们填充成下面的样子。
char a[4];
转换
struct {
char original[4];
char redzone[60];
} a; //32 bytes aligned
如果这里你问我为什么填充60 bytes。其实我也不知道。这个转换例子也是从kasan作者的ppt中拿过来的。估计要涉及编译器相关的知识,我无能为力了,但是下面做实验来猜吧。当然了,ppt的内容也需要验证才具有说服力。尽信书则不如无书。我特地写三个全局变量来验证。发现system.map分配地址之间的差值正好是0x40。因此这里的确是填充60 bytes。 另外从我的测试发现,如果上述的数组a的大小是33的时候,填充的redzone就是63 bytes。所以我推测,填充的原理是这样的。全局变量实际占用内存总数s(以byte为单位)按照每块32 bytes平均分成n块。假设最后一块内存距离目标32 bytes还差y bytes(if s%32 == 0,y = 0),那么redzone填充的大小就是(y + 32) bytes。画图示意如下(s%32 != 0)。因此总结的规律是:redzone = 63 – (s - 1) % 32
全局变量redzone区域对应的shadow memory是在什么填充的呢?又是如何调用的呢?这部分是由编译器帮我们完成的。编译器会为每一个全局变量创建一个函数,函数名称是:_global__sub_i_65535_1_##global_variable_name。这个函数中通过调用__asan_register_globals()函数完成shadow memory标记。并且将自动生成的这个函数的首地址放在.init_array段。在kernel启动阶段,通过以下代调用关系最终调用所有全局变量的构造函数。kernel_init_freeable()->do_basic_setup() ->do_ctors()。do_ctors()代码实现如下:
static void __init do_ctors(void)
{
ctor_fn_t *fn = (ctor_fn_t *) __ctors_start;
for (; fn vmlinux.txt命令得到反编译文件。现在好多重要的信息在vmlinux.txt。现在主要就是查看vmlinux.txt文件。先看一下_global__sub_i_65535_1_smc_num1函数的实现。
汇编和c语言传递参数在arm64平台使用的是x0~x7。通过上面的汇编计算一下,x0=0xffff200009682c50,x1=3。然后调用__asan_register_globals()函数,x0和x1就是传递的参数。我们看一下__asan_register_globals()函数实现。
void __asan_register_globals(struct kasan_global *globals, size_t size)
{
int i;
for (i = 0; i = 4
struct kasan_source_location *location;
#endif
};
第一个成员beg就是全局变量的首地址。跟上面的分析一致。第二个成员size从上面数据看出是7,正好对应我们定义的smc_num3[7],正好7 bytes。size_with_redzone的值是0x40,正好是64。根据上面猜测redzone=63-(7-1)%32=57。加上size正好是64,说明之前猜测的redzone计算方法没错。name成员对应的地址是ffff2000092bd6d0。看下ffff2000092bd6d0存储的是什么。
所以name就是全局变量的名称转换成字符串。同样的方式得到module_name的地址是ffff2000092bd6b8。继续看看这段地址存储的数据。
一目了然,module_name是文件的路径。has_dynamic_init的值就是0,这是c++需要的。我用的gcc版本是5.0左右,所以这里的kasan_abi_version=4。这里location成员的地址是ffff200009682c20,继续追踪该地址的数据。 ffff200009682c20 b8d62b09 0020ffff 0e000000 0f000000 解析这段数据之前要先了解struct kasan_source_location结构体。
/* the layout of struct dictated by compiler */
struct kasan_source_location {
const char *filename;
int line_no;
int column_no;
};
第一个成员filename地址是ffff2000092bd6b8和module_name一样的数据。剩下两个数据分别是14和15,分别代表全局变量定义地方的行号和列号。现在回到上面我定义变量的截图,仔细数数列号是不是15,行号截图中也有哦!特地截出来给你看的。剩下的struct kasan_global数据就是smc_num1和smc_num2的数据。分析就不说了。前面说_global__sub_i_65535_1_smc_num1函数会被自动调用,该地址数据填充在__ctors_start和__ctors_end之间。现在也证明一下观点。先从system.map得到符号的地址数据。 ffff2000093ac5d8 t __ctors_start ffff2000093ae860 t __ctors_end 然后搜索一下_global__sub_i_65535_1_smc_num1的地址ffff200009381df0被存储在什么位置,记得搜索的关键字是f01d3809 0020ffff。 ffff2000093ae0c0 f01d3809 0020ffff 181e3809 0020ffff 可以看出ffff2000093ae0c0地址处存储着_global__sub_i_65535_1_smc_num1函数地址。这个地址不是正好位于__ctors_start和__ctors_end之间嘛!
现在就剩下__asan_register_globals()函数到底是是怎么初始化shadow memory的呢?以char a[4]为例,如下图所示
a[4]只有4 bytes可以访问,所以对应的shadow memory的第一个byte值是4,后面的redzone就填充0xfa作为越界检测。a[4]只有4 bytes可以访问,所以对应的shadow memory的第一个byte值是4,后面的redzone就填充0xfa作为越界检测。因为这里是全局变量,因此分配的内存区域位于kernel区域。
4.7. 栈分配变量的readzone是如何分配的?
从栈中分配的变量同样和全局变量一样需要填充一些内存作为redzone区域。下面继续举个例子说明编译器怎么填充。首先来一段正常的代码,没有编译器的插手。
void foo()
{
char a[328];
}
再来看看编译器插了哪些东西进去。
void foo() {     char rz1[32];     char a[328];     char rz2[56];     int *shadow = (&rz1 >> 3)+ kasan_shadow_offse;     shadow[0] = 0xffffffff;     shadow[11] = 0xffffff00;     shadow[12] = 0xffffffff; ------------使用完毕--------------     shadow[0] = shadow[11] = shadow[12] = 0; }
红色部分是编译器填充内存,rz2是56,可以根据上一节全局变量的公式套用计算得到。但是这里在变量前面竟然还有32 bytes的rz1。这个是和全局变量的不同,我猜测这里是为了检测栈变量左边界越界问题。蓝色部分代码也是编译器填充,初始化shadow memory。栈的填充就没有探究那么深入了,有兴趣的读者可以自己探究。
5. error log信息包含哪些信息?
从kernel的documentation文档找份典型的kasan bug输出的log信息如下。
输出的信息很丰富,包含了bug发生的类型、slub输出的object内存信息、call trace以及shadow memory的状态值。其中红色信息都是比较重要的信息。我没有写demo历程,而是找了一份log信息,不是我想偷懒,而是锻炼自己。怎么锻炼呢?我想问的是,从这份log中你可以推测代码应该是怎么样的?我可以得到一下信息: 1) 程序是通过kmalloc接口申请内存的; 2) 申请的内存大小是123 bytes,即p = kamlloc(123); 3) 代码中类似往p[123]中写1 bytes导致越界访问的bug; 4) 在3)步骤发生前没有任何的对该内存的写操作; 如果你也能得到以上4点猜测,我觉的我写的这几篇文章你是真的看明白了。首先输出信息是有slub的信息,所以应该是通过kmalloc()接口;在打印的shadow memory的值中,我们看到连续的15个0和一个3,所以申请的内存size就是15x8+3=123;由于是往ffff8800693bc5d3地址写1个字节,并且object首地址是ffff8800693bc558,所以推测是往p[123]写1 byte出问题;由于log中将object中所有的128 bytes数据全部打印出来,一共是127个0x6b和一个0xa5(slub debug文章介绍的内容)。所以我推测在3)步骤发生前没有任何的对该内存的写操作。


Linux应用开发-libjpeg库交叉编译与使用
CDN突破边界 边缘计算竞赛开场
新型树莓派计算模块开发套件已于RS全面接受预定
天线,到底应该怎么摆放?
Verilog任务与函数的区别
Linux内核KASAN实现原理详解
柔性印刷石墨烯基电容式多传感器阵列,用于机器人对目标物体的认知抓取
iphone8什么时候上市?iphone8背盖设计曝光,iphone8的发布能否让全球消费者为之疯狂?
精密放大器:零漂移特性和更宽电源电压及输入电压范围
AS6200C这款数字温度传感器能满足苛刻的系统级精度要求
iPhone7抢购模式开启 可通过运营商渠道第一时间入手
双输出、多相降压型DC/DC控制器
电力变压器的结构和分类介绍
GD32330C-START开发板试用体验:模拟IIC驱动OLED屏
音频格式有哪些
4路网络信号输入光盘硬盘同步刻录录播机
明年鸿蒙生态设备将达8至10亿台!Apple Watch禁售如何解套?大众集团宣布拥抱特斯拉充电标准/热点科技新闻点
增强现实和虚拟现实的区别
plc是什么_单片机是什么_plc和单片机哪个简单好学
黄致列都不信 ,来《我是歌手》踢馆的小AI是个什么鬼?