数组越界与栈溢出

 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, 2arr[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-16irbp-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] = 0i 重置为 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 值从而绕过
Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计