内核堆栈溢出通常有两种情况。一种是函数调用栈超出了内核栈THREAD_SIZE的大小, 这是栈底越界,另一种是栈上缓冲越界访问,这是栈顶越界。
检测栈底越界
以arm平台为例,内核栈THREAD_SIZE为8K,当调用栈层次过多或某调用栈上分配过大的 空间,就会导致它越界。越界后struct thread_info结构可能被破坏,轻则内核 panic,重则内核数据被覆盖仍继续运行。
检测栈顶越界
对于栈顶越界,gcc提供了支持。打开内核配置CONFIG_CC_STACKPROTECTOR后,会打 开编译选项-fstack-protector.
CC_STACKPROTECT补丁是Tejun Heo在09年给主线kernel提交的一个用来防止内核堆栈溢出的补丁。默认的config是将这个选项关闭的,可以在编译内核的时候, 修改.config文件为CONFIG_CC_STACKPROTECTOR=y来启用。未来飞天内核可以将这个选项开启来防止利用内核stack溢出的0day攻击。这个补丁的防溢出原理是: 在进程启动的时候, 在每个buffer的后面放置一个预先设置好的stack canary,你可以把它理解成一个哨兵, 当buffer发生缓冲区溢出的时候, 肯定会破坏stack canary的值, 当stack canary的值被破坏的时候, 内核就会直接当机。那么是怎么判断stack canary被覆盖了呢? 其实这个事情是gcc来做的,内核在编译的时候给gcc加了个-fstack-protector参数, 我们先来研究下这个参数是做什么用的。
先写个简单的有溢出的程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 |
|
反汇编看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
没什么特别的,我们在加上-fstack-protector参数看看:
1 2 3 4 |
|
这次程序打印了一条堆栈被溢出的信息,然后就自动退出了。
在反汇编看下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
使用-fstack-protector参数后, gcc在函数的开头放置了几条汇编代码:
1 2 3 |
|
将代码段gs偏移0×14内存处的值赋值给了ebp-4, 也就是第一个变量值的后面。
在call完memeset后,有如下汇编代码:
1 2 3 4 5 |
|
在memset后,gcc要检查这个操作是否发生了堆栈溢出, 将保存在ebp-4的这个值与原来的值对比一下,如果不相同, 说明堆栈发生了溢出,那么就会执行stack_chk_fail这个函数, 这个函数是glibc实现的,打印出上面看到的信息, 然后进程退出。
从这个例子中我们可以看出gcc使用了-fstack-protector参数后,会自动检查堆栈是否发生了溢出, 但是有一个前提就是内核要给每个进程提前设置好一个检测值放置在%gs:0×14位置处,这个值称之为stack canary。所以我们可以看到防止堆栈溢出是由内核和gcc共同来完成的。
gcc的任务就是放置几条汇编代码, 然后和%gs:0×14位置处的值进行对比即可。 主要任务还是内核如何来设置stack canary, 也是CC_STACKPROTECTOR补丁要实现的目的, 下面我们仔细来看下这个补丁是如何实现的。
既然gcc硬性规定了stack canary必须在%gs的某个偏移位置处, 那么内核也必须按着这个规定来设置。
对于32位和64位内核, gs寄存器有着不同的功能。
64位内核gcc要求stack canary是放置在gs段的40偏移处, 并且gs寄存器在每cpu变量中是共享的,每cpu变量irq_stack_union的结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
gs_base只是一个40字节的站位空间, stack_canary就紧挨其后。并且在应用程序进出内核的时候,内核会使用swapgs指令自动更换gs寄存器的内容。
32位下就稍微有点复杂了。由于某些处理器在加载不同的段寄存器时很慢, 所以内核使用fs段寄存器替换了gs寄存器。 但是gcc在使用-fstack-protector的时候, 还要用到gs段寄存器, 所以内核还要管理gs寄存器,我们要把CONFIG_X86_32_LAZY_GS选项关闭, gs也只在进程切换的时候才改变。 32位用每cpu变量stack_canary保存stack canary。
1 2 3 4 5 |
|
内核是处于保护模式的, 因此gs寄存器就变成了保护模式下的段选子,在GDT表中也要有相应的设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
GDT表中的第28个表项用来定为stack canary所在的段。
1 2 |
|
GDT_STACK_CANARY_INIT在刚进入保护模式的时候被调用, 这个段描述符项被设置为基地址为0, 段大小设为24,因为只在基地址为0, 偏移为0×14处放置一个4bytes的stack canary, 所以24字节正好。不理解的同学可以看看intel保护模式的手册, 对着段描述符结构一个个看就行了。
在进入保护模式后, start_kernel()会调用boot_init_stack_canary()来初始话一个stack canary。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
随机出了一个值赋值给每cpu变量, 32位是stack_canary, 64位是irq_stack_union。
内核在进一步初始化cpu的时候,会调用setup_stack_canary_segment()来设置每个cpu的GDT的stack canary描述符项:
start_kernel()->setup_per_cpu_areas()->setup_stack_canary_segment:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
在内核刚进入保护模式的时候, stack canary描述符的基地址被初始化为0, 现在在cpu初始化的时候要重新设置为每cpu变量stack_canary的地址, 而不是变量保存的值。通过这些设置当内核代码在访问%gs:0×14的时候, 就会访问stack canry保存的值。注意:setup_stack_canary_segment是针对32位内核做设置, 因为64位内核中的irq_stack_union是每cpu共享的, 不用针对每个cpu单独设置。 然后就可以调用switch_to_new_gdt(cpu);来加载GDT表和加载gs寄存器。
经过上述初始化过程,在内核代码里访问%gs:0×14就可以定位stack canary的值了, 那么每个进程的stack canary是什么时候设置的呢?
在内核启动一个进程的时候, 会把gs寄存器的值设为KERNEL_STACK_CANARY
1 2 3 4 5 6 7 8 9 10 |
|
内核在fork一个进程的时候, 有如下操作:
1 2 3 4 5 6 |
|
随机初始化了一个stack_canary保存在task_struct结构中的stack_canary变量中。当进程在切换的时候, 通过switch宏把新进程的stack canary保存在每cpu变量stack_canary中, 当前进程的stack_canary也保存在一个每cpu变量中,完成stack canary的切换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
|
前面讲过当gcc检测到堆栈溢出的时候, 会调用glibc的stack_chk_fail函数, 但是当内核堆栈发生溢出的时候,不能调用glibc的函数,所以内核自己实现了一个stack_chk_fail函数:
kernel/panic.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
当内核堆栈发生溢出的时候,就会执行stack_chk_fail函数, 内核当机。
这就是这个补丁的原理,不懂的同学请参考: