本文回顾笔者在UCB学习的CS161 的 Memory Safety(内存安全),笔者觉得这一章很有必要单独拉出来整理一下,在一些设计API流程中都有启发意义。
文中的配图均来自课程官方 textbook: https://textbook.cs161.org/
Concepts
Memory Safety(内存安全)关心的是:程序是否会读/写本不该访问的内存。一旦出现违规访问,就可能导致 information disclosure(信息泄露)、logic bypass(逻辑绕过)、甚至 control-flow hijacking(控制流劫持)。
CS161 通常用两类性质来组织这部分内容:
- spatial safety(空间安全):不发生 out-of-bounds(越界)访问,例如数组越界读写。
- temporal safety(时间安全):不发生 use-after-lifetime(生命周期之后仍访问),例如 use-after-free(释放后使用)、double free(重复释放)。
Address Space
一个进程的虚拟地址空间可粗略分成四段:
- code:指令
- static:全局/静态变量
- heap(堆):动态分配(
malloc/free) - stack(栈):函数调用栈帧(locals、saved registers、return address)
常用的结构是:
- heap 向高地址增长
- stack 向低地址增长

这张图的重要性在于:很多漏洞本质是“写穿边界”,覆盖到相邻对象。相邻对象如果是 flag、function pointer、返回地址等关键数据,后果就会变得非常严重。
Endianness
x86 采用 little-endian(小端序):多字节整数的 least significant byte(最低有效字节) 放在最低地址。 下面的图是表示0x44332211这个数字的存储。

这在构造 payload(攻击载荷)时很关键:比如要把某个 32-bit 地址写进 return address,输入字节顺序需要按小端序排列。
Call Stack
理解 stack-based 漏洞,首先要建立 stack frame(栈帧) 的结构直觉。以 32-bit x86 为例,通常关注三个寄存器:
- EIP:instruction pointer(控制流位置)
- EBP:frame pointer(栈帧基址)
- ESP:stack pointer(栈顶)
一个典型栈帧(概念上)从高到低大致是:
- arguments(参数)
- return address(返回地址),常见位置
EBP + 4 - saved frame pointer(保存的帧指针 / SFP),常见位置
EBP - locals(局部变量),常见位置
EBP - ...
此外常用偏移记忆是:第一个参数通常在 EBP + 8。

这类布局直接解释了:如果 local buffer(局部缓冲区)发生 overflow 并向高地址写穿,通常会先碰到 SFP,再碰到 return address。
Overflows
buffer overflow(缓冲区溢出) 指程序向 buffer 写入超过容量的数据,而 C/C++ 默认不做 bounds check,就会导致越界写。
最直接的危害是覆盖相邻数据,例如把 is_admin 之类的 flag 覆盖成非零,从而实现 logic bypass(逻辑绕过)。


Stack Smashing
stack smashing(栈溢出控制流劫持) 的目标是控制数据而不是普通变量。
当 overflow 足够长时,它可以覆盖到:
- saved frame pointer(SFP)
- return address(RIP/EIP)
一旦 return address 被改写,函数执行 ret 时就会跳到攻击者指定的位置,实现 control-flow hijacking(控制流劫持)。

经典攻击是把 shellcode(注入代码) 放进 buffer,并把 return address 改成指向 buffer 的地址。现代系统通常有 NX/DEP 等防护来阻断“写入即执行”,但“覆盖 return address”仍然是理解后续 ROP 的基础。
Format Strings
format string vulnerability(格式化字符串漏洞) 通常来自把用户输入当作 printf 的 format string,例如:
printf(user_input); // vulnerable
printf("%s", user_input); // safe pattern
由于 printf 是 variadic function(可变参数函数),format string 决定了它会从栈上“取多少参数、按什么类型解释”。当 format string 与真实参数不匹配时,printf 会把栈上其它内容当作参数读取,从而产生:
%x/%p:stack scanning(扫栈)→ information disclosure(信息泄露)%s:把栈值当指针解引用 → arbitrary read(任意读)%n:把“已输出字符数”写回某地址 → arbitrary write(任意写)

Integer Conversions
integer conversion vulnerability(整数转换漏洞) 常见于 signed/unsigned 混用、隐式类型转换、整数溢出(wraparound)。
高频模式包括:
- signed-to-unsigned cast bypass:负数
int传给size_t参数后变成巨大正数,导致复制长度爆炸(例如memcpy)。 - wraparound allocation bug:长度计算(如
len + k)在 32-bit 下溢出为很小的数,分配很小的 buffer,却执行很大的 copy/read。
核心原则是:检查要在与最终操作一致的类型域里完成,并对可能的 overflow 做显式处理。
Off-by-One
off-by-one(差一错误) 指只越界写 1 byte,但依然可能可利用。
在栈上,如果这个 1 byte 刚好覆盖到 saved frame pointer 的最低有效字节,可能触发 stack pivot(栈迁移):
- 利用 1-byte overwrite(单字节覆盖)改变 SFP,使其指向攻击者布置的“假栈帧”
- 函数 epilogue(收尾)时从错误位置恢复栈帧
ret从假栈帧取到伪造 return address,转移控制流


Mitigations
Mitigations(缓解措施)的目标不是“消灭 bug”,而是降低 exploitability(可利用性),提高攻击成本,形成 defense in depth(纵深防御)。
Safe APIs
优先使用带长度的 API,减少“无边界写”的机会:
fgets替代getssnprintf替代sprintf- 使用
memcpy/memmove时严格验证长度
Non-executable Pages
NX / DEP(不可执行页) 把 writable 与 executable 分离,阻断“把 shellcode 写进栈/堆然后跳过去执行”的经典路径。
对应的常见攻击演化是 code reuse(代码复用):
- return-to-libc:跳转到现成 libc 函数
- ROP(Return-Oriented Programming):用 gadgets(以
ret结尾的短指令序列)拼链执行
Stack Canaries
stack canary(栈金丝雀) 在 locals 与控制数据之间插入随机值,返回前检查是否被改写;若被改写则终止程序。它对“连续写穿到 return address”的攻击非常有效。

ASLR
ASLR(地址空间布局随机化) 随机化 stack/heap/libs 等基址,阻止写死地址的 ret2libc/ROP。很多情况下,攻击者需要先获得 address leak(地址泄露)才能稳定利用。
Pointer Authentication
pointer authentication(指针认证)(如 PAC 思路)为指针/返回地址附加认证信息,阻止攻击者伪造指针值。即便内存可被改写,未通过认证的指针也难以被 CPU 接受。
Defense in Depth
典型组合是:
- NX 阻止注入代码直接执行
- canary 阻止大量 stack-return overwrite
- ASLR 提高定位 gadget/libc 的难度
- pointer authentication 进一步保护控制数据完整性
在现实攻击中,往往需要“漏洞 + 信息泄露 + 代码复用 + 绕过多层防护”才能成功。
Engineering
Memory-safe languages
采用 memory-safe language(内存安全语言)(如 Java、Go、Python,或 Rust 的 safe subset)可以从语言层面避免大类 memory safety bug(bounds check、lifetime rules 等)。
Analysis and testing
在必须使用 C/C++ 的系统里,工程上常见的降低风险方式包括:
- static analysis(静态分析)
- dynamic analysis(动态分析,如 sanitizers)
- fuzzing(模糊测试)
- 依赖更新与补丁管理
Checklist
- Bounds:所有 copy/read/write 都有明确的上限与验证。
- Types:避免 silent signed/unsigned conversion;对
size_t与 wraparound 做显式保护。 - Formats:不允许
printf(user_input);format string 必须受控;避免%n暴露。 - Build:开启 NX、stack canary、ASLR;平台支持时启用更强的硬件/ABI 防护。
- Process:sanitizers + fuzzing 纳入 CI;依赖保持更新。