跳转到内容

Lesson 61: 缓冲区溢出安全分析

练习任务

难度:中

给定 3 段 C 代码片段,完成"缓冲区溢出安全分析"——不执行实际溢出攻击,而是作为安全分析师,系统地分析每段代码的内存布局、溢出风险和防护方案。

你需要填写 overflow_lab.c 中的 7 个 TODO 区域,输出一份固定格式的安全分析报告:

片段代码风险等级
1gets(buf)buf[16]CRITICAL
2strcpy(buf, src)buf[32]HIGH
3fgets(buf, sizeof(buf), stdin)buf[16]SAFE

报告内容包括:ASCII 栈帧图(含地址偏移标注)、偏移量计算、溢出风险判断、Stack Canary(金丝雀)防护原理讲解、六种防护机制对比表(Canary / ASLR / DEP / RELRO / PIE / FORTIFY)、以给出综合风险评估和安全编程准则。

提示:栈从高地址向低地址增长,数组从低地址向高地址增长——这两个方向"对撞"是缓冲区溢出能够覆盖返回地址的根本原因。画图时始终问自己:buf[N] 越界写入的方向,正好指向 rbp 和返回地址。


核心知识点

  • x86-64 栈帧结构与调用约定 — call 压返回地址、push rbp 保存基址、sub rsp,N 分配局部变量,理解栈帧 = 理解溢出
  • 缓冲区溢出的根本原因 — 栈向低地址增长,数组向高地址增长,buf[N] 越界正好冲向 canary → rbp → 返回地址
  • gets 零边界检查的危害 — 唯一参数是缓冲区指针,不知道大小,C11 已正式移理)
  • strcpy \0 终止的双刃剑 — 不检查目标大小但 \0 限制 payload 内容
  • fgets size 参数的安全边界 — 只需正确传入 sizeof(buf),就能天然阻止溢出
  • 偏移量计算公式 — offset = sizeof(buf) + 对齐填充 + (8 if canary) + 8(rbp),掌握其推导过程
  • Stack Canary 哨兵机制 — 来自煤矿金丝雀比喻,返回地址前插入随机值,返回前校验,最低字节为 \0 防字符串泄露
  • Canary 的局限性 — 信息泄露可精确覆盖、Fork 子进程共享 canary 可暴力破解、不保护数据指针和 GOT 表
  • 纵深防御体系 — Canary(检测) → ASLR(隐藏) → DEP/NX(阻止执行) → RELRO(保护 GOT) → PIE(位置无关) → FORTIFY_SOURCE(编译强化)
  • 安全替代函数表 — gets→fgets、strcpy→strncpy+手动\0、sprintf→snprintf、strcat→strncat
  • 安全编译选项 — -fstack-protector-strong、-D_FORTIFY_SOURCE=2、-fPIE -pie、-Wl,-z,relro,-z,now

代码框架

overflow_lab.c
c
#include <stddef.h>
#include <stdio.h>
#include <string.h>

/* 栈帧分析数据结构 */
typedef struct {
    int buf_size;
    int offset_to_ret;
    int overflow_risk;
    const char *func_name;
    const char *description;
} StackAnalysis;

static void print_separator(void) {
    printf("══════════════════════════════════════════════════════\n");
}

/* ─── TODO 1: 打印代码片段 1 的栈帧分析 (gets 溢出) ─── */
#error TODO 1-7: 完成所有分析章节
/*
 * 片段 1: void vulnerable_gets(void) { char buf[16]; gets(buf); }
 *
 * 栈帧布局 (x86-64, 无 canary):
 *   +24  返回地址(8B)    ← gets 可覆盖
 *   +16  保存的 rbp(8B)
 *   +8   [填充/对齐](8B)
 *   +0   buf[16]          ← gets 从这里写入
 *
 * buf[0] 到返回地址的偏移 = 16 + 8 + 8 = 32 字节
 */

/* ─── TODO 2: 代码片段 2 栈帧分析 (strcpy 溢出) ────── */
/*
 * 片段 2: void vulnerable_strcpy(char *src) { char buf[32]; strcpy(buf, src); }
 *
 * 偏移 = 32 + 8 = 40 字节
 */

/* ─── TODO 3: 代码片段 3 栈帧分析 (fgets 安全版) ─── */
/*
 * 片段 3: void safe_fgets(void) { char buf[16]; fgets(buf, sizeof(buf), stdin); }
 *
 * 偏移 32 字节,但 fgets 最多写入 16 字节,无法触及返回地址
 */

/* ─── TODO 4: 金丝雀 (Stack Canary) 防护原理 ─── */
/*
 * 带 canary 的栈帧: buf → [对齐] → CANARY(8B, 最低字节\0) → rbp → ret
 * 入口: mov rax, fs:0x28; 写入栈帧
 * 返回前: mov rax, [rbp-8]; xor rax, fs:0x28; je ok → call __stack_chk_fail
 */

/* ─── TODO 5: 防护机制对比表 ─── */
/*
 * Canary / ASLR / DEP / RELRO / PIE / FORTIFY_SOURCE 六种机制
 */

/* ─── TODO 6: 综合风险评估 ─── */
/*
 * 三段风险评级 + 五条安全准则
 */

/* ─── TODO 7: 学习建议 ─── */
/*
 * 动手实验、工具推荐、标准规范、编译器默认选项、经典论文
 */

int main(void) {
#error TODO 1-7: 完成所有分析章节
    return 0;
}

本题的"代码框架"比较特殊——不是实现算法,而是用 printf 输出一份结构化的安全分析报告。你需要读懂每个 TODO 上方注释中的要求,然后逐步填充,使输出与 expected_output.txt 精确匹配。

TIP

先不往下翻看参考解答。试着在纸上画出三个片段各自的栈帧布局图,标注地址偏移。关键是理解"栈向低地址增长,数组向高地址增长"这个方向差异——它是所有溢出攻击的地理基础。


深度讲解

1. 栈帧结构与 x86-64 调用约定——溢出理解的地基

1.1 函数调用的栈操作全景

每当一个函数被调用,CPU 执行一套固定仪式——理解这套仪式是理解缓冲区溢出的绝对前提。

调用者 (caller) 视角:
  push 参数 将第 7 个起的参数压栈 (x86-64  6 个用寄存器)
  call function 将返回地址 (RIP) 压栈,然后跳转到函数

被调用者 (callee) 视角:
  push rbp 保存调用者的栈帧基址
  mov  rbp, rsp 建立自调用者的栈帧基址
  sub  rsp, N 为局部变量分配 N 字节空间
  ... 函数体 ...
  leave mov rsp, rbp; pop rbp (恢复调用者栈帧)
  ret pop rip (弹出返回地址并跳转)
stack_frame_demo.c
c
/*
 * 用最简单的函数观察栈帧操作:
 * $ gcc -O0 -o demo demo.c
 * $ objdump -d demo | grep -A15 '<simple_func>'
 */
void simple_func(int x) {
    char buf[16];
    buf[0] = x;
}

/*
 * 反汇编后大约是这样的 (x86-64):
 *
 * simple_func:
 *   push   rbp              ; 保存调用用者的栈帧基址
 *   mov    rbp, rsp         ; 建立新栈帧
 *   sub    rsp, 32          ; 分配 32 字节局部空间 (16+对齐)
 *   mov    eax, edi         ; 参数 x 在 edi 中
 *   mov    BYTE PTR [rbp-16], al  ; buf[0] = x
 *   leave                   ; 恢复栈帧
 *   ret                     ; 返回
 */

NOTE

sub rsp, 32 而不是 sub rsp, 16 说明了 x86-64 ABI 的 16 字节栈对齐要求——即使 buf 只有 16 字节,编译器仍然多分配 16 字节来维持对齐。这些"多出来的"空间在溢出攻击中充当了填充物。

1.2 x86-64 典型栈帧布局

      高地址 (栈底方向,地址值更大)
      ┌──────────────────────┐
  调用者的栈帧 调用者的局部变量等
      ├──────────────────────┤
  函数参数 (第 7 个起)  │  ← x86-64 前 6 个参数用寄存器
      ├──────────────────────┤
  返回回地址 (8 bytes)   │  ← call 指令压入的 RIP
      ├──────────────────────┤
  保存的 rbp (8 bytes) │  ← 调用者的栈帧基址
      ├──────────────────────┤ rbp (当前函函数栈帧基址)
  canary (8 bytes)     │  ← -fstack-protector 插入的哨兵
      ├──────────────────────┤
  [栈对齐填充]          │  ← x86-64 ABI 要求 16 字节对齐
      ├──────────────────────┤
  局部变量 / 数组 char buf[N] 从这里开始
      ├──────────────────────┤
  callee-saved 寄存器 rbx, r12-r15 (如果使用)
      └──────────────────────┘
      低地址 (栈顶方向) ← rsp

IMPORTANT

注意看各区域的相对位置:返回地址在高地址,缓冲区在低地址。这意味着如果从缓冲区向高地址方向溢出(数组索引递增),会依次经过:对齐填充 → canary → rbp → 返回地址。这就是溢出攻击的地理路线图。

1.3 栈增长 vs 数组增长——致命的方向碰撞

这是理解缓冲区溢出的最关键的一张图

栈增长方向:  高地址 低地址  (push 操作递减 rsp)
数组增长方向: 低地址 高地址  (buf[i] 的地址 = buf + i)

因此:
  buf[0]   在最低地址
  buf[N-1] 在较高地址
 buf[N] 写入 向高地址越界 覆盖 canary 覆盖 rbp 覆盖返回地址

示意:
  低地址                    buf                        canary/rbp/ret   高地址
  ─────────────────────────────────────────────────────────────────────────→
  [buf[0]][buf[1]]...[buf[N-1]][超出部分覆盖canary][覆盖rbp][覆盖返回地址]
  写入起点 ←──────────────────── 溢出方向 ────────────────────────────→
简易记忆口诀:
  "push 减 (向低),数组增 (向高)"
  二者面对面走 越界写入刚好撞上返回地址

CAUTION

许多初学者在这里搞反方向——认为溢出是向低地址方向覆盖。如果你把方向画反了,整个安全模型就错了:你会认为 canary 放在 buf 的"另一边",而实际它放在 buf 的"溢出路径"上。


2. gets(buf) — 缓冲区溢出最危险的入口

2.1 gets 为什么如此危险

gets_vulnerability.c
c
void vulnerable_gets(void) {
    char buf[16];
    gets(buf);  /* 危险!gets 不检查边界 */
}

gets() 只有一个参数——缓冲区指针。它从 stdin 读取字符直到遇到换行符或 EOF,但完全不知道缓冲区有多大

正常输入 "hello" (5 字节 + \0):
  buf: [h][e][l][l][o][\0]... ← 安全,未触及返回地址

恶意输入 'A' × 32 + 恶意地址 (8 字节):
  buf: [A][A]...[A][A][A][A]...[A][恶意地址]
       └── buf[0..15] ──┘└─填充─┘└─rbp─┘└─返回地址─┘
  结果: 函数 "返回" 到攻击者指定的地址 控制流劫持

gets() 的危险四重奏

  1. 零边界检查gets 只有一个参数(缓冲区指针),不知道缓冲区大小
  2. 任意长长度输入:从 stdin 读取直到到换行符,可以是任意长度
  3. 直接可攻击:攻击者通过 stdin 直接控制溢出内容
  4. 已被标准移除:C11 标准正式移除 gets(),但遗留代码中仍大量存在

IMPORTANT

C11 移除 gets() 是 C 语言标准化史上最重要的安全决策之一。但 GCC 为向后兼容仍支持它(编译时带 -Wimplicit-function-declaration 警告)。任何出现 gets() 的代码都应立即修复。

2.2 片段 1 的完整栈帧分析

stack_frame_gets.c
c
/*
 * 代码: void vulnerable_gets(void) { char buf[16]; gets(buf); }
 *
 * 栈帧布局 (x86-64, 无 canary):
 *
 *   地址偏移       内容                说明
 *   ─────────────────────────────────────────────────
 *   +24           ┌──────────────┐
 *                 │  返回地址     │ ← gets 溢出可覆盖!
 *   +16           ├──────────────┤
 *                 │  保存的 rbp   │
 *   +8            ├──────────────┤
 *                 │  [栈对齐填充]  │  (8 bytes, 对齐到 16 字节)
 *   +0            ├──────────────┤
 *                 │  buf[0..15]   │ ← gets() 从这里开始写入
 *                 └──────────────┘
 *
 * 偏移量计算:
 *   buf[0] 到返回地址的距离 = 16 (buf) + 8 (对齐) + 8 (rbp) = 32 字节
 *
 * 注:若编译器紧密排列(无对齐填充),则为 24 字节。
 *     实际偏移取决于编译器版本和优化级别。本题使用 32 字节作为教学示例。
 */
溢出攻击的精确 payload:
  输入 "A" × 32 + 8 字节恶意地址
         └── 填充 ──┘ └─ 覆盖返回地址 ─┘

  当函数执行 ret 指令时:
    CPU 从栈上弹出"返回地址"
    但这个地址已被替换为恶意地址
 跳转到攻击者指定的内存位置
    函数体执行攻击者植入的代码 (shellcode) 或 ROP 链

3. strcpy(buf, src) — 隐蔽的杀手

3.1 strcpy 的溢出机制

strcpy_vulnerability.c
c
void vulnerable_strcpy(char *src) {
    char buf[32];
    strcpy(buf, src);  /* 危险!strcpy 不检查边界 */
}

strcpy两个参数(目标和源),但同样不检查目标缓冲区的大小。它从 src 拷贝字节直到遇到 \0,完全无视 buf 的容量。

如果 src 指向一个超长字符串(无 \0 或很晚才有 \0):
  strcpy 将持续拷贝直到遇到 \0
 覆盖 buf[32..39] (rbp)
 覆盖 buf[40..47] (返回地址)
 继续覆盖调用者的栈帧...

注意:strcpy \0 终止是一把尴尬的双刃剑:
  - 保护侧:限制了 payload 中不能包含 \0 字节 (很多 shellcode \0)
  - 危险侧:但依然不能防止溢出本身——源字符串可能长达 MB

3.2 片段 2 的完整栈帧分析

stack_frame_strcpy.c
c
/*
 * 代码: void vulnerable_strcpy(char *src) { char buf[32]; strcpy(buf, src); }
 *
 * 栈帧布局 (x86-64, 无 canary):
 *
 *   地址偏移       内容                说明
 *   ─────────────────────────────────────────────────
 *   +40           ┌──────────────┐
 *                 │  返回地址     │ ← strcpy 溢出可覆盖!
 *   +32           ├──────────────┤
 *                 │  保存的 rbp   │
 *   +0            ├──────────────┤
 *                 │  buf[0..31]   │ ← strcpy() 从这里开始写入
 *                 └──────────────┘
 *
 * 偏移量计算:
 *   buf[0] 到返回地址的距离 = 32 (buf) + 8 (rbp) = 40 字节
 *   (32 字节已对齐到 16 字节,无需额外填充)
 */

3.3 gets vs strcpy 差异对比

维度gets()strcpy()
输入源stdin(标准输入)字符串参数(内存中的 src)
终止条件换行符 '\n'空字符 '\0'
攻击难度低(直接 stdin 输入)中(需控制 src 内容和容和长度)
\0 限制无(可包含任意字节)有(\0 终止拷贝,限制 payload)
典型场景网络服务、命令行工具字符串处理、配置文件解析
C 标准状态C11 移除仍保留(但有安全替代)

TIP

getsstrcpy 更危险的原因:gets 的输入源是 stdin——攻击者通过键盘输入就能直接触发。而 strcpy 需要先控制 src 字符串的内容(通常需要组合其他漏洞),攻击链路更长。


4. fgets 安全版本 — 边界受控的正确范例

4.1 fgets 为什么安全

fgets_safe.c
c
void safe_fgets(void) {
    char buf[16];
    fgets(buf, sizeof(buf), stdin);  /* 安全!限定了最大最大长度 */
}

fgets(buf, size, stream)三个参数,最关键的第二个参数 size 告诉了函数缓冲区的容量:

fgets(buf, 16, stdin) 的行为:
  1. 最多读取 15 个字符(第 2 个参数 - 1)
  2. 16 个字节固定写入 '\0'
  3. 遇到换行符 '\n' 也停止
  4. 遇到 EOF 也停止

因此:最大写入范围 = buf[0..15],共 16 字节
     返回地址在 +24 偏移处,完全无法触及
fgets_stack_frame.c
c
/*
 * 代码: void safe_fgets(void) { char buf[16]; fgets(buf, sizeof(buf), stdin); }
 *
 * 栈帧布局 (同片段 1,但行为完全不同):
 *
 *   地址偏移       内容                说明
 *   ─────────────────────────────────────────────────
 *   +24           ┌──────────────┐
 *                 │  返回地址     │ ← fgets 无法到达!
 *   +16           ├──────────────┤
 *                 │  保存的 rbp   │ ← fgets 无法到达!
 *   +8            ├──────────────┤
 *                 │  [栈对齐填充]  │ ← fgets 无法到达!
 *   +0            ├──────────────┤
 *                 │  buf[0..15]   │ ← fgets 最多写 15+1 字节
 *                 └──────────────┘
 *
 * 偏移量同样是 32 字节,但 fgets 只写入 16 字节,
 * 距返回地址还有 16 字节"安全距离"。
 */

4.2 正确使用姿势与常见陷阱

fgets_correct_usage.c
c
/* ✅ 正确:使用 sizeof 自动适配缓冲区大小 */
char buf[16];
fgets(buf, sizeof(buf), stdin);

/* ✅ 也正确:显式指定(但需确保与声明一致) */
fgets(buf, 16, stdin);

/* ❌ 错误:传入错误的 size(大于大于实际缓冲区) */
char buf[16];
fgets(buf, 256, stdin);  /* 缓冲区区只有 16 字节,却告诉 fgets 有 256! */

/* ❌ 错误:忘记 sizeof 是字节数 */
wchar_t wbuf[16];
fgets((char*)wbuf, sizeof(wbuf), stdin);  /* 32 字节!可能超出 wbuf 实际大小 */

WARNING

fgets 的安全性建立在正确传入 size 参数的前提下。如果你错误地传入了一个比实际缓冲区更大的数值,fgets 就会变成 | gets 一样危险的函数。这就是为什么 sizeof(buf) 如此重要——它让编译器自动追踪真实大小。


5. Stack Canary(栈金丝雀)— 栈上的哨兵

5.1 名称由来:煤矿中的金丝雀

19世纪的煤矿:
  ┌─────────────────────────────────┐
 矿工带着金丝雀笼子下矿

 金丝雀对一氧化碳 (CO) 极度敏感   │
 CO 浓度升高 金丝雀先死
 矿工看到金丝雀死亡 立即撤离

 金丝雀 = 生物传感器 / 早期预警
  └─────────────────────────────────┘

计算机中的 Stack Canary:
  ┌─────────────────────────────────┐
 编译器在栈帧中放置一个随机值

 溢出覆盖 canary 值被改变
 函数返回回前检测到变化
 立即终止程序 (__stack_chk_fail)│

 canary = 栈溢出检测器 / 哨兵
  └─────────────────────────────────┘

5.2 带 Canary 的栈帧与检测流程

      地址偏移       内容                说明
      ─────────────────────────────────────────────
      +32           ┌──────────────┐
  返回地址 攻击者的最终目标
      +24           ├──────────────┤
  保存的 rbp
      +16           ├──────────────┤
  CANARY 哨兵值 (8 bytes)
      +8            ├──────────────┤
  [栈对齐填充]  │
      +0            ├──────────────┤
  buf[0..15] 溢出从这里开始
                    └──────────────┘

溢出路径:
  buf[0] buf[15] [对齐] → CANARY → rbp → 返回地址
  安全区域 ────────→ 触发警报!→ 程序终止
canary_lifecycle.c
c
/*
 * Canary 生命周期 (编译器的视角):
 *
 * ╔══════════════════════════════════════════════╗
 * ║  函数入口 (prologue):                        ║
 * ║    1. 从 TLS 读取随机 canary 值              ║
 * ║       mov rax, QWORD PTR fs:0x28            ║
 * ║    2. 写入栈帧 (rbp-8)                      ║
 * ║       mov QWORD PTR [rbp-8], rax            ║
 * ║                                              ║
 * ║  函数体执行: ... 可能的溢出 ...              ║
 * ║                                              ║
 * ║  函数返回前 (epilogue):                      ║
 * ║    3. 读取栈帧中的 canary                    ║
 * ║       mov rax, QWORD PTR [rbp-8]            ║
 * ║    4. 与 TLS 原始值异或                     ║
 * ║       xor rax, QWORD PTR fs:0x28            ║
 * ║    5. 若零 → je .L_ok → ret                 ║
 * ║    6. 若非零 → call __stack_chk_fail        ║
 * ╚══════════════════════════════════════════════╝
 */

5.3 Canary 的三个巧思

巧思 1:最低字节为 \0(NULL 字节)

canary 值示例: 0x00 7f 3a 91 4c 2e b8 d5

               \0 字节

为什么这样设计?
  大多数字符串函数 (strcpy, gets, sprintf) 遇到 \0 停止拷贝。
  如果攻击者想通过字符串溢出来覆盖 canary:
    "AAAA...\x00\x7f\x3a..." \0 strcpy 停止!

  攻击者必须精确知道 canary 值并原样写入,
  这大大增加了攻击难度。

巧思 2:随机值(每次运行不同)

程序启动时:
  canary 由内核的随机数生成器 (getrandom/urandom) 初始化
  存储在 TLS (Thread-Local Storage) 中

每次运行:
  $ ./vuln canary = 0x00a3f91c...
  $ ./vuln canary = 0x007b2e4d...  (完全不同!)

攻击者无法硬编码 canary 值。

巧思 3:存储在 TLS 中

TLS (Thread-Local Storage):
  - 每个线程独立的存储区域
  - 不能通过普通内存访问直接读取
  - x86-64: fs 段寄存器指向 TLS
  - 需要内存泄露漏洞才能读取

5.4 Canary 的局限性与绕过方法

局限性详细说明利用场景
信息泄露若攻击者能先读取 canary 值(如通过 format string 漏洞),则可精确覆盖printf("%s", buf) 泄露栈内容
非连续写入Canary 只能检测从低地址向高地址的顺序覆盖。直接写入返回地址(如任意地址写漏洞)不触发 canary*(int*)(buf+40) = evil_addr
Fork 暴力破解fork() 后子进程继承父进程的 canary 值。攻击者可逐字节尝试,每次崩溃只杀死子进程网络服务器 fork 模型
不保护其他目标覆盖函数指针、数据指针、GOT 表、或修改关键局部变量(如 is_admin flag)可能绕过 canary逻辑漏洞利用
绕过 Canary 4 种经典方法:

方法 A: 信息泄露 + 精确覆盖
  Step 1: 利用格式化字符串漏洞泄露 canary
  Step 2: 构造 payload = 填充 + canary原值 + rbp + 恶意返回地址
  Step 3: 触发溢出 canary 检测通过 劫持返回地址

方法 B: 改写 GOT 表(不碰 canary)
  Step 1: 溢出 buf,但不覆盖返回地址
  Step 2: 覆盖 buf 后面的函数指针 指向 system()
  Step 3: 后续代码调用该函数指针 执行 system()

方法 C: 覆盖局部变量
  void login() {
      char buf[16];
      int is_admin = 0;      buf 之后(更高地址)
      gets(buf);             溢出覆盖 is_admin 1
      if (is_admin) { ... } 绕过认证!
  }

方法 D: 逐字节暴力破解(fork 服务器)
  for (byte_pos = 1; byte_pos < 8; byte_pos++) {
      for (guess = 0; guess < 256; guess++) {
          发送 payload: 填充 + [已破解字节] + guess
          if (子进程没有崩溃)  该字节正确!
      }
  }
  // 平均需要 8 × 128 = 1024 次尝试

6. 纵深防御体系——多道防线的协同

6.1 多层防护链

现代操作系统采用多层防护,每层针对不同攻击阶段:

    攻击者


  ┌──────────┐
  Canary 1 层: 检测栈溢出
  └────┬─────┘       (阻止简单的栈粉碎)
 绕过

  ┌──────────┐
   ASLR 2 层: 隐藏地址布局
  └────┬─────┘       (使 ROP/JOP 难以定位 gadget)
 绕过

  ┌──────────┐
 DEP/NX 3 层: 阻止代码注入
  └────┬─────┘       (栈/堆不可执行)
 绕过

  ┌──────────┐
  RELRO 4 层: 保护 GOT
  └────┬─────┘       (阻止 GOT 覆写)
 绕过

     目标

6.2 完整机制对比表

防护机制全称防护目标工作原理启用方式局限性
Stack CanaryStack Smashing Protector栈缓冲区溢出检测返回地址前放置随机哨兵值,返回前校验-fstack-protector-strong信息泄露可绕过;不保护非栈目标
ASLRAddress Space Layout Randomization代码复用攻击 (ROP/JOP)随机化栈、堆、mmap、库、可执行文件的基地址内核默认 (/proc/sys/kernel/randomize_va_space)信息泄露可绕过;32-bit 熵不足 (~8 bit)
DEP / NX-bitData Execution Prevention / No-eXecute代码注入入攻击 (Shellcode)CPU 标记数据页为不可执行;栈/堆不能同时写 + 执行硬件 NX-bit + OS 支持 (-z noexecstack)ROP/JOP 可绕过(复用已有代码);JIT 需要可执行堆
RELRORelocation Read-OnlyGOT 表覆写攻击将重定位表设为只读;Full RELRO 在启动时解析所有符号-Wl,-z,relro / -Wl,-z,nowPartial RELRO 只保护 .got;Full RELRO 增加启动时间
PIEPosition Independent Executable固定地址攻击可执行文件编译为位置无关代码,配合 ASLR 随机加载-fPIE -pie需 ASLR 配合;轻微性能开销 (~1-3%)
FORTIFY_SOURCECompile-time Fortification不安全库函数调用编译时替换 strcpy__strcpy_chk(带边界检查)-D_FORTIFY_SOURCE=2需源码重编译;不能检测所有情况;对某些模式无效

6.3 实际编译示例

compile_security_options.sh
bash
# 最小防护(危险!仅用于教学演示)
gcc -fno-stack-protector -z execstack -no-pie -o vuln vuln.c

# 标准防护(现代 Linux 默认)
gcc -fstack-protector-strong -D_FORTIFY_SOURCE=2 \
    -fPIE -pie -Wl,-z,relro -Wl,-z,now \
    -o secure secure.c

# 查看二进二进制启用的安全特性
checksec --file=./secure
# 输出示例:
#   RELRO:    Full RELRO
#   STACK CANARY:  Found
#   NX:       Enabled
#   PIE:      Enabled

NOTE

这些编译选项在现代 GCC(≥6)中很多已经是默认开启的。但了解它们仍然重要——当你在嵌入式系统或不完整的工具链上编译时,可能需要手动确保这些防护生效。


7. 安全编程准则——从源头消灭漏洞

7.1 危险函数及安全替代

| 危险函数 | 风险 | | 安全替代 | 注意事项 | | ------------------------ | --------------- | --------------------------------------- | -------------------------------- | | gets(buf) | 零边界检查 | fgets(buf, sizeof(buf), stdin) | fgets 保留换行行符,需手动去除 | | strcpy(dst, src) | 不检查 dst 大小 | strncpy(dst, src, n-1); dst[n-1]='\0' | strncpy 不保证 \0 结尾 | | strcat(dst, src) | 不检查剩余空间 | strncat(dst, src, n-strlen(dst)-1) | 需计算剩余空间 | | sprintf(buf, fmt, ...) | 不检查 buf 大小 | snprintf(buf, sizeof(buf), fmt, ...) | snprintf 保证 \0 结尾(POSIX) | | scanf("%s", buf) | 不检查 buf 大小 | scanf("%15s", buf)fgets | 宽度限定限定符只限字符数,不含 \0 | | vsprintf(buf, fmt, va) | 不检查 buf 大小 | vsnprintf(buf, sizeof(buf), fmt, va) | 同 snprintf |

IMPORTANT

strncpy 虽然在名字里有个 "n",但它的行为让人意外:如果源字符串长度 ≥ n,目标不会\0 结尾。安全用法是 strncpy(dst, src, sizeof(dst)-1); dst[sizeof(dst)-1]='\0';——手动在最后一个字节置 \0

7.2 五条金科玉律

规则 1: 永远使用带边界检查的函数
 gets(buf)
 fgets(buf, sizeof(buf), stdin)

规则 2: 编译时启用所有安全选项
 -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE -pie

规则 3: 永远不要假设输入数据的长度是安全的
 "用户名不会超过 32 字节"
 无论如何都使用 sizeof() 或已知边界

规则 4: 理解栈帧布局有助于写出更安全的代码
 知道 buf 溢出后会覆盖什么 知道如何防护

规则 5: 代码审查时特别关注危险函数
 grep -nE '\bgets\b|\bstrcpy\b|\bstrcat\b|\bsprintf\b|\bscanf\("%s"\)' *.c

参考解答

练习1: TODO 1 — gets() 栈帧分析
solution_todo1_gets.c
c
    /* ─── TODO 1: 代码片段 1 gets() 栈帧分析 ─── */
    printf("╔══════════════════════════════════════════════════════╗\n");
    printf("║      缓冲区溢出安全分析实验报告                      ║\n");
    printf("║      Buffer Overflow Security Analysis Lab           ║\n");
    printf("╚══════════════════════════════════════════════════════╝\n");
    printf("\n");
    printf("【预备知识】x86-64 栈帧结构\n");
    printf("\n");
    /* ... 完整的栈帧基础介绍 ... */

核心输出包括:

  1. ASCII 框展示代码片段
  2. 栈帧布局图(标注 +0, +8, +16, +24 偏移)
  3. 偏移计算:buf[0] 到返回地址 = 16 + 8 + 8 = 32 字节
  4. 风险判断:gets 无边界检查,CRITICAL
  5. 攻击示意:'A' × 32 + 恶意地址 劫持返回地址
练习2: TODO 2 — strcpy() 栈帧分析
solution_todo2_strcpy.c
c
    /* ─── TODO 2: 代码片段 2 strcpy() 栈帧分析 ─── */
    print_separator();
    printf("\n");
    printf("【代码片段 2】strcpy() 缓冲区溢出 — 风险等级:HIGH\n");
    printf("\n");
    /* ... 代码展示 ... */
    /* ... 栈帧图(buf[32] + rbp = +40 偏移)... */
    /* ... gets vs strcpy 对比表 ... */

核心输出包括:

  1. ASCII 框展示片段 2 代码
  2. 栈帧布局图(偏移 +0, +32, +40)
  3. 偏移计算:buf[0] 到返回地址 = 32 + 8 = 40 字节
  4. gets vs strcpy 对比表(输入源、终止条件)
  5. \0 终止的双刃剑分析
练习3: TODO 3 — fgets() 安全版分析
solution_todo3_fgets.c
c
    /* ─── TODO 3: 代码片段 3 fgets() 安全版分析 ─── */
    print_separator();
    printf("\n");
    printf("【代码片段 3】fgets() 安全版本 — 风险等级:SAFE\n");
    printf("\n");
    /* ... 代码展示 ... */
    /* ... 栈帧图(标注"fgets 无法到达")... */
    /* ... fgets 安全机制说明 ... */

核心输出包括:

  1. ASCII 框展示片段 3 代码
  2. 栈帧布局图(标注所有"无法到达")
  3. 偏移同样是 32 字节,但 fgets 最多写入 16 字节
  4. fgets 安全机制:size 参数限制、换行/EOF 停止、强制 \0 结尾
  5. 正确使用 vs 误用对比(sizeof(buf) 是核心)
练习4: TODO 4 — Stack Canary 原理讲解
solution_todo4_canary.c
c
    /* ─── TODO 4: 金丝雀 (Stack Canary) 防护原理 ─── */
    print_separator();
    printf("\n");
    printf("【深入分析】金丝雀 (Stack Canary) 防护原理\n");
    printf("\n");
    /* ... 煤矿金丝雀故事 ... */
    /* ... 带 canary 的栈帧图 ... */
    /* ... 检测流程 (入口/返回前) ... */
    /* ... canary 特征表格 ... */
    /* ... 局限性 (4 点) + 绕过方法 (4 种) ... */

核心输出包括:

  1. 名称由来(煤矿金丝雀故事)
  2. 带 canary 的栈帧布局图(buf → 对齐 → CANARY → rbp → ret)
  3. 检测流程:入口读取 fs:0x28 → 写入栈帧 → 返回前异或比较
  4. Canary 三个特征:随机值、\0 最低字节、TLS 存储
  5. 四种局限性及绕过方法
练习5: TODO 5 — 防护机制对比表
solution_todo5_defense_table.c
c
    /* ─── TODO 5: 防护机制对比表 ─── */
    print_separator();
    printf("\n");
    printf("【防护机制对比】纵深防御体系\n");
    printf("\n");
    /* ... 6 种防护机制表格 ... */
    /* ... 纵深防御链示意图 ... */

核心输出包括:

  1. 6 行 × 4 列表格(防护机制 | 防护目标 | 原理 | 局限性)
  2. 覆盖 Canary / ASLR / DEP / RELRO / PIE / FORTIFY_SOURCE
  3. 纵深防御链示意:攻击者 → [Canary] → [ASLR] → [DEP] → [RELRO] → 目标
练习6: TODO 6 — 综合风险评估
solution_todo6_risk_assessment.c
c
    /* ─── TODO 6: 综合风险评估 ─── */
    print_separator();
    printf("\n");
    printf("【综合风险评估】\n");
    printf("\n");
    /* ... 三段代码风险评级表 ... */
    /* ... 五条安全编程准则 ... */

核心输出包括:

  1. 3 行风险评估表(片段 | 等级 | 原因)
    • gets → CRITICAL:无边界检查,stdin 直接控制
    • strcpy → HIGH:无边界检查,需控制 src
    • fgets → SAFE:显式指定最大长度
  2. 五条安全编程准则(含具体替代函数)
  3. 安全编译选项汇总
练习7: TODO 7 — 学习建议
solution_todo7_further_learning.c
c
    /* ─── TODO 7: 学习建议 ─── */
    print_separator();
    printf("\n");
    printf("【进一步学习建议】\n");
    printf("\n");
    /* ... 动手实验命令 ... */
    /* ... 工具推荐 (objdump/checksec/gdb) ... */
    /* ... 标准规范 (CWE/CERT/MISRA) ... */
    /* ... 编译器默认安全选项 ... */
    /* ... 经典论文推荐 ... */

核心输出包括:

  1. 动手实验命令(-fno-stack-protector 观察溢出效果)
  2. 工具推荐(objdump、checksec、gdb)
  3. 标准规范(CWE-120、CWE-121、CERT C STR31-C、MISRA C Rule 21.6)
  4. 编译器默认安全选项(GCC ≥6、Clang ≥5)
  5. 经典论文(Smashing The Stack、StackGuard、ROP)

对照检查:计算偏移时是否考虑了栈对齐填充?犯溢出的栈增长方向是否正确(向高地址覆盖)?Canary 的位置是否在 rbp 之下、局部变量之上?fgets 的 size 参数用了 sizeof(buf) 吗?报告中标注了 \0 字节对字符串函数的影响吗?


课堂讨论

  1. 为什么 C11 要移除 gets() 而不是修复它?
  2. 如果攻击者已经知道 canary 的值,canary 还剩下什么保护作用?
  3. 为什么 fork 出来的子进程 canary 相同?这个设计合理吗?
  4. strncpystrcpy 的溢出风险一样吗?
  5. 在现代 Linux 上,一个简单的栈溢出还能成功吗?
  6. 这道题为什么选择"分析"而不是"利用"?

讨论答案

Q1: 为什么 C11 要移除 gets() 而不是修复它?

gets() 的接口设计从根本上就是有缺陷的——它只有一个参数(缓冲区指针),没有缓冲区大小信息。要"修复"它必须改变函数签名(增加 size 参数),这实际上就是 fgets() 的接口。标准化委员会选择移除它并引导程序员使用 fgets(),而不是维护一个向后兼容的危险接口。

这一决策也引发了 C 社区的广泛讨论——部分人认为标准化委员会应该保持向后兼容(即使函数危险,至少程序员知道它的用法),另一部分人认为安全优先(使用危险函数 = 写出危险代码,标准化组织有责任移除)。最终安全派胜出。ISO C11 不仅是移除了 gets(),还引入了 Annex K(bounds-checking interfaces),提供了 gets_s() 等带边界检查的替代函数——虽然 Annex K 本身在业界争议很大。

Q2: 如果攻击者已经知道 canary 的值,canary 还剩下什么保护作用?

几乎没有。canary 的安全模型假设攻击者不知道它的值。一旦通过信息泄露获取了 canary 值,攻击者可以在溢出 payload 中原样放回,__stack_chk_fail 不会被触发。这就是为什么纵深防御如此重要——ASLR 和 DEP 作为第二二、第三道防线继续提供保护。

在真实攻击链中,信息泄露通常是整个攻击的第一步——攻击者先用 format string 漏洞或越界读取来泄漏 canary 值和 libc 基址,然后再用缓冲区溢出来劫持控制流。这种"Info Leak → Canary Bypass → ROP Chain"的攻击模式在 CTF 比赛和 APT 攻击中屡见不鲜。

但 canary 仍然有意义——它把"直接溢出就可以"变成了"必须先信息泄露再溢出",显著提高了攻击门槛。就像你家的门锁——专业窃贼能撬开,但不装锁的话任何人一推就能进。

Q3: 为什么 fork 出来的子进程 canary 相同?这合理吗?

技术上合理(fork 复制整个地址空间),但安全上是一个已知弱点。fork() 之后子进程是父进程的精确副本,包括 TLS 中的 canary 值。对于使用 fork() 模型的网络服务器(如 Apache prefork),攻击者可以通过反复尝试逐字节破解 canary,因为每次崩溃只杀死子进程,父进程继续存活并 fork 出新的(具有相同 canary 的)子进程。

具体攻击方法:攻击者逐字节尝试 canary 值。第 1 字节始终是 \0(已知)。对于第 2 字节,尝试 0x00-0xFF 的每个值,发送到服务器。如果猜测正确,子进程继续执行(canary 检测通过);如果错误,子进程崩溃——但父进程不受影响,立即 fork 出新的子进程准备接收下一次尝试。平均需要 8 × 128 = 1024 次尝试即可破解完整 canary。

这种攻击对使用 fork() 模型的网络服务(如 xinetd、Apache prefork MPM)尤其有效。现代方案如 posix_spawnclone + execve 可以避免这个弱点,因为新进程有全新的地址空间和 canary。

Q4: strncpy 和 strcpy 的溢出风险一样吗?

不完全一样。strncpy(dst, src, n) 最多拷贝 n 个字符,如果 n ≤ dst 的大小则不会溢出。但 strncpy 有一个隐藏陷阱:如果 strlen(src) ≥ n,则 dst 不会以 \0 结尾,后续的字符串操作可能读取越界。

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

int main(void) {
    char buf[8];
    strncpy(buf, "Hello, World!", sizeof(buf));
    /* buf = {'H','e','l','l','o',',',' ','W'} — 没有 \0! */
    printf("%s\n", buf);  /* 会继续打印 buf 后面的内存,直到遇到 \0 */
    return 0;
}

正确的安全用法:

strncpy_safe.c
c
strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0';  // 手动确保 \0 结尾

所以答案是:strncpy 可以安全——但前提是你知道并处理了它的 \0 陷阱。许多初学者以为 strncpy 就是"安全的 strcpy",这种误解本身就是安全隐患。

Q5: 在现代 Linux 上,一个简单的栈溢出还能成功吗?

默认情况下非常困难。现代 Linux 发行版默认启用:Stack Canary (-fstack-protector-strong)、ASLR、NX-bit、PIE、Full RELRO。单独的一个 gets() 溢出需要同时绕过 Canary(需要信息泄露)和 ASLR(需要地址泄露),再加上 NX-bit 阻止了 shellcode 直接执行,攻击者需要构造 ROP 链。

但"困难"不等于"不可能"。一个足够有动机的攻击者可以通过组合漏洞完成攻击链:

  1. 格式化字符串漏洞 → 泄露 canary 值和 libc 地址
  2. 缓冲区溢出 → 精确覆盖 canary + ROP 链
  3. ROP 链 → 调用 mprotect 使栈可执行 → 跳转到 shellcode

而且,攻击面比你想象的更广:

  • IoT 设备、嵌入式系统往往没有这些防护
  • 遗留系统(如运行 RHEL 5 的服务器)防护不完整
  • 第三方库可能用 -O0 编译,canary 被优化掉
  • 静态链接的二进制不能享受 ASLR 的完整保护

这也是为什么安全分析如此重要——不能指望防护替你解决一切问题。

Q6: 这道题为什么选择"分析"而不是"利用"?

安全教育的目的是培养安全的开发者,而非攻击者。理解溢出原理和防护机制后,开发者在写代码时就能本能地避免危险模式。相比之下,CTF 式的利用教学虽然有吸引力,但容易让学生将注意力放在攻击技巧上而非防御思维。

两者的视角差异:

维度CTF 利用教学安全分析教学(本题)
目标获取 flag / shell理解漏洞根源
技能编写 exploit、ROP 链栈帧分析、防护评估
心态"我怎么打破它""我怎么确保它不会破"
适用人群安全研究员、渗透测试所有 C/C++ 开发者
副作用可能强化攻击技能建立安全编码本能

先学会防御,再了解攻击,这才是安全教育的正确顺序。


课后练习

  1. 验证偏移量计算。编写一个简单的程序,在 buf[16] 后声明一个标记变量,用 printf("%p") 打印 buf 和标记变量的地址,计算实际偏移。与课程中的理论值(32 字节)对比,分析差异原因。

    知识点提示:编译器可能重排局部变量顺序、插入 padding、或省略未使用的变量。使用 -O0 -fno-stack-protector 编译以获得最可预测的布局。用 objdump -d 查看反汇编确认实际栈分配大小。

    参考解答
    ex1_verify_offset.c
    c
    #include <stdio.h>
    
    int main(void) {
        char buf[16];
        int marker = 0xDEADBEEF;
    
        printf("buf    address: %p\n", (void*)buf);
        printf("marker address: %p\n", (void*)&marker);
        printf("difference: %ld bytes\n",
               (unsigned long)((char*)&marker - buf));
        return 0;
    }
    
    /* 编译: gcc -O0 -fno-stack-protector -o verify verify.c */
    /* 运行: ./verify */
    /* 期望: difference >= 16 (可能更大,取决于编译器) */
  2. fgets 的安全边界验证。编写程序测试 fgets(buf, 16, stdin) 在输入不同长度时的行为。分别输入 5、14、15、20、100 个字符,用 strlen 和内存 dump 观察 buf[15] 位置的值。

    知识点提示:用管道或重定向提供不同长度的输入。验证 fgets 的行规:最多读 (size-1) 个字符、总是 \0 结尾、超长输入时剩余部分留在输入缓冲区中。

    参考解答
    ex2_fgets_verify.c
    c
    #include <stdio.h>
    #include <string.h>
    
    int main(void) {
        char buf[16];
        if (fgets(buf, sizeof(buf), stdin)) {
            printf("strlen: %zu\n", strlen(buf));
            printf("buf[15] value: 0x%02x\n",
                   (unsigned char)buf[15]);
            /* 检查 buf[15] 总是 \0 */
            if (buf[15] == '\0')
                printf("OK: buf[15] is null terminator\n");
            else
                printf("BUG: buf[15] is NOT null!\n");
        }
        return 0;
    }
    
    /* 测试: echo "12345" | ./a.out         → strlen: 5 (含 \n) */
    /*       echo "12345678901234567890" | ./a.out → strlen: 15 */
    /*       注意: 输入超长时,剩余部分留在 stdin 缓冲区中 */
  3. 观察 Canary 在反汇编中的体现。写一个含有 char buf[16] 的函数,用 gcc -fstack-protector-strong -O0 编译,然后用 objdump -d 查看反汇编。找出 canary 从 TLS 读取和返回前校验的汇编指令。

    知识点提示:在函数序言中找 mov rax, QWORD PTR fs:0x28mov QWORD PTR [rbp-8], rax;在函数尾声找 mov rax, QWORD PTR [rbp-8]xor rax, QWORD PTR fs:0x28 和条件跳转到 __stack_chk_fail

    参考解答
    ex3_observe_canary.c
    c
    #include <stdio.h>
    
    void demo_canary(void) {
        char buf[16];
        fgets(buf, sizeof(buf), stdin);
        printf("%s", buf);
    }
    
    /* 编译: gcc -fstack-protector-strong -O0 -S -o canary.s canary.c */
    /* 或: gcc -fstack-protector-strong -O0 -o canary canary.c */
    /* 然后: objdump -d canary | grep -A20 '<demo_canary>' */
    asm
    ; 关键汇编片段 (x86-64):
    ;
    ; 函数序言:
    ;   push   rbp
    ;   mov    rbp, rsp
    ;   sub    rsp, 32
    ;   mov    rax, QWORD PTR fs:0x28      ← 从 TLS 读取 canary
    ;   mov    QWORD PTR [rbp-8], rax      ← 写入栈帧
    ;
    ; ... 函数体 ...
    ;
    ; 函数尾声:
    ;   mov    rax, QWORD PTR [rbp-8]      ← 从栈帧读取 canary
    ;   xor    rax, QWORD PTR fs:0x28      ← 与 TLS 原始值比较
    ;   je     .L_ok                        ← 相同 → 安全退出
    ;   call   __stack_chk_fail             ← 不同 → 终止
    ; .L_ok:
    ;   leave
    ;   ret
  4. 编写 is_stack_safe 检查函数。实现一个函数,接受字符串长度和缓冲区大小,返回 0(安全)或 1(可能溢出)。考虑 fgetsstrncpy(需 \0)、snprintf 等不同函数的边界语义。

    知识点提示:不同的安全函数的边界语义不同——fgets(buf, N, ...) 最多写 N 字节(含 \0),要求 N <= sizeof(buf)strncpy(dst, src, N) 最多写 N 字节但不保证 \0,安全条件是 N <= sizeof(dst)strlen(src) < N

    参考解答
    ex4_stack_safety_check.c
    c
    #include <stddef.h>
    
    /* 检查 fgets 是否安全 */
    int fgets_is_safe(size_t buf_size, int fgets_n) {
        /* fgets(buf, n, ...) 最多写 n 字节 (含 \0)
         * 安全条件: n <= buf_size */
        return fgets_n <= (int)buf_size;
    }
    
    /* 检查 strncpy 是否安全 (含 \0 语义) */
    int strncpy_is_safe(size_t buf_size, size_t src_len, int n) {
        /* strncpy(dst, src, n): 最多拷贝 n 字节
         * 安全条件: n <= buf_size AND src_len < n (确保 \0) */
        return n <= (int)buf_size && src_len < (size_t)n;
    }
    
    /* 检查 snprintf 是否安全 */
    int snprintf_is_safe(size_t buf_size, int n) {
        /* snprintf(buf, n, ...): 最多写 n 字节 (含 \0)
         * 安全条件: n <= buf_size */
        return n <= (int)buf_size;
    }
  5. 用 checksec 分析系统二进制。在自己的 Linux 系统上,使用 checksec --file=/bin/ls(或 readelf -l / readelf -d)查看系统二进制的安全特性。记录哪些防护已启用,哪些未启用,并思考原因。

    知识点提示checksec 是 pwntools 中的工具。也可用 readelf -l binary | grep GNU_STACK 检查 NX、readelf -d binary | grep BIND_NOW 检查 Full RELRO、readelf -h binary | grep Type 检查 PIE。

    参考解答
    bash
    # 安装 checksec (来自 pwntools)
    pip install pwntools
    checksec --file=/bin/ls
    
    # 手动检查 (无需 checksec):
    # PIE: ELF type 应为 DYN (shared object file)
    readelf -h /bin/ls | grep Type:
    
    # NX: GNU_STACK 段应缺少 E (Execute) 标志
    readelf -l /bin/ls | grep GNU_STACK
    # 期望输出: GNU_STACK 0x... RW  0x... (没有 E)
    
    # RELRO: 查找 BIND_NOW 标志
    readelf -d /bin/ls | grep -E 'BIND_NOW|FLAGS_1'
    # 期望: (BIND_NOW) 或 (FLAGS_1) 包含 NOW
    
    # Full RELRO: 需要有 BIND_NOW
    # Partial RELRO: 有 GNU_RELRO 段但没有 BIND_NOW

    思考:为什么 /bin/ls 的一些防护可能不如预期全面?可能是因为它不处理不可信输入,性能优先于安全;也可能是因为某些防护(如 Full RELRO)对简单二进制收益不大。


参考资料

  • Aleph One. "Smashing The Stack For Fun And Profit." Phrack Magazine, Vol 7, Issue 49, 1996. — 栈溢出利用的经典开山之作
  • Cowan, C. et al. "StackGuard: Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks." USENIX Security, 1998. — Stack Canary 的原始论文
  • Shacham, H. "The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls (on the x86)." ACM CCS, 2007. — ROP 的奠基论文
  • CWE-120: Buffer Copy without Checking Size of Input. https://cwe.mitre.org/data/definitions/120.html
  • CERT C Coding Standard: STR31-C — Guarantee that storage for strings has sufficient space for character data and the null terminator
  • MISRA C:2012, Rule 21.6 — "The Standard Library function gets shall not be used."
  • GCC Manual: Program Instrumentation Options — -fstack-protector, -fstack-protector-strong, -fstack-protector-all

"Those who cannot remember the past are condemned to repeat it." — George Santayana

Released under the MIT License.