1
2
3
4
5
6
7
8
9
10
11
12
|
#include <stdio.h>
int main()
{
int i = 0;
int arr[3] = {0};
for(;i<=3;i++) {
arr[i] = 0;
printf("Hello, World! \n");
}
return 0;
}
|
循环条件 i <= 3 导致 i 取值 0, 1, 2, 3,而 arr[3] 只有合法索引 0, 1, 2。arr[3] = 0 是一次越界写入。
1
2
3
4
5
6
7
8
|
# 无栈保护
gcc -O0 -fno-omit-frame-pointer -fno-stack-protector test.c -o ver_noprot
# 有栈保护(演示 canary 检测)
gcc -O0 -fno-omit-frame-pointer test.c -o ver_prot
# AddressSanitizer(精准捕获越界)
gcc -O0 -fsanitize=address test.c -o ver_asan
|
objdump -d 分析汇编代码
无栈保护 (ver_noprot)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 48 83 ec 10 sub $0x10,%rsp ; 分配 16 字节
; int i = 0
1155: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) ; i 在 rbp-4
; int arr[3] = {0}
115c: 48 c7 45 f0 00 00 00 movq $0x0,-0x10(%rbp) ; arr[0..1] = 0
1163: 00
1164: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp) ; arr[2] = 0 (rbp-8)
116b: eb 20 jmp 118d <main+0x44> ; 跳转到条件检查
; 循环体
116d: 8b 45 fc mov -0x4(%rbp),%eax ; eax = i
1170: 48 98 cltq ; rax = i (符号扩展)
1172: c7 44 85 f0 00 00 00 movl $0x0,-0x10(%rbp,%rax,4) ; arr[i] = 0 ← 越界处
1179: 00
117a: 48 8d 05 83 0e 00 00 lea 0xe83(%rip),%rax ; "Hello, World!"
1181: 48 89 c7 mov %rax,%rdi
1184: e8 c7 fe ff ff call 1050 <puts@plt>
1189: 83 45 fc 01 addl $0x1,-0x4(%rbp) ; i++
; 条件检查
118d: 83 7d fc 03 cmpl $0x3,-0x4(%rbp) ; i <= 3 ?
1191: 7e da jle 116d <main+0x24> ; 是则继续循环
1193: b8 00 00 00 00 mov $0x0,%eax
1198: c9 leave
1199: c3 ret ; 无 canary 检查!
|
栈布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
高地址
++
| 返回地址 | ← rbp+8
++
| 保存的 %rbp | ← rbp+0
++
| i (4 字节) | ← rbp-4 ← arr[3] 写入目标!
++
| arr[2] (4 字节) | ← rbp-8
++
| arr[1] (4 字节) | ← rbp-12
++
| arr[0] (4 字节) | ← rbp-16
++
低地址
|
arr[0] 在 rbp-16,i 在 rbp-4,间距正好 12 字节 = 3 个 int。arr[3] = 0 写入 rbp-16+12 = rbp-4 恰好覆盖 i。
有栈保护 (ver_prot)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
0000000000001169 <main>:
1169: f3 0f 1e fa endbr64
116d: 55 push %rbp
116e: 48 89 e5 mov %rsp,%rbp
1171: 48 83 ec 20 sub $0x20,%rsp ; 分配 32 字节(更大)
; canary 插入
1175: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax ; 从 TLS 读 canary
117c: 00 00
117e: 48 89 45 f8 mov %rax,-0x8(%rbp) ; 存入 rbp-8
1182: 31 c0 xor %eax,%eax
; int i = 0
1184: c7 45 e8 00 00 00 00 movl $0x0,-0x18(%rbp) ; i 在 rbp-24
; int arr[3] = {0}
118b: 48 c7 45 ec 00 00 00 movq $0x0,-0x14(%rbp) ; arr[0..1] = 0 (rbp-20)
1192: 00
1193: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%rbp) ; arr[2] = 0 (rbp-12)
119a: eb 20 jmp 11bc <main+0x53>
; 循环体(同版本 A)
119c: 8b 45 e8 mov -0x18(%rbp),%eax
119f: 48 98 cltq
11a1: c7 44 85 ec 00 00 00 movl $0x0,-0x14(%rbp,%rax,4) ; arr[i] = 0
11a8: 00
11a9: 48 8d 05 54 0e 00 00 lea 0xe54(%rip),%rax
11b0: 48 89 c7 mov %rax,%rdi
11b3: e8 a8 fe ff ff call 1060 <puts@plt>
11b8: 83 45 e8 01 addl $0x1,-0x18(%rbp) ; i++
11bc: 83 7d e8 03 cmpl $0x3,-0x18(%rbp) ; i <= 3 ?
11c0: 7e da jle 119c <main+0x33>
; canary 检查
11c2: b8 00 00 00 00 mov $0x0,%eax
11c7: 48 8b 55 f8 mov -0x8(%rbp),%rdx ; 加载 canary
11cb: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx ; 与原始值比较
11d2: 00 00
11d4: 74 05 je 11db <main+0x72> ; 一致则跳过
11d6: e8 95 fe ff ff call 1070 <__stack_chk_fail@plt> ; 不一致!
11db: c9 leave
11dc: c3 ret
|
栈布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
高地址
++
| 返回地址 | ← rbp+8
++
| 保存的 %rbp | ← rbp+0
++
| canary (8 字节) | ← rbp-8 ← arr[3] 写入目标!
++
| arr[2] (4 字节) | ← rbp-12
++
| arr[1] (4 字节) | ← rbp-16 (movq 的一部分)
++
| arr[0] (4 字节) | ← rbp-20
++
| i (4 字节) | ← rbp-24
++
低地址
|
Canary(金丝雀值)被编译器插入在 arr[2] 和保存的 %rbp 之间(rbp-8)。arr[3] = 0 写入 rbp-20+12 = rbp-8 恰好覆盖 canary。
运行时结果
无栈保护 (ver_noprot)
1
2
3
4
5
6
7
8
|
$ timeout 1 ./ver_noprot 2>&1
Hello, World! # 无限循环输出,1 秒后被 timeout 终止
Hello, World!
Hello, World!
Hello, World!
...(无数行)
$ echo $? # 退出码
124 # 128 + SIGALRM = timeout
|
结果: 无限循环 ✅。每次 i 增长到 3 时,arr[3] = 0 把 i 重置为 0。
有栈保护 (ver_prot)
1
2
3
4
5
6
7
8
9
|
$ stdbuf -oL ./ver_prot 2>&1
Hello, World! # i=0
Hello, World! # i=1
Hello, World! # i=2
Hello, World! # i=3 ← 此时 arr[3] = 0 覆盖了 canary
*** stack smashing detected ***: terminated
Aborted (core dumped)
$ echo $?
134 # 128 + SIGABRT
|
结果: 打印 4 次后,__stack_chk_fail 检测到 canary 被破坏,调用 abort() 终止进程。
⚠️ 注意: 重定向到文件时 stdout 变为全缓冲,abort 会跳过缓冲区刷新。需要使用 stdbuf -oL 强制行缓冲才能看到打印内容。
AddressSanitizer (ver_asan)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
$ timeout 2 ./ver_asan 2>&1
=================================================================
==24785==ERROR: AddressSanitizer: stack-buffer-overflow
on address 0x7ffdb27aa1dc at pc 0x559838411395
WRITE of size 4
#0 0x559838411395 in main (ver_asan+0x1395)
#1 0x7febe40bad8f in __libc_start_call_main
Address 0x7ffdb27aa1dc is located in stack of thread T0
at offset 44 in frame
#0 0x559838411258 in main
This frame has 1 object(s):
[32, 44) 'arr' (line 6)
Memory access at offset 44 overflows this variable
Shadow bytes:
=>0x1000364ed430: 00 00 00 00 00 00 f1 f1 f1 f1 00[04]f3 f3 00 00
^^ ← 越界写入位置
SUMMARY: AddressSanitizer: stack-buffer-overflow (ver_asan+0x1395) in main
|
ASan 解读:
[32, 44) 'arr' — arr 占用字节偏移 32~43(12 字节)
offset 44 — 写入位置在 arr 结束后的第 1 字节
WRITE of size 4 — 4 字节写入(一个 int),正好踩在 arr 边界外的第一个 int
- shadow 字节
00[04] — 00 表示可寻址,04 表示越界 4 字节
对比总结
| 方面 | 版本 A:无保护 | 版本 B:有保护 | 版本 C:ASan |
| - |::|:–:|:-:|
| 栈分配 | sub $0x10 (16 B) | sub $0x20 (32 B) | 由 ASan 自动处理 |
| Canary | ❌ 无 | ✅ 有 (mov %fs:0x28) | ❌ 无(ASan 替代) |
| arr 基址 | rbp-16 | rbp-20 | 由 ASan 布局 |
| i 位置 | rbp-4 | rbp-24 | 由 ASan 布局 |
| arr[3] 覆盖 | i(循环变量) | canary(金丝雀) | ASan redzone |
| 运行时行为 | 无限循环 | 4 次打印 + abort | 3 次打印 + 越界报告 |
| 退出码 | 124 (timeout) | 134 (SIGABRT) | 1 (ASan abort) |
| 防护生效时机 | 永不 | 函数返回前 | 越界即刻 |
汇编差异详解
栈帧大小不同
1
2
3
4
5
|
; 版本 A
sub $0x10,%rsp ; 16 字节:12 (arr) + 4 (i) = 16,紧密排列
; 版本 B
sub $0x20,%rsp ; 32 字节:12 (arr) + 4 (i) + 8 (canary) + 对齐
|
栈保护不仅插入了 canary 检查代码,还改变了整个栈布局,增加了栈帧大小。
arr[i] = 0 的地址计算
两版均使用 -0x10(%rbp,%rax,4) 或 -0x14(%rbp,%rax,4) 的寻址模式——base + index*scale + displacement。当 i = 3 时:
1
2
3
4
5
|
; 版本 A:base = rbp-16, i=3 → rbp-16+12 = rbp-4 = &i ✓ 覆盖 i
movl $0x0,-0x10(%rbp,%rax,4)
; 版本 B:base = rbp-20, i=3 → rbp-20+12 = rbp-8 = &canary ✓ 覆盖 canary
movl $0x0,-0x14(%rbp,%rax,4)
|
函数返回路径
1
2
3
4
5
6
7
8
9
10
11
|
; 无栈保护:简单返回
1198: c9 leave
1199: c3 ret
; 有栈保护:先检查 canary
11c7: mov -0x8(%rbp),%rdx
11cb: sub %fs:0x28,%rdx
11d4: je 11db <main+0x72>
11d6: call __stack_chk_fail@plt
11db: leave
11dc: ret
|
安全启示
<= 是数组遍历的天敌 — 始终用 i < size 而非 i <= size - 1
- 栈保护不是万能的
- canary 只能检测溢出,不能阻止溢出
- 只在函数返回前检查——攻击者在返回前仍有机会造成破坏
- 信息泄露攻击可以读取 canary 值从而绕过