Skip to content

第一百六十八章:栈保护

渡劫期

涉及内核源码:

离开 KASLR 的迷雾高原,林小源来到一条狭窄的栈道前。栈道悬在深渊之上,木板一块接一块地铺向远方。

一个身穿黑衣的守卫拦住了他。

"栈道不是随便走的,"守卫的声音沙哑,"我叫 Canary——栈金丝雀。你每走一步,我都在你脚下放一个标记。如果你踩碎了标记,说明栈道被破坏了,我会立刻截断这条路。"

林小源低头看去。每块木板的缝隙间,都嵌着一颗微小的、闪烁着金光的种子。那是随机生成的值——0xFF 开头,确保即使输入中包含空字节也无法伪造。

"这些种子放在哪里?"

"局部变量和返回地址之间,"Canary 指着栈道的结构,"攻击者要覆盖返回地址,就必须先经过种子。但他不知道种子的值,因为每次函数调用都会重新生成。一旦种子被篡改——" Canary 猛地一跺脚,栈道前方的木板轰然断裂,",程序终止。"

c
/*
 * 栈保护机制:
 *
 * 1. Stack Canaries (栈金丝雀)
 *    在栈上放置随机值
 *    函数返回前检查
 *    被修改则终止程序
 *
 * 2. NX (No-Execute)
 *    栈不可执行
 *    防止在栈上执行代码
 *
 * 3. ASLR
 *    随机化栈地址
 *    攻击者不知道栈在哪里
 *
 * 4. Shadow Stack (影子栈)
 *    CET (Control-flow Enforcement Technology)
 *    硬件支持的返回地址保护
 *
 * Stack Canary 布局:
 *   [局部变量]
 *   [canary]  ← 随机值
 *   [saved rbp]
 *   [return address]
 *
 * 检查:
 *   if (canary != original_canary)
 *       __stack_chk_fail();
 */

/* 模拟 canary */
unsigned long generate_canary(void) {
    return (unsigned long)rand() | 0xFF00000000000000;
}

/* 模拟有 canary 保护的函数 */
void protected_function(const char *input) {
    unsigned long canary = generate_canary();
    char buffer[16];

    printf("  函数开始:\n");
    printf("    canary = 0x%lx\n", canary);
    printf("    buffer 地址: %p\n", (void*)buffer);

    /* 复制输入 */
    int len = strlen(input);
    printf("    输入长度: %d\n", len);

    /* 检查是否溢出 */
    if (len >= 16) {
        printf("    警告: 缓冲区溢出!\n");
        printf("    canary 可能被覆盖\n");
    }

    /* 复制数据 */
    memcpy(buffer, input, len < 16 ? len : 16);

    /* 检查 canary */
    printf("  函数返回前:\n");
    printf("    canary = 0x%lx\n", canary);
    /* 实际中: if (canary != original) __stack_chk_fail(); */
}

printf("=== 栈保护 — 防御栈溢出 ===\n\n");

srand(time(NULL));

printf("--- Stack Canary ---\n");
printf("原理:\n");
printf("  在栈上放置随机值\n");
printf("  函数返回前检查\n\n");

printf("--- 正常调用 ---\n");
protected_function("hello");

printf("\n--- 溢出攻击 ---\n");
protected_function("AAAAAAAAAAAAAAAAAAAAAAAAAAAA");

printf("\n--- 栈布局 ---\n");
printf("[高地址]\n");
printf("  return address\n");
printf("  saved rbp\n");
printf("  canary     ← 随机值\n");
printf("  buffer[16] ← 局部变量\n");
printf("[低地址]\n\n");

printf("--- 保护机制 ---\n");
printf("1. Stack Canary:\n");
printf("   -fstack-protector\n");
printf("   检查栈溢出\n\n");
printf("2. NX (DEP):\n");
printf("   栈不可执行\n");
printf("   -z noexecstack\n\n");
printf("3. ASLR:\n");
printf("   随机化栈地址\n");
printf("   echo 2 > /proc/sys/kernel/randomize_va_space\n\n");
printf("4. Shadow Stack:\n");
printf("   CET 硬件支持\n");
printf("   保护返回地址\n\n");

printf("--- 编译选项 ---\n");
printf("gcc -fstack-protector-all\n");
printf("  所有函数都有 canary\n\n");
printf("gcc -fstack-protector-strong\n");
printf("  有风险的函数有 canary\n");

栈道走到一半,林小源注意到脚下有一层隐隐发光的薄膜。

"那是什么?"

"NX,No-Execute,"Canary 说,"这层薄膜让栈道上的一切都只能读写,不能执行。就算攻击者在栈上注入了恶意代码,没有这片区域的执行权限,那些代码就是一堆废数据。"

林小源蹲下身,用手指触碰薄膜。它冰冷而坚硬,像一面无形的玻璃。

"那攻击者怎么办?"

Canary 冷笑一声:"他们被迫用 ROP——Return-Oriented Programming。从已有的代码里拼凑出攻击链。每次调用一个小片段,拼起来执行恶意逻辑。麻烦得多,但不是不可能。"

"所以你也不是万能的。"

"所以我还有帮手,"Canary 向身后一指。栈道的尽头,有三层防护依次矗立——NX 禁止执行,ASLR 让栈的地址每次随机偏移,Shadow Stack 在硬件层面额外保存一份返回地址的副本。

"攻击者要同时绕过四层。"Canary 的语气带着一丝骄傲,"每一层都可能被单独突破,但叠加在一起,难度是指数级增长。"

林小源走过栈道,来到一座坚不可摧的要塞前。要塞的墙壁上刻着编译选项。

"纵深防御,"他抚摸着墙上的 -fstack-protector-all 字样,"不是靠一道墙挡住所有攻击,而是让攻击者必须穿越一道又一道障碍。"

Canary 站在他身后,平静地说:"你已经明白了。安全没有银弹。Stack Canary 会被信息泄漏绕过,NX 会被 ROP 绕过,ASLR 会被侧信道绕过。但它们组合在一起——攻击者需要同时具备多种能力,每一种都可能出错,每一步都可能暴露行踪。"

林小源转身,看向来时的路。那条栈道上闪烁着金色的种子,薄膜折射着冷光,地址在每次启动时都会重新排列。

"单独的防御是脆弱的,"他说,"但组合起来,就是一道活的防线。"


道藏笔记

内核启示

栈溢出攻击要覆盖返回地址,栈保护就在中间插一道坎。Stack Canary 在局部变量和返回地址之间放一个随机值,函数返回前检查有没有被改——改了就 __stack_chk_fail() 终止程序。编译时 -fstack-protector-strong 给有风险的函数加 canary,-fstack-protector-all 给所有函数都加。

NX 让栈上的数据不能执行,攻击者注入的 shellcode 变成废数据(不过他们还能搞 ROP)。ASLR 每次随机化栈地址,Shadow Stack 在硬件层面额外存一份返回地址的副本。单独每一层都可能被绕过,但叠在一起攻击者需要同时具备多种能力。

栈保护是哨兵——发现异常,立即终止。


破关试炼

栈保护之试

栈保护一章中,为所有函数启用栈保护的编译选项是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道