长亭百川云 - 文章详情

格式化字符串漏洞攻击实战

闻道解惑

38

2024-07-13

 cheating 是 CTF 里的一道 PWN 题。主要攻击点就在于格式化字符串漏洞的利用。

一、陷阱

题目中布置了一个陷阱。如果用 IDA 6.8 来分析就很容易陷入陷阱,用 IDA 7.0 分析就会发现一些不一样的地方。

可以发现,IDA 6.8 识别出的 strcmp、puts 被 IDA 7.0 识别为了 strncmp、printf。用 readelf -r 查看,和 IDA 6.8 的结果一致。

为什么 IDA 6.8 和 readelf 会显示出错误的库函数?原因在于 cheating 文件中的 .dynstr section 进行了特殊处理,布置了一个陷阱。用 readelf -S 看下。

可以看出,cheating 文件的 .dynstr 需要被加载到内存的 0x400490 地址,对应在文件中的 offset 为 0xf91。看下这个 string table 的内容。

这个 string table 写的确实是 strcmp 和 puts。细心一点会发现,这两个函数名后面都有多余的0x00,出题者还是留下了一点篡改的痕迹:)

但事实上,加载 ELF 文件时,并不会加载 0xF91 的 string table,而是会加载位于 0x490 位置的 string table,这里才是对应 .dynstr 目标地址 0x400490 的真命天子 。

好了,现在我们知道,可以关掉被误导的 IDA 6.8,继续用 IDA 7.0 来分析程序吧。

二、主流程和 sub_400ACC() 的输入检查

首先看一下防御情况。

cheating 的主函数 sub_400BC0() 如下。

函数的逻辑是:

  • 1、调用 sub_400ACC() 进行输入检查

  • 1.1、如果检查不通过,goto 2

  • 1.2、如果检查通过,接收用户输入并传递给 printf 输出,触发格式化字符串漏洞

  • 2、输出bye并退出

如果要触发格式化字符串漏洞,首先需要通过 sub_400ACC() 的检查。看一下这个检查函数。

主要逻辑是:

  • 1、生成一个64字节的字符串,其中前十个字节固定为 “cheating U”, 后54个字节为0-9随机字符。

  • 2、用户输入字符串,与这个随机字符串进行 strncmp,相同则检查通过。

处理了陷阱之后,我们会发现这里的检查函数用的是 strncmp,只比较了十一个字节。排除掉固定前缀 cheating U 的十个字节,也就只剩下一个字节,范围在 0-9。我们选定一个值(比如0),进行多次碰撞就可以了。如果没有识别出陷阱,把这里误以为是 strcmp,发现必须碰撞54个字节的随机值,就只能一头雾水地发呆啦。

攻击脚本如下。

重复触发几次之后,接收到 slogan: 字符串,顺利通过检查!

三、格式化字符串漏洞

通过校验之后,回到主函数 sub_400BC0() 的 if 分支内,printf 的第一个参数可控,这里是很明显的格式化字符串漏洞。

标准做法,分三步来实现 get shell

  • 解决程序的退出问题

  • 泄漏 system 和 /bin/sh 的地址

  • 执行 sytem(“/bin/sh”)get shell

3.1 程序退出问题

很显然,printf 执行完成之后,程序就不再接收用户输入,而是继续执行输出“bye”并退出。我们需要让程序不退出,而是重新回到触发格式化字符串漏洞的地方,以便于进一步的利用。

因此,我们需要找一个地址来改写,修改代码流程。看一下程序段,发现代码段是不能修改的,不过可以修改got表。

很容易想到,把 exit 的 got 表地址改掉,改到 if 分支里,就可以在调用 exit 的时候回到主流程中。

exit 的 got 表地址是 0x602078,默认值是 plt 表中 exit 表项中 jmp 指令的下一条指令地址 0x400846,我们要将这个值,修改为目标地址 0x400BE9。也就是说,需要修改两个字节,将 0x602078地址的两个字节从 0x846修改为 0xBE9

所以,我们构造如下代码。

执行之后,再次接收到 slogan: 字符串,成功将代码流程劫持,可以进入下一步攻击了。

需要注意的是,我们将 exit() 的 got 地址修改为 0x400BE9 之后,实际上是通过一次 call 指令重入了当前函数,也就意味着栈被抬高了一层(call 指令用于保存函数返回地址)。后续继续使用 printf 的格式化字符串漏洞时,每次都会多偏移一个参数的位置,这一点需要注意。

3.2 泄漏 system 和 /bin/sh 的地址

要 get shell,我们需要泄漏出 glibc 中 system() 和 /bin/sh 的地址。在环境提供了 libc.so.6 文件的条件下,我们只需要泄漏出任何一个库函数的地址,都可以通过文件中的偏移来计算出我们想要的符号地址。

看一下 got 表,我们选择 read 函数来泄漏地址。为什么选择 read?回看一下主函数。

在存在漏洞的 printf 函数执行前,read 函数已经被调用了,所以此时 got 表中 read 函数的表项中已经保存了它在 glibc 库中的真实地址。

也就是说,我们需要泄漏出 0x602050 地址的内容。用 “%s” 就好了。

执行一下,成功获取到 system() 和 /bin/sh 的内存地址!

3.3 执行 sytem(“/bin/sh”)get shell

我们已经拿到 system 的地址,还有任意地址写的漏洞,也能布置栈空间。接下来就是看怎样调用 system(“/bin/sh”) 最方便了。

有很多方法可以实现这一步。最常用的方法是,利用 x64 程序的万能 gadget:**init()**函数,通过 ROP 来实现。

有没有更轻松的方法呢?回看一眼主函数。

咦? printf 的入参就是用户输入的 buf。这就意味着,只要我们把 printf 的 got 表改成 system 的地址,下一轮迭代时再发送 “/bin/sh” 的字符串,就可以直接执行 system(“/bin/sh”) 了,很简单是不是:)

查一下 got 表。printf 的地址是 0x602030,我们的目标是将这个地址的内容改写为前面获取到的 system 函数的真实地址。

攻击脚本如下:

执行一下,成功 Get Shell !

原始程序下载 cheating:http://www.yaowendao.com/image/cheat

攻击脚本链接 pwn_cheating.py:http://www.yaowendao.com/image/pwn\_cheating.py

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2