Linux内核中stack protector检查的原理

Linux内核中的Stack Protector(也称为栈保护或金丝雀保护)是一种安全机制,用于检测内核栈溢出攻击。其核心原理是在函数栈帧中插入一个随机值(金丝雀值),并在函数返回前验证该值是否被篡改。以下是其详细实现原理:


核心原理

  1. 金丝雀(Canary)的插入

    • 编译器在函数栈帧起始位置(紧邻返回地址之前)插入一个随机值(金丝雀)。
    • 示例栈布局:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      High Address
      +------------------+
      | 函数参数 |
      +------------------+
      | 返回地址 |
      +------------------+
      | 保存的寄存器 |
      +------------------+
      | 局部变量 |
      +------------------+
      | **金丝雀值** | <- Stack Protector 插入位置
      +------------------+
      Low Address
  2. 金丝雀值的生成

    • 系统启动时,内核通过boot_init_stack_canary()初始化一个全局随机值(基于TSC时间戳+随机熵池)。
    • 每个CPU核心有独立金丝雀值(通过percpu变量存储),避免多核间冲突。
  3. 编译时插桩

    • 启用CONFIG_STACKPROTECTOR后,编译器(如GCC的-fstack-protector)会在函数入口将金丝雀值复制到栈帧:
      1
      2
      3
      4
      5
      6
      7
      void foo() {
      unsigned long canary = __stack_chk_guard; // 从percpu变量加载
      // ... 函数逻辑 ...
      // 编译器插入的检查代码
      if (canary != __stack_chk_guard)
      __stack_chk_fail();
      }
    • 函数返回前,编译器插入代码验证金丝雀值是否被修改。
  4. 栈溢出检测

    • 攻击者尝试覆盖返回地址时,必须先覆盖金丝雀值
    • 函数返回前发现金丝雀值变化,触发__stack_chk_fail()
  5. 崩溃处理

    • __stack_chk_fail()调用panic("stack-protector: Kernel stack is corrupted")
    • 立即终止系统,防止漏洞利用。

关键实现细节

  • 随机性来源
    • 早期启动阶段:使用rdtsc()时间戳 + jiffies
    • 随机数子系统初始化后:用更强的get_random_bytes()刷新金丝雀值。
  • 多核安全
    • 每个CPU核心通过this_cpu_read(__stack_chk_guard)获取独立金丝雀值。
    • 避免核心间共享导致的安全绕过。
  • 性能优化
    • 仅保护包含局部数组有特殊属性的函数(通过__attribute__((stack_protect)))。
    • 可通过CONFIG_STACKPROTECTOR_STRONG(GCC的-fstack-protector-strong)扩大保护范围。

用户态 vs 内核态 Stack Protector

特性 用户态 (GCC) Linux内核
金丝雀存储 TLS(线程局部存储) Per-CPU变量
随机化时机 程序启动时 内核启动 + 周期性刷新
崩溃行为 终止进程 (core dump) 内核panic(系统崩溃)
保护函数范围 所有含数组的函数(默认) 可配置(基本/强保护)

如何启用

内核配置选项:

1
2
3
CONFIG_CC_STACKPROTECTOR=y      # 基本保护
CONFIG_CC_STACKPROTECTOR_STRONG=y # 更强保护(推荐)
CONFIG_STACKPROTECTOR_PER_TASK=y # 按任务金丝雀(ARM64等)

局限性

  1. 不防所有溢出
    • 仅保护返回地址和金丝雀之间的数据
    • 跳过金丝雀覆盖局部变量(如结构体内溢出)可能绕过检测。
  2. 信息泄露风险
    • 若攻击者能读取金丝雀值,可伪造它实现绕过(需结合其他漏洞)。
  3. 性能开销
    • 每个保护的函数有额外指令(约0-5%性能损失)。

实例:内核代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义percpu金丝雀值
DEFINE_PER_CPU(unsigned long, __stack_chk_guard);

// 启动时初始化
void __init boot_init_stack_canary(void)
{
unsigned long canary = get_random_canary();
current->stack_canary = canary;
this_cpu_write(__stack_chk_guard, canary);
}

// 栈检查失败处理
void __stack_chk_fail(void)
{
panic("stack-protector: Kernel stack is corrupted\n");
}

通过这种机制,Linux内核有效防御了大部分栈溢出攻击,提升了系统安全性。