栈上数组越界与栈溢出
引言
在学习 C 语言的过程中,使用数组,字符串(可以理解为特殊的数组)时,都会注意下标是否会越界,以保证程序正常运行。而在这里,我们将会反其道而行之,探索栈溢出或栈上数组越界时会发生什么,以及怎么利用这些漏洞实现攻击。
虚拟地址空间(以 Linux 为例)
在现代的操作系统中,
每个进程都有一个独立,连续的虚拟地址空间,而虚拟地址空间是按页(4k 大小)与物理内存一一对应,按需分配。用户态的程序基本只能接触的虚拟地址空间,对内存的操作也基本是对虚拟地址空间的操作。
下图即为 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 test,leave 可以近似理解为 mov rsp, rbp; pop rbp,ret 可以近似理解为 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: **
-
控制程序流的本质就是控制
rip寄存器。 -
结合栈上的返回地址和
ret指令实现控制rip寄存器。
具体例子可以去看CTF-Wiki上的ret2text