专栏名称: CTFer的魔法棒
CTFer的魔法棒,你的CTF参赛指南。 查看比赛日程、学习竞赛相关知识应有尽有
目录
相关文章推荐
企名片  ·  2024年中最佳财务顾问TOP10——云道资本 ·  13 小时前  
企名片  ·  2024年中最佳财务顾问TOP10——云道资本 ·  13 小时前  
会计雅苑  ·  软件行业舞弊迹象及审计应对之策 ·  昨天  
会计雅苑  ·  中国邮政集团关于变更会计师事务所的公告 ·  3 天前  
马靖昊说会计  ·  关乎你的钱袋,2025年度个人所得税专项附加 ... ·  5 天前  
51好读  ›  专栏  ›  CTFer的魔法棒

【技术分享】教练!那根本不是IO!——从printf源码看libc的IO

CTFer的魔法棒  · 公众号  ·  · 2017-11-08 19:28

正文




本文转载自安全客

原文链接:http://bobao.360.cn/learning/detail/4490.html               

点击文末阅读原文即可跳转



前(fei)言(hua)


我们似乎天天都在使用IO,最典型的使用就是printf,scanf,以前我们只知道printf会有格式化字符串漏洞,可是我们并没有怎么深究过IO具体的是怎么回事,以及具体有什么可以攻击的点。

2016 HITCON有一道 house of orange,是一道堪称经典的题目,第一次(或者似乎是第一次?)让我们把攻击的思维往IO FILE里去考虑,于是我们开始思考libc的虚表的可攻击性,不幸的是,libc的开发人员也很快意识到了这个虚表的问题,在2.24的libc版本中对vtables进行了加固:

2.24 libc更新日志中的一个内容:
 [20191] stdio: libio: vtables hardening


于是这个方法慢慢变得困难了起来,还好我们的思路不仅仅是这样……

本文主要从经典的虚表原理开始说起,中间补充一下scanf和printf的原理,最后提到一种较新的(或者是我认为较新的?)思路。


从虚表开始说起


首先我们来看下经典的(虽然似乎是2016之后才流行起来的)_IO_FILE_plus的虚表攻击方式。

1._IO_FILE 与 _IO_FILE_plus

源码永远是回答心中疑问的好老师,首先来看看关于这两个结构体的源码:

以及_IO_FILE_plus:


我们可以看到_IO_FILE_plus的组成,其实就是一个_IO_FILE结构体本身再加上一个跳表,从plus这个名称我们也能看出来,其实这个地方是为了兼容C++,对于C++的对象来说,除了数据以外还有方法,方法的实现是会用到跳表的,为了能够兼容,除了_IO_FILE本身以外,只能再添加一个跳表,然后使用新的结构体来进行兼容。

事实上在libc内部对于FILE结构体就是用_IO_FILE_plus来进行表示的,但是对于pwn选手来说,只要有函数指针,就有控制执行流的可能,唯一的问题是,用谁的函数指针?

这个其实并不是一个难事,因为每一个文件一定都有3个FILE,也就是以下三个,我想大家已经不能再熟悉他们了:

是的,就是stdin, stdout和stderr,好了,那么这种利用的思路应该就比较明确了:只要我们有办法控制stdin,stdout和stderr的虚表指针,我们就能够在使用到这三个结构体的虚表的时候控制执行流。

不过还有一个小问题,到底在什么时候这些函数指针会被用到?那么让我们继续从输入输出开始说起……

2.你不熟悉的scanf和printf

以下内容源码较长,可能引起不适,请适度观看。为了简单,我们就从printf开始看。首先是printf的入口:

直接移交给了vfprintf,好吧,再来看vfprintf

(觉得代码太长的同学可以直接跳到最后看结论)

(小编注:这段代码太长了,同学们如果觉得阅读不便请直接点击阅读原文)



这一段代码估计已经把大家的汗都看出来了,我们做个总结吧:其实就一句话,printf最终调用了虚表里的函数来完成输出任务

也就是说,只要使用了printf,我们就相当于调用了虚表里的某个函数,具体哪一个还需要从源码去看,不过关于虚表的部分说到这基本也就够了,scanf的内容其实也是一样,最终都会到虚表里进行执行。

到这里,我们就解决了关于利用虚表时候的问题,那就是什么时候调用,所以只要有输入输出,我们就可以调用到虚表的某个函数了

3.总结一下虚表的利用方法

因为libc中的标准输入输出函数会用到stdin,stdout和stderr几个结构体,而最终都会使用虚表函数来完成具体操作,所以如果可以操作虚表指针,就可以控制执行流。

4.libc-2.24

在2.24中,增加了一个虚表的检测机制,也就是虚表必须位于某一个位置以内,超过这一段就会直接被abort掉,所以这个看似美好的方法到2.24就已经用不了了。


没了虚表,想想别的


1.输入buf也可以搞事情

到刚才,我们分析了虚表之前的部分,可是,我们其实是没有一直走到最底层的,因为至少得到read/write系统调用才算是真正进行了输入输出的操作,而这个操作我们并没有看到,那是因为他们都被实现在了虚表里。

现在让我们来分析一下scanf的虚表实现内容吧。这次我们少看点源码,就看看这个underflow:

在调用underflow之前其实会进行一个_IO_read_ptr++的操作,配合上underflow,我想大家都应该能看懂这个的含义吧?

_IO_buf_base, _IO_buf_end, _IO_read_ptr, _IO_read_end 4个变量都是在_IO_FILE的结构体里的,buf_base到buf_end是一个buf,而read_ptr到read_end则比较神奇了,我猜测可能是还没有处理的部分,read_ptr在一开始和buf_base相等,输入之后read_end会指向输入之后的结尾部分,buf_end是不变的,每次输入只能输入buf_end-buf_base个size,而且只有在read_ptr >= read_end,也就是为空的时候才能够读入buf_base。

根据实际测验发现,每一次scanf似乎read_ptr都会加一,其实用到这个结论就可以了。

当然,最主要的地方还是调用read系统调用,写入的位置就在buf_base!于是如果可以更改这个值,就可以利用scanf进行任意写了!

这个手法虽然相对虚表来说限制颇多,但是至少是提供了一个任意写的方案,可以作为扩大控制能力的一种手法,算是一种新的思路。

2.WHCTF 2017 stackoverflow

接下来我们来看一下这种新思路的应用吧。题目来源于WHCTF 2017。

题目有意思的地方就在于他的手法了。只能写入一个NULL的情况是非常受限制的,还是看看分析吧。

1)漏洞位置

①首先是input_name存在一个没有null结尾的输入,于是可以造成泄露,效果是可以泄露出libc,这个是比较简单的地方。

②main_proc中存在一个越界写,当输入size大于0x300000的时候,tmp_size会保存,之后重新输入之后tmp_size没有更新,导致越界写。

2)利用思路

问题1:越界写,且只能写入一个null,看似毫无用处,不过好在可以写入很多个null,于是malloc也可以进行多次,所以第一个任务是要能够写东西到有意义的地方,栈,堆或者libc,通过分配大地址导致堆mmap,我们可以使得分配的内容在libc之前附近的位置,于是通过越界写就可以写入libc了。

问题2:写啥?这个真的是卡了很多人的一个地方,最终的选择,是写了_IO_buf_base,这个题目比较特殊,给出的libc-2.24.so偏移有特殊性,_IO_buf_base比_IO_buf_end小1,而且_IO_buf_end地址的最低位刚好是00,于是向base写入一个00,就可以指向end,之后往end写入malloc_hook的地址,然后循环一下使read_ptr和read_end相等,再次读入,就可以写入malloc_hook了

问题3:如何扩大控制。其实控制了执行流,就比较简单了,我们找了一个read:

这个read是input_name里的,往栈上写入内容,之后就可以进行rop了。

3)exp

这道题目其实就是一个写buf的手法的利用,只要能够想到用写buf的手法其实就很简单了。


总结


1.scanf和printf之类的输入输出函数最终都会调用相应虚函数完成底层操作,2.24之前可以通过更改虚表来控制执行流。

2.底层操作最终通过read等系统调用进行完成,也就是实现在虚表里,被初始化进虚表。

3.对于scanf来说,虚表实现写入的时候会使用到buf,这里的buf会在scanf时候用到,所以可以通过控制buf来达到对libc的一个任意写入,这个方法没有被2.24影响。

4.libc当中值得注意的地方还有很多,应该更多的去深入到源码去寻找这些有意思的东西。


本文转载自安全客

点击“阅读全文”,获得更佳阅读体验