浅谈leave ret指令及其利用
前言
由于在之前pwn的学习中栈迁移只知道怎么用,原理啥的一概不晓,属于一坨的那种,每次碰到栈迁移都无比难受,今天花了一下午的时间浅浅的重新学习一下leave ret以及栈迁移原理。
基础知识
在讲这些东西之前,我觉得有必要把pop 和push指令说明一下,pop指令是把rsp上的值传给某某寄存器,比如,pop rdi,就是把rsp上的值传给rdi,这也就是为什么有人总是说pop是弹栈的指令(白话:rsp在栈顶,pop rdi就是把栈顶上的值弹到了rdi上),同样的 push指令是把某寄存器里的值或者某个字符串传给了rsp上,而原本的rsp上的值被压到了下面,为什么原本在rsp的值会在下面?因为栈是一个后进先出的表,同样的它只会对后进来的字符进行处理。
以下是弹栈(举例pop rdi)的操作
pop rdi前:
pop rdi 后:
pop rdi(以及其他的pop 寄存器指令)是干嘛的呢,有什么作用,简单来说就是传参,大家可以参考其他大佬的文章。
leave指令
这个指令的实际意思就是 mov rsp rbp; pop rbp;(ret就是pop rip)。这个指令听起来就有点绕,简单来说就是,执行了leave指令之后,rsp上的地址变成了rbp的地址+8(64位),rbp上的地址变成了rbp地址里面的东西。听着还是不简单,直接上图。
执行前:
执行后:
漏洞
那么也就说明执行leave之后,rsp是由rbp的地址决定的,ret指令是pop rip,也就是将rsp的值传给rip,那么也就是说,rbp可以控制rip,那有人肯定会说了,你这肯定在扯淡啊,rbp的地址是个栈地址,咱们控制不了啊,最多只能控制rbp的内容啊,那如果我们再执行一遍leave ret呢
leave 前 |
两边leave ret之后,rip的地址为原来的rbp里面的地址(rbp指向的地址)+8的里面的地址( *( *rbp+8)),而rbp里面的东西,我们是可以通过输入来覆盖的,那么也就是说,我们可以通过覆盖rbp和执行两遍leave ret来改变rsp rbp 甚至rip劫持执行流,不光这样,我们还可以往里面写东西(前提是那个地址可写),这也就是所谓的栈迁移。如此而来,我们能够栈迁移的条件是什么?
一般来说
1:能够溢出(来执行leave ret),至少得覆盖rbp;
2:目的地址可写。
例题
我们来找个例题看一下[https://pan.baidu.com/s/1moBfffThW5-ClklOotkt0g?pwd=chen]:
先看保护
开了NX保护
发现只有一个read并且只能溢出8个字节,打ret2libc肯定是不够的(光泄露libc都不行),
我们查看read函数发现我们不光可以覆盖rbp,甚至rsi(这里是指read的第二个参数)我们也能覆盖。在执行read之后还会自动执行一个leave ret。
因此我们可以利用栈迁移来泄露libc,再利用栈迁移来执行system…
我们gdb看一下
我们发现0x600000—-0x601000是可写的,我们可以把栈迁移到这一部分,但是要注意,函数的got表也在这一部分,因此我们不要随意的写。
我们先写第一步,覆盖rbp里面的值,并且把返回地址覆盖成read(溢出的8个字节刚好),而这里的返回地址要能够控制rsi为rbp-buf;
执行leave ret之后
此时执行流被我们控制到了read。而后面关键的来了,我们可以利用read把此时的rbp覆盖,然后rsp 和rip也都能让我们覆盖。
我们同样的覆盖返回地址为read,此时我们leave 一下。
现在我们成功把栈迁移到了可写的段上,注意此时rbp是0x600d38,下次read的buf的地址为rbp-0x20(也就是0x600d18),那么我们下次执行玩read会ret一下rip也就变成了rsp里面的东西,也就是会执行0x600d18里面的东西,
这里我们就成功劫持执行流我们就可以执行puts来泄露libc,只能执行0x30大小(这里read的大小就是0x30)。
随后我们就可以故技重施来执行system(“/bin/sh”);
exp:
from pwn import* |