题目来源:https://ctf.bugku.com/challenges#pwn3
0x00 题目分析
首先常规操作checksec一波
????…护盾全开?那还做个🔨 (╯‵□′)╯︵┻━┻
稳住,先别急着放弃,至少拖入IDA看一眼
人没了…无后门函数,代码还这么复杂。不过静心看一遍,只有33行开始才是有用的,然后再仔细看一眼,只有红框处的两句是实际有用的,这样问题就能简化很多了。
然后来分析一波题目。很明显,第一个红框处的read是用于栈溢出的,而且溢出长度还自定义,,虽说下面有一个if判断读入长度是否大与0x270,但实则无所谓的,在第一个read中栈溢出后,if检测到过长,再次read时,只需要send一个小于0x260长度的字符串即可,这样就不会影响第一次溢出所要进行的操作。
这题的难点在于它的护盾全开,需要我们多次溢出获取canary和基址,但本质上是不难的,因为每次溢出原理几乎是相同的,只是exp会略长一些。这个程序每执行一次vul函数会有两次read,且第一次read后紧跟一个puts,puts是通过"\x00"截断输出字符串,所以我们只需要第一次read覆盖"\x00"然后用puts溢出一个值,然后在第二个read中修改ret地址劫持流程跳转回main函数用于恢复栈即可。通过多次调用vul函数来leak出所有需要的值最后就能getshell了。所以步骤如下
- 覆盖canary最低位,leak canary
- 输出vul函数的ret地址,减去偏移从而leak程序基址
- 计算偏移,输出栈上main函数的返回地址,减去偏移从而leak libc的基址
- 调用libc system函数getshell
0x01 leak canary
先看一下程序栈结构
很常规,canary在rbp上面,那根据canary最低为必为"\x00"的特点,我们将其最低为覆盖,用puts接收后再将其置为原值即可。这里没什么套路直接上exp:
|
|
但是这里需要解释一下如何恢复栈,也就是payload最后为什么要加"\x20"。根据PIE的特点,地址的末三位是不会被随机化的,也就是我们IDA中看到的值,但在实际操作上,在程序基址还没有泄露时,我们只能操作末两位(因为只能两位两位的填充,倒数第四位是随机化的,所以没法修改倒数第三位)
可以看到,vul函数的ret地址末三位是0xD2E,而main函数的起始地址是0xD20,它们倒数第三位是一样的,那就说明可以通过上述方法覆盖实现跳转。但为什么覆盖rbp以后两位就是覆盖ret的末两位呢?因为x86架构全部采用小端序存储,即高字节存在高地址处,然后栈是从高地址向低地址增长的,那ret的末两位(低字节)自然存在栈的低地址处,即靠近rbp的那一头,所以覆盖rbp以后的两位就是覆盖了ret的末两位。
0x02 leak base_elf
拿到程序canary以后就可以随便实现栈溢出了,所以接下来需要leak程序基址,把PIE给破防了。那怎么leak程序基址呢?按照前面的思路,padding至rbp为止,然后用个recvuntil就可以输出ret的地址了。前面都好处理,关键点在泄露出ret的地址后,如何得知偏移。从上面那张反编译后的Text View图可以看出,main函数里通过call调用了vul函数,而call指令会将下一条指令的地址压栈,vul函数执行完毕后,ret指令就会将RIP指向call指令的后一条指令的地址。分析出原理后,偏移就显而易见了,vul函数的ret值相对于程序基址的偏移就是0xD2E
|
|
0x03 leak base_libc
做到这里为止,已经相当于关掉了canary和PIE保护,那这题就已经和最普通的ret2libc题没区别了。这里我想到可以泄露libc基址的方法有两种(如果有大师傅想到别的骚操作欢迎留言),一种是延续前面的方法,栈溢出后仍一直padding,直至main函数的返回地址前,输出main函数的ret的值。main函数的ret的地址是libc空间中的某条指令,计算偏移即可获取libc基址;另一种方法则是常规的借助base_elf调用puts函数输出puts函数的真实地址,因为puts函数是libc函数,减去其偏移即可得到libc基址。
这里我采用第一种方法,这个方法难点在于main的ret地址与rbp之间的偏移如何计算,其实也不难,直接read前下断点或者edb跟踪一下就看出来了(注意要在前面的工作已经做完的情况下再下断点或跟踪,重复调用main函数这个过程是会栈抬升的)
然后要注意的是,得到ret的地址后,它与libc基址并不是libc.symbols["__libc_start_main"],而是libc.symbols["__libc_start_main"]+240 如果你问这240是怎么来的,emmm….这图上不写着了嘛 (╬▔皿▔),然后其实main函数ret的值一定是 __libc_start_main函数地址加240位偏移
|
|
如果用第二种方法,那更简单,payload改成这样就行了,最常规的套路
payload='a'*600+p64(canary)+p64(1)+p64(pop_rdi+base_elf)+p64(elf.got['puts']+base_elf)+p64(elf.plt['puts']+base_elf)+p64(base_elf+0xd20)
0x03 getshell
然后结束了呀,所有需要的值都已经leak了,直接常规操作弹shell
|
|
总结一下吧,这题考察点其实就是破三个护盾,整体流程应该是简单的,多次劫持流程就行,没有别的设坎的地方,我所用的方法是最常规的,不知道有没有更骚的姿势或者其他非预期方法,或者有没有写错的地方_(:」∠),欢迎大师傅们留言指出。