栈
栈是一种LIFO(last-in,first-out)形式的数据结构,所有的数据都是后进先出。这种形式的数据结构正好满足我们调用函数的方式: 父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回。栈支持两种基本操作,push和pop。push将数据压入栈中,pop将栈中的数据弹出并存储到指定寄存器或者内存中。
栈帧
栈帧,也就是stack frame,其本质就是一种栈,只是这种栈专门用于保存函数调用过程中的各种信息(参数,返回地址,本地变量等)。栈帧有栈顶和栈底之分,其中栈顶的地址最低,栈底的地址最高,SP(栈指针)就是一直指向栈顶的。在x86-32bit中,我们用 ebp 指向栈底、用 esp 指向栈顶。
函数调用
调用有两件事情:第一,将老的(调用者的) ebp 压入栈,此时 esp 指向它。第二,将 esp 的值赋给 ebp, ebp 就有了新的值,它也指向存放老 ebp 的栈空间。这时,它成了是函数 MyFunction() 栈帧的栈底。这样,我们就保存了“调用者”函数的 ebp,并且建立了一个新的栈帧。
函数返回
当函数完成自己的任务后,它会将 esp 移到 ebp 处,然后再弹出旧的 ebp 的值到 ebp寄存器。这样ebp 就恢复到了函数调用前的状态了。
栈溢出
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。此外,我们也不难发现,发生栈溢出的基本前提是
- 程序必须向栈上写入数据。
- 写入的数据大小没有被良好地控制。
过程
通过寻找危险函数,我们快速确定程序是否可能有栈溢出,以及有的话,栈溢出的位置在哪里。常见的危险函数如下
- 输入
- gets,直接读取一行,忽略'\x00'
- scanf
- vscanf
- 输出
- sprintf
- 字符串
- strcpy,字符串复制,遇到'\x00'停止
- strcat,字符串拼接,遇到'\x00'停止
- bcopy
确定填充长度
计算我们所要操作的地址与我们所要覆盖的地址的距离。常见的操作方法就是打开 IDA,根据其给定的地址计算偏移。一般变量会有以下几种索引模式
- 相对于栈基地址的的索引,可以直接通过查看 EBP 相对偏移获得
- 相对应栈顶指针的索引,一般需要进行调试,之后还是会转换到第一种类型。
- 直接地址索引,就相当于直接给定了地址。
一般来说,我们会有如下的覆盖需求
- 覆盖函数返回地址,这时候就是直接看 EBP 即可。
- 覆盖栈上某个变量的内容,这时候就需要更加精细的计算了。
- 覆盖 bss 段某个变量的内容。
- 根据现实执行情况,覆盖特定的变量或地址的内容。
配置pwn环境虚拟
安装ubuntu64后打开终端:
sudo apt-get update
sudo apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
下载pip:
下载pwn:
pip install pwntools
下载pdg:
apt install gdb
下载pwnpd:
sudo apt update
sudo apt-get install gdb python3 python3-pip python3-dev git libglib2.0-dev libcairo2-dev
pip3 install pwndbg
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh
下载libcsearcher:
pip install libcsearcher -i https://pypi.tuna.tsinghua.edu.cn/simple