栈
栈
Source栈基础
栈的结构
关于栈的定义,在指南中有这么部分的描述:栈空间是计算机内存中确定了的内存区域,也有着一些指针指向相应的内存地址,在x96架构下这个指针位于ESP寄存器,而在x86-64下位于RSP寄存器,
而在计算机底层,栈的主要的用途是:(1):存储局部变量,(2):执行CALL指令调用函数的时候,保存函数地址以便函数结束时正确返回,(3):传递函数参数
栈的主要有两种操作,push 和 pop
1 | push 用于压栈,将数据压入栈中(栈顶) // 这个过程中需要对ESP/RSP/SP/进行+4/+8的操作,因为只有这样才能使得压进栈的参数正确入栈 |
程序的栈是从进程地址空间的高地址向低地址增长的
那么是怎么确定栈的呢?
答:栈是由无数个栈帧组成,
那么怎么确定一个栈帧呢?
答:两种寄存器,esp(rsp) 和 ebp(rbp) ,esp(rsp)代表了栈顶,而 ebp(rbp) 代表了栈底,二者中间的内存就构造起了一个栈帧
那么程序是怎么执行的呢?执行的过程是怎么控制的呢?
答:程序的执行是由一个寄存器eip(rip)控制的,当eip(rip)指向哪条汇编就会执行这条汇编,那么只要我们控制eip也就相当于控制了程序的执行过程,那么这个我们也称呼其为 控制程序执行流
ok,现在是栈的基础部分:寄存器(这里只说通用寄存器),和传参;
寄存器和传参
i386(32位)系统中存在以下的(通用)寄存器
1 | eax: 通常用来执行加法,函数调用的返回值一般也放在这里面 |
x64(64位)
因为是64位系统,所以为了兼顾使用,就选择i386的版本进行更新拓展,使i386的版本的寄存器变成了x64的低32位,
所以寄存器相关效果和x86没什么区别
但是名字出现了区别:现在使r开头而不是e开头,其余的都不变,
同时新增了8个新的通用寄存器
1 | r8~r15 |
至于传参顺序就一句话,32位使用栈传参 ,按照顺序一个个传入对应的函数,比如:
1 | 存在函数read(int fd, void *buf, size_t count) |
至于64位就不一样,这个是先使用寄存器传参,然后再用栈传参
这六个寄存器分别是:
1 | RDI, RSI, RDX, RCX, R8, R9 |
因此,如果想调用read函数
1 | payload = p64(read) + p64(rdi) + p64(fd) + p64(rsi) + p64(*buf) + p64(rdx) + p64(count) |
ret2系列
ret2text
我们把函数存在的地方叫做text段,
什么是栈溢出?
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变
栈的结构如下(以32位为例 64位一样的)
1 | | esp | |
那么我们栈溢出使得我们可以修改ebp 和 retn里面的数据,那么此时我们就可以使控制程序的程序执行流
好,那让我说说为什么可以控制程序执行流(控制eip)
ret 的汇编其实是 pop eip 就是将栈顶的值传递给eip,同时esp+4(将这个值删掉),那么当执行到ret 的时候,esp在retn这里,那么就会把retn里面的值弹给eip,那么eip就会跳到那个内容里面继续执行,那么这个时候我们修改这个值不就可以控制eip跳转到我们想要其执行的地方嘛!
这就是ret2text,跳转回text段上执行恶意代码
SROP
sigreturn是一个系统调用,在类 unix 系统发生 signal 的时候会被间接地调用
signal 机制
signal 是一种类 unix 系统中进程之间相互传递信息的一种方法,也就是所谓的信号软中断,进程之间可以通过系统调用 kill 来发送软中断信号
在发生signal的时候,会进入内核态,会将进程挂起,内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址
而这个函数的作用有且只有一个,那就是pop ,将栈上的东西依次弹给寄存器,(注:所有寄存器)
