栈上数组越界与栈溢出

引言

在学习 C 语言的过程中,使用数组,字符串(可以理解为特殊的数组)时,都会注意下标是否会越界,以保证程序正常运行。而在这里,我们将会反其道而行之,探索栈溢出或栈上数组越界时会发生什么,以及怎么利用这些漏洞实现攻击。

虚拟地址空间(以 Linux 为例)

在现代的操作系统中,

每个进程都有一个独立,连续的虚拟地址空间,而虚拟地址空间是按页(4k 大小)与物理内存一一对应,按需分配。用户态的程序基本只能接触的虚拟地址空间,对内存的操作也基本是对虚拟地址空间的操作。

下图即为 Linux 系统基本的虚拟地址空间的结构

Linux系统基本的虚拟地址空间的结构

kernel space 为系统内核的内存映射

stack 为进程的栈内存

dynamic link libraries 为动态链接库的内存映射

heap 为堆内存

ELF 为可执行程序的内存映射

GAP 为未使用的空白内存

基本的栈帧结构(以 x64 的栈为例)

在 C 语言中,函数的临时变量是储存于栈上的。栈的增长方向是高地址向低地址,栈底在高地址一侧。

每个函数有自己对应的栈帧,下图为栈帧的基本结构。

栈帧的基本结构

RBP 为栈底寄存器,RSP 为栈顶寄存器,分别记录了栈帧中记录数据部分的起始和终止地址。函数的临时变量的在内存中的位置都是通过这两个寄存器加减偏移确定的。

栈底分别还记录了上一个栈帧的 RBP 的值,以及函数的返回地址。

函数调用与栈帧变化

以下代码案例演示了函数调用时栈帧的压栈和出栈过程:

#include <stdio.h>

void test() {
    char* str = "Hello World!";
}

int main() {
    test();
    return 0;
}

看一下汇编:

pwndbg> disassemble test
Dump of assembler code for function test:
   0x0000555555555129 <+0>:     endbr64 
   0x000055555555512d <+4>:     push   rbp
   0x000055555555512e <+5>:     mov    rbp,rsp
   0x0000555555555131 <+8>:     lea    rax,[rip+0xecc]        # 0x555555556004
   0x0000555555555138 <+15>:    mov    QWORD PTR [rbp-0x8],rax
   0x000055555555513c <+19>:    nop
   0x000055555555513d <+20>:    pop    rbp
   0x000055555555513e <+21>:    ret    
End of assembler dump.
pwndbg> disassemble main
Dump of assembler code for function main:
   0x000055555555513f <+0>:     endbr64 
   0x0000555555555143 <+4>:     push   rbp
   0x0000555555555144 <+5>:     mov    rbp,rsp
   0x0000555555555147 <+8>:     mov    eax,0x0
   0x000055555555514c <+13>:    call   0x555555555129 <test>
   0x0000555555555151 <+18>:    mov    eax,0x0
   0x0000555555555156 <+23>:    pop    rbp
   0x0000555555555157 <+24>:    ret    
End of assembler dump.

call 0x555555555129 <test>可以近似理解为 push addr_after_call; jmp testleave 可以近似理解为 mov rsp, rbp; pop rbpret 可以近似理解为 pop rip

对应的栈帧变化如下(添加底色的汇编指令为即将执行的指令,call test 为 GIF 的开始)

数组越界

以下案例第二个 for 循环中存在数组越界(相比源代码有修改):

#include <stdio.h>

int main() {
    unsigned long long arr[10];
    int i;  // 在函数作用域声明

    for (i = 0; i < 10; i++) {
        arr[i] = 0xdeadbeef;
    }

    for (i = 0; i < 12; i++) {
        printf("arr[%d] = 0x%llx\n", i, arr[i]);
    }

    return 0;
}
// gcc -no-pie -fno-stack-protector -g test2.c -o a.out

输出:

# zhailin @ DESKTOP-4OQQP8F in ~/365-Days-Get-ISCAS-Internship/week1/stack_overflow_code on git:main x [17:17:10] 
$ ./a.out 
arr[0] = 0xdeadbeef
arr[1] = 0xdeadbeef
arr[2] = 0xdeadbeef
arr[3] = 0xdeadbeef
arr[4] = 0xdeadbeef
arr[5] = 0xdeadbeef
arr[6] = 0xdeadbeef
arr[7] = 0xdeadbeef
arr[8] = 0xdeadbeef
arr[9] = 0xdeadbeef
arr[10] = 0x1000
arr[11] = 0xa0000000b

可以看到程序并没有出现异常退出的情况,同时 arr[10]arr[11] 也读出了数据。

pwndbg> b 9
Breakpoint 1 at 0x401164: file test2.c, line 11.
pwndbg> r
Starting program: /home/zhailin/365-Days-Get-ISCAS-Internship/week1/stack_overflow_code/a.out 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main () at test2.c:11
11          for (i = 0; i < 12; i++) {
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────────────────
 RAX  9
 RBX  0
 RCX  0xdeadbeef
 RDX  0x7fffffffd838 —▸ 0x7fffffffdba7 ◂— 'USER=zhailin'
 RDI  1
 RSI  0x7fffffffd828 —▸ 0x7fffffffdb5b ◂— '/home/zhailin/365-Days-Get-ISCAS-Internship/week1/stack_overflow_code/a.out'
 R8   0x7ffff7e1bf10 (initial+16) ◂— 4
 R9   0x7ffff7fc9040 (_dl_fini) ◂— endbr64 
 R10  0x7ffff7fc3908 ◂— 0xd00120000000e
 R11  0x7ffff7fde660 (_dl_audit_preinit) ◂— endbr64 
 R12  0x7fffffffd828 —▸ 0x7fffffffdb5b ◂— '/home/zhailin/365-Days-Get-ISCAS-Internship/week1/stack_overflow_code/a.out'
 R13  0x401136 (main) ◂— endbr64 
 R14  0x403e18 (__do_global_dtors_aux_fini_array_entry) —▸ 0x401100 (__do_global_dtors_aux) ◂— endbr64 
 R15  0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0
 RBP  0x7fffffffd710 ◂— 1
 RSP  0x7fffffffd6b0 ◂— 0xdeadbeef
 RIP  0x401164 (main+46) ◂— mov dword ptr [rbp - 4], 0
──────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────────────────
 ► 0x401164 <main+46>    mov    dword ptr [rbp - 4], 0     [0x7fffffffd70c] <= 0
   0x40116b <main+53>    jmp    main+94                     <main+94>
    ↓
   0x401194 <main+94>    cmp    dword ptr [rbp - 4], 0xb     0x0 - 0xb     EFLAGS => 0x297 [ CF PF AF zf SF IF df of ac ]
   0x401198 <main+98>  ✔ jle    main+55                     <main+55>
    ↓
   0x40116d <main+55>    mov    eax, dword ptr [rbp - 4]                EAX, [0x7fffffffd70c] => 0
   0x401170 <main+58>    cdqe   
   0x401172 <main+60>    mov    rdx, qword ptr [rbp + rax*8 - 0x60]     RDX, [0x7fffffffd6b0] => 0xdeadbeef
   0x401177 <main+65>    mov    eax, dword ptr [rbp - 4]                EAX, [0x7fffffffd70c] => 0
   0x40117a <main+68>    mov    esi, eax                                ESI => 0
   0x40117c <main+70>    lea    rax, [rip + 0xe81]                      RAX => 0x402004 ◂— 'arr[%d] = 0x%llx\n'
   0x401183 <main+77>    mov    rdi, rax                                RDI => 0x402004 ◂— 'arr[%d] = 0x%llx\n'
───────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────────────
In file: /home/zhailin/365-Days-Get-ISCAS-Internship/week1/stack_overflow_code/test2.c:11
    6 
    7     for (i = 0; i < 10; i++) {
    8         arr[i] = 0xdeadbeef;
    9     }
   10 
 ► 11     for (i = 0; i < 12; i++) {
   12         printf("arr[%d] = 0x%llx\n", i, arr[i]);
   13     }
   14 
   15     return 0;
   16 }
───────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffd6b0 ◂— 0xdeadbeef
... ↓        7 skipped
─────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────
 ► 0         0x401164 main+46
   1   0x7ffff7c29d90 __libc_start_call_main+128
   2   0x7ffff7c29e40 __libc_start_main+128
   3         0x401075 _start+37
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p i
$1 = 10
pwndbg> p/x &arr
$2 = 0x7fffffffd6b0
pwndbg> p/x &arr[i]
$3 = 0x7fffffffd700
pwndbg> p/x &arr[i]
$4 = 0x7fffffffd700
pwndbg> stack 20
00:0000│ rsp 0x7fffffffd6b0 ◂— 0xdeadbeef
... ↓        9 skipped
0a:0050│-010 0x7fffffffd700 ◂— 0x1000
0b:0058│-008 0x7fffffffd708 ◂— 0xa00401050
0c:0060│ rbp 0x7fffffffd710 ◂— 1
0d:0068│+008 0x7fffffffd718 —▸ 0x7ffff7c29d90 (__libc_start_call_main+128) ◂— mov edi, eax
0e:0070│+010 0x7fffffffd720 ◂— 0
0f:0078│+018 0x7fffffffd728 —▸ 0x401136 (main) ◂— endbr64 
10:0080│+020 0x7fffffffd730 ◂— 0x1ffffd810
11:0088│+028 0x7fffffffd738 —▸ 0x7fffffffd828 —▸ 0x7fffffffdb5b ◂— '/home/zhailin/365-Days-Get-ISCAS-Internship/week1/stack_overflow_code/a.out'
12:0090│+030 0x7fffffffd740 ◂— 0
13:0098│+038 0x7fffffffd748 ◂— 0xfd2633626e8069eb

现在我们可以通过数组越界读到栈上的内容,我们继续尝试通过数组越界来往栈上写东西。

通过越界篡改栈上的内容

以下程序中arr[11] = 0xcafebabe;存在数组越界。

// gcc -no-pie -fno-stack-protector -g test.c
#include <stdio.h>

int main() {
    unsigned long long arr[10];
    unsigned long long var = 0xdeadbeef;

    printf("var = %llx\n", var);

    arr[11] = 0xcafebabe;

    printf("var = %llx\n", var);

    return 0;
}

输出如下,其中var被成功修改:

# zhailin @ DESKTOP-4OQQP8F in ~/365-Days-Get-ISCAS-Internship/week1/stack_overflow_code on git:main x [18:00:39] 
$ gcc -no-pie -fno-stack-protector -g test3.c

# zhailin @ DESKTOP-4OQQP8F in ~/365-Days-Get-ISCAS-Internship/week1/stack_overflow_code on git:main x [18:00:52] 
$ ls
a.out  test.c  test2.c  test3.c

# zhailin @ DESKTOP-4OQQP8F in ~/365-Days-Get-ISCAS-Internship/week1/stack_overflow_code on git:main x [18:00:53] 
$ ./a.out 
var = deadbeef
var = cafebabe

通过越界控制程序流

**Key: **

  1. 控制程序流的本质就是控制 rip 寄存器。

  2. 结合栈上的返回地址和 ret 指令实现控制 rip 寄存器。

具体例子可以去看CTF-Wiki上的ret2text


栈上数组越界与栈溢出
https://zer0ptr.github.io/2025/11/01/stack-overflow/
作者
zer0ptr
发布于
2025年11月1日
许可协议