第一百六十八章:栈保护
渡劫期涉及内核源码:
一
离开 KASLR 的迷雾高原,林小源来到一条狭窄的栈道前。栈道悬在深渊之上,木板一块接一块地铺向远方。
一个身穿黑衣的守卫拦住了他。
"栈道不是随便走的,"守卫的声音沙哑,"我叫 Canary——栈金丝雀。你每走一步,我都在你脚下放一个标记。如果你踩碎了标记,说明栈道被破坏了,我会立刻截断这条路。"
林小源低头看去。每块木板的缝隙间,都嵌着一颗微小的、闪烁着金光的种子。那是随机生成的值——0xFF 开头,确保即使输入中包含空字节也无法伪造。
"这些种子放在哪里?"
"局部变量和返回地址之间,"Canary 指着栈道的结构,"攻击者要覆盖返回地址,就必须先经过种子。但他不知道种子的值,因为每次函数调用都会重新生成。一旦种子被篡改——" Canary 猛地一跺脚,栈道前方的木板轰然断裂,",程序终止。"
/*
* 栈保护机制:
*
* 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");#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
/*
* 栈保护机制:
*
* 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(); */
}
int main() {
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");
return 0;
}二
栈道走到一半,林小源注意到脚下有一层隐隐发光的薄膜。
"那是什么?"
"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 在硬件层面额外存一份返回地址的副本。单独每一层都可能被绕过,但叠在一起攻击者需要同时具备多种能力。
栈保护是哨兵——发现异常,立即终止。
栈保护之试
栈保护一章中,为所有函数启用栈保护的编译选项是什么?