Canary 一词意为金丝雀,相传以前矿工们会携之下矿井,一旦金丝雀死亡则说明矿井中存在瓦斯泄露的情况。而 Stack Canary 的作用也类似:每开辟一个新栈帧时,先在毗邻上一栈帧处设置一个特定值,然后在每次函数返回时检查一下该值是否被改变;如被改变,则说明栈被越界写所破坏。
用户态程序
上面提到的关于 Canary 值的设置、校验,以及出错后的处理,这些动作的代码逻辑都是由编译器负责生成并嵌入到可执行文件中的。用户需要做,或者说可以做的事情只有:
- 需要添加
-fstack-protector
/-fstack-protector-strong
编译参数。 - 可以在代码中定义
__stack_chk_guard
全局变量,以自定义 Canary 的值;如未配置,则默认使用 C 库中定义的默认值。 - 可以在代码中定义
__stack_chk_fail()
函数,以自定义 Canary 检查失败后程序的行为。如未配置,则默认使用 C 库提供的默认实现。
已有定义:
- glibc:glibc/debug/stack_chk_fail.c
- Android bionic:libc/bionic/__stack_chk_fail.cpp
Linux 内核
由于 Linux 内核的编译和运行环境没有 C 库的支持,因此上述的 __stack_chk_guard
和 __stack_chk_fail()
都需要由内核自行定义。
此外,上述的 __stack_chk_guard
相当于全局共用同一个 Canary 值,而 x86 和 arm64 还支持 Per-task Canary:添加 -mstack-protector-guard=sysreg
编译参数,并由 -mstack-protector-guard-{reg,offset}
编译参数来指定获取 Canary 时用的基准寄存器和偏移量。
1# arch/arm64/Makefile
2ifeq ($(CONFIG_STACKPROTECTOR_PER_TASK),y)
3prepare: stack_protector_prepare
4stack_protector_prepare: prepare0
5 $(eval KBUILD_CFLAGS += -mstack-protector-guard=sysreg»-»------- \
6 -mstack-protector-guard-reg=sp_el0»----- \
7 -mstack-protector-guard-offset=$(shell»- \
8 awk '{if ($$2 == "TSK_STACK_CANARY") print $$3;}' \
9 include/generated/asm-offsets.h))
10endif
这里的原理是:当一个线程陷入内核态执行后,一些体系结构中会有一个寄存器指向一个该线程私有的内存区域/数据结构,例如 Thread Local Storage(TLS)或者是 struct task_struct
,以此来保存或者获取一些独属于该线程的信息:如 arm64 上的 sp_el0
和 x86 上的 fs
。因此我们只需要把 Canary 添加到线程私有区域中,就可以通过基准 + 偏移的方式获取到。
所以总的来说,不论是用户态程序还是内核,只要把相关的函数、变量通过编译器钩子/参数给到编译器即可,编译器会帮你做剩下的事情。
参考资料
- Stack Smashing Protection (GNU Compiler Collection (GCC) Internals)
- ARM64 Per-task stack canary 代码作者的讲解博客
- 关于
-mstack-protector-guard
的文档:AArch64 ,x86