一 启用方式
打开配置CONFIG_CC_STACKPROTECTOR,重新编译内核即可。
二 工作原理
搜索配置CONFIG_CC_STACKPROTECTOR产生的影响,可得到如下结果:
1.include/linux/stackprotector.h
#ifdef CONFIG_CC_STACKPROTECTOR
# include <asm/stackprotector.h>
#else
static inline void boot_init_stack_canary(void)
{
}
#endif
其中asm/stackprotector.h文件内容:
extern unsigned long __stack_chk_guard;
/*
* Initialize the stackprotector canary value.
*
* NOTE: this must only be called from functions that never return,
* and it must always be inlined.
*/
static __always_inline void boot_init_stack_canary(void)
{
unsigned long canary;
/* Try to get a semi random initial value. */
get_random_bytes(&canary, sizeof(canary));
canary ^= LINUX_VERSION_CODE;
current->stack_canary = canary;
__stack_chk_guard = current->stack_canary;
}
2.kernel/fork.c
#ifdef CONFIG_CC_STACKPROTECTOR
tsk->stack_canary = pax_get_random_long();
#endif
3.kernel/panic.c
#ifdef CONFIG_CC_STACKPROTECTOR
/*
* Called when gcc's -fstack-protector feature is used, and
* gcc detects corruption of the on-stack canary value
*/
void __stack_chk_fail(void)
{
dump_stack();
panic("stack-protector: Kernel stack is corrupted in: %pA\n",
__builtin_return_address(0));
}
EXPORT_SYMBOL(__stack_chk_fail);
#endif
4.arch/mips/kernel/process.c
#ifdef CONFIG_CC_STACKPROTECTOR
#include <linux/stackprotector.h>
unsigned long __stack_chk_guard __read_mostly;
EXPORT_SYMBOL(__stack_chk_guard);
#endif
5.include/linux/sched.h
struct task_struct {
......
#ifdef CONFIG_CC_STACKPROTECTOR
/* Canary value for the -fstack-protector gcc feature */
unsigned long stack_canary;
#endif
....
}
6.arch/mips/kernel/r4k_switch.s
LEAF(resume)
....
#if defined(CONFIG_CC_STACKPROTECTOR) && !defined(CONFIG_SMP)
PTR_LA t8, __stack_chk_guard
LONG_L t9, TASK_STACK_CANARY(a1)
LONG_S t9, 0(t8)
#endif
7.arch/mips/makefile
ifdef CONFIG_CC_STACKPROTECTOR
KBUILD_CFLAGS += -fstack-protector
endif
这些搜索结果可以总结为:定义并设置了变量__stack_chk_guard,定义了函数__stack_chk_fail(),但是发现__stack_chk_guard设置后并没有使用,函数__stack_chk_fail()也没有被调用,顿时线索断了。
但是他们既然被定义了,就一定会被使用到,并且内核不会依赖于其他的库文件,于是打算从obj文件下手,随便找了个init/mounts.o,查看了下重定位表,如下所示:
RELOCATION RECORDS FOR [.init.text]:
OFFSET TYPE VALUE
.......
00000120 R_MIPS_HI16 __stack_chk_guard
00000124 R_MIPS_LO16 __stack_chk_guard
........
00000344 R_MIPS_HI16 __stack_chk_guard
0000034c R_MIPS_LO16 __stack_chk_guard
00000358 R_MIPS_26 __stack_chk_fail
00000388 R_MIPS_HI16 __stack_chk_guard
00000390 R_MIPS_LO16 __stack_chk_guard
从重定位表看__stack_chk_fail和__stack_chk_guard确实是使用到了,查看对应的反汇编结果:
Disassembly of section .init.text
.........
00000120 <mount_block_root>:
120: 3c020000 lui v0,0x0
124: 8c420000 lw v0,0(v0)
128: 27bdff98 addiu sp,sp,-104
12c: afa2003c sw v0,60(sp)
130: 3c020000 lui v0,0x0
134: afb70060 sw s7,96(sp)
138: 0080b821 move s7,a0
13c: 8c440000 lw a0,0(v0)
140: afb2004c sw s2,76(sp)
..........
33c: 0c000000 jal 0 <readonly>
340: 02002821 move a1,s0
344: 3c020000 lui v0,0x0
348: 8fa3003c lw v1,60(sp)
34c: 8c420000 lw v0,0(v0)
350: 10620003 beq v1,v0,360 <mount_block_root+0x240>
354: 8fbf0064 lw ra,100(sp)
358: 0c000000 jal 0 <readonly>
35c: 00000000 nop
360: 8fb70060 lw s7,96(sp)
364: 8fb6005c lw s6,92(sp)
368: 8fb50058 lw s5,88(sp)
36c: 8fb40054 lw s4,84(sp)
打开init/mounts.c文件,找到mount_block_root()函数,发现并没有调用__stack_chk_fail和__stack_chk_guard,顿时就明白了,这个应该跟上面的搜索结果7,编译选项-fstack-protector相关,通过对比着看汇编代码,在函数入口:
120: 3c020000 lui v0,0x0
124: 8c420000 lw v0,0(v0)
128: 27bdff98 addiu sp,sp,-104
12c: afa2003c sw v0,60(sp)
将__stack_chk_guard的值存入栈顶,然后在函数返回之前:
344: 3c020000 lui v0,0x0
348: 8fa3003c lw v1,60(sp)
34c: 8c420000 lw v0,0(v0)
350: 10620003 beq v1,v0,360 <mount_block_root+0x240>
取出保存在当前栈顶的值与__stack_chk_guard进行比较,如果相等,则跳转到360,顺利返回,否则继续顺序执行:
354: 8fbf0064 lw ra,100(sp)
358: 0c000000 jal 0 <readonly>
通过上面查看到的重定位表,00000358 R_MIPS_26 __stack_chk_fail ,调用到的就是__stack_chk_fail ()这个函数,至此,整个过程应该非常明了了,编译选项-fstack-protector在函数编译时插入一些代码,函数入口在栈顶存储__stack_chk_guard的值,在函数返回之前取出存放在栈顶的值和__stack_chk_guard进行比较,如果内存溢出修改了这个存放的值,则比较的结果不相等,调用__stack_chk_fail,对于内核__stack_chk_fail()函数直接宕机。
三 补充说明
通过上面的推断,发现比较关键的是__stack_chk_guard这个值的安全性,这个值是在内核启动的时候,调用boot_init_stack_canary()函数随机生成的,所以理论上来说这个值是不可猜测的。另外每个进程都对应一个不同的值:tsk->stack_canary = pax_get_random_long();这样进一步增加了安全性,在每次进程切换时设置将要运行的进程的stack_canary到__stack_chk_guard,但是对于多核处理器,由于__stack_chk_guard只存了一份,并不是per cpu变量,所以__stack_chk_guard的值并不会在进程切换时改变,因为假设某个cpu核发生进程切换,重新设置改变了__stack_chk_guard的值,则此时正在内核中运行的其他cpu核可能在函数返回时检查不过二导致系统宕机。至于__stack_chk_guard为啥不搞成per cpu变量,这个就很好理解了,因为gcc只会引用__stack_chk_guard这个变量。