__stack_chk_guard 深入解析:原理、示例与 musl/glibc 代码路径

__stack_chk_guard 是 GCC/Clang 栈保护(SSP, Stack Smashing Protector)机制中的核心变量之一。很多人在反汇编里见过它,但常见误解是:它是不是内核变量、什么时候初始化、为什么能拦截栈溢出。本文基于一个具体示例和运行时实现路径,把这些问题串起来说明。

一、概念说明:__stack_chk_guard 是什么

__stack_chk_guard 本质上是 canary(金丝雀)参考值。编译器在函数入口和出口自动插入检查逻辑:

  1. 函数入口:把 guard 值保存到当前函数栈帧。
  2. 函数返回前:比较栈中的副本和原始 guard。
  3. 不一致:调用 __stack_chk_fail(),进程立即终止。

这个机制的安全意义是:攻击者若想覆盖返回地址,通常必须先破坏 canary,而 canary 一旦被改写,函数返回前就会被检测出来。

二、谁在做这件事:编译器与 C 库分工

栈保护不是单一组件完成,而是协同机制:

所以需要明确:__stack_chk_guard 变量本体属于用户态运行时,不是“内核维护的全局变量”。内核通常只在进程启动时通过 AT_RANDOM 等渠道提供随机熵。

三、具体例子:一个会溢出的登录函数

下面是一个最小示例(故意保留不安全写法):

#include <stdio.h>
#include <string.h>

void login(const char *password) {
    char buffer[8];
    int is_admin = 0;

    strcpy(buffer, password);  // 无边界检查,存在溢出风险

    if (strcmp(buffer, "secret") == 0) {
        is_admin = 1;
    }

    puts(is_admin ? "welcome admin" : "bad password");
}

3.1 不启用栈保护时

如果输入超过 buffer 容量,溢出会继续覆盖相邻栈数据,严重时可改写返回地址,形成控制流劫持入口。

3.2 启用栈保护后

编译器会在 login 的序言保存 canary,在尾声做比较。如果输入过长导致 canary 被覆盖,返回前触发失败处理,进程中止。典型编译方式:

gcc -O2 -fstack-protector -o demo demo.c

更激进版本:

gcc -O2 -fstack-protector-all -o demo demo.c

典型失败输出(glibc 环境常见):

*** stack smashing detected ***: terminated
Aborted (core dumped)

3.3 简单场景说明(按输入长度看行为)

这个例子可以直接用三种输入理解:

  1. 输入 secret(6 字节)
    buffer[8] 能完整容纳,不发生溢出,canary 不变,函数正常返回。
  2. 输入 12345678(8 字节)
    刚好写满缓冲区边界,通常也不会覆盖 canary,函数正常返回。
  3. 输入 123456789(9 字节及以上)
    超出缓冲区后继续向后写,极易覆盖 canary;函数尾声比较失败,调用 __stack_chk_fail 终止进程。

对应内存上的直觉是:想碰到返回地址,先要经过 canary 槽位;canary 先变,程序就先终止。

四、代码分析:从汇编模式到运行时路径

不同架构指令细节不同,但总体结构一致。可抽象为:

; 函数入口
load guard -> reg
store reg -> [stack_canary_slot]

; ... 函数主体 ...

; 函数返回前
load [stack_canary_slot] -> reg1
load guard -> reg2
cmp reg1, reg2
jne __stack_chk_fail
ret

这解释了为什么该机制能拦住大量“覆盖返回地址”的经典栈溢出:覆盖路径上必须先穿过 canary 槽位。

五、__stack_chk_guard 何时会变化

正常情况下,guard 在进程启动早期初始化一次,随后应保持稳定。运行中发现 guard 改变,通常意味着以下之一:

因此,“运行时 guard 变化”是高度可疑信号,不应视为正常现象。

六、musl C 代码说明(定义、初始化、线程传播、失败路径)

下面用你分析中对应的 musl 代码路径来说明关键点。

6.1 __stack_chk_guard 定义与 __init_ssp 初始化

src/env/__stack_chk_fail.c(简化):

uintptr_t __stack_chk_guard;

void __init_ssp(void *entropy)
{
    if (entropy) memcpy(&__stack_chk_guard, entropy, sizeof(uintptr_t));
    else __stack_chk_guard = (uintptr_t)&__stack_chk_guard * 1103515245;

#if UINTPTR_MAX >= 0xffffffffffffffff
    ((char *)&__stack_chk_guard)[1] = 0;
#endif

    __pthread_self()->canary = __stack_chk_guard;
}

这段代码表达了四件事:

  1. guard 是用户态全局变量(uintptr_t __stack_chk_guard;)。
  2. 优先使用外部熵(entropy,通常来自 AT_RANDOM)。
  3. 无熵时使用兜底值(可用但强度较弱)。
  4. 初始化后同步到当前线程的 canary 字段,供线程上下文中的检查路径使用。

6.2 进程启动阶段如何把 AT_RANDOM 传给 __init_ssp

src/env/__libc_start_main.c(简化):

void __init_libc(char **envp, char *pn)
{
    size_t i, *auxv, aux[AUX_CNT] = { 0 };
    ...
    for (i=0; auxv[i]; i+=2)
        if (auxv[i] < AUX_CNT) aux[auxv[i]] = auxv[i+1];
    ...
    __init_tls(aux);
    __init_ssp((void *)aux[AT_RANDOM]);
    ...
}

这里的关键是:在 main 执行前,musl 已完成 guard 初始化。
所以业务代码进入前,SSP 依赖的数据已就绪。

6.3 线程结构与新线程 canary 继承

线程结构中有 canary 字段(src/internal/pthread_impl.h):

struct pthread {
    ...
    uintptr_t canary;
    ...
};

线程创建时复制父线程 canary(src/thread/pthread_create.c):

new->canary = self->canary;

这保证了多线程下 canary 数据在运行时结构里是一致可用的。

6.4 校验失败后的处理:__stack_chk_fail

src/env/__stack_chk_fail.c(简化):

void __stack_chk_fail(void)
{
    a_crash();
}

musl 的失败路径非常短:直接崩溃退出,不尝试恢复。
这是典型 fail-fast 策略,避免在“栈已损坏”状态继续执行复杂逻辑。

6.5 musl 这一套实现的工程特征

七、glibc 对照:同目标,不同工程风格

glibc 与 musl 在核心目标上一致:都通过 canary 检测栈破坏并 fail-fast。差异更多体现在工程层面:

八、边界与局限:它不是万能防护

__stack_chk_guard 很重要,但能力边界也要明确:

九、实践建议

  1. 在构建系统中默认启用 -fstack-protector-strong(或更强策略)。
  2. 同时启用 PIE、RELRO、NX 和 FORTIFY_SOURCE。
  3. 优先替换高风险 API(如 strcpy, sprintf, gets)。
  4. 将 canary 视为“最后一道完整性检查”,而非唯一安全策略。

十、结论

__stack_chk_guard 的价值可以概括为一句话:
它通过“函数级栈完整性校验”把很多本可沉默成功的栈覆盖攻击,转化为可检测、可终止的失败路径。

从机制到实现,无论是 musl 还是 glibc,本质都遵循同一个原则:在控制流可信度下降时,尽快停止执行,避免把漏洞升级为可利用攻击。

My Github Page: https://github.com/liweinan

Powered by Jekyll and Theme by solid

If you have any question want to ask or find bugs regarding with my blog posts, please report it here:
https://github.com/liweinan/liweinan.github.io/issues