首先祝大家新年快乐,最近做了一道pwn题,挺有意思的,是利用协程切换时临界区控制不当而导致的UAF,这题做了我很久,两天多。(可能是因为我菜)所以感觉很有收获,写这个不仅是分享,也是把我自己做题的心路历程记录下来,总结经验。
协程,简单的说,就是用户态的,程序自己所控制的线程。我们知道,线程的管理与调度,一般是操作系统所控制的,但是协程,是用户自己所控制的。在操作系统看来,无论你建立了多少个协程,都只把它看作是一个线程。
一个linux帮我们定义好的结构体,记录了一个协程的状态,比如寄存器信息,等等。。。
linux协程的具体内容我不会细讲了。。毕竟这是一道pwn的writeup不是linux协程开发教程。。。上面只是大概地说了一下。。。详细内容可以看看 linux的man http://man7.org 或者https://segmentfault.com/p/1210000009166339/read#2-1-_getcontext_u5B9E_u73B0
这道pwn是开源的,以下是源代码
题目提供了一个crc32计算服务,我们可以请求crc32计算,添加协程,切换到协程,暂停协程,收集计算结果并显示。这个服务使用的是非抢占式的协程切换,需要yield主动切换到其他协程。如果对线程切换原理有过了解,看这个代码应该不难。。如果没有,呃,好好学习大学的操作系统课程再来打CTF。。。!(逃
这个漏洞点可真是难找,我当时对着代码看了好久都没发现任何显而易见问题(溢出或者格式化字符串漏洞或者UAF Double Free什么的)。找了好久,终于发现,本应该是临界区的pop操作,被分开了!
注释是我自己加的,意思是,这个yield可以被利用,如果操作得当,*stack可以被设置为一个dangling pointer。
但是又有一个问题,他每次切换回来的时候,都会检查*stack和old_head是否相等,如果不等,那么会重新加载一次new_head和old_head并且再次yield。这给利用造成了一些困难。不过,我们可以想想堆运行原理:被free后的内存会被insert到fastbin中,再malloc的话会直接从fastbin里面取,这样会导致内存地址是一样的。如果对堆的运行机制不了解,可以看看这篇文章https://jaq.alibaba.com/community/art/show?articleid=315。
这样我们就有利用的思路了。
接下来就是想,该怎么利用这个UAF了。
与劫持C++虚表的UAF利用的套路不同,这个不是C++程序,所以利用只有另求方法。
这个时候job_stack的值在fastbin中,但是我们对node没有直接写入的权限,不能像一些套路一样,改写fd的值,使malloc返回自定义的地址。如果我们再分配一个128字节的job,新的job node和旧的job node会指向同一个地址。所以栈这个单向链表会形成一个环,想了一想,发现不好利用。
那么,既然job数据的大小是可控的,那么我们为什么不能让这个分配到一个0x10的fastbin呢?我们来看看malloc的顺序:
可见,是先malloc输入的缓冲区,再malloc存结果的缓冲区,最后push里面再malloc node的缓冲区。
此时,0x10的fastbin中有两个chunk,如果size < 8的话,input会拿第一个,而result就拿第二个。而result,是存放计算crc32结果的地方,同时也是*job_stack此时的值!既然是存放我们输入数据的crc32计算结果的地方,我们相当于可以控制他的值!那么,如果我们构造一个4字节的payload,使得crc32的计算结果是我们某个可控的chunk的地址(比方说,第一个job内容的地址),便可以伪造一个job struct,其中result指向某个地址,input指向crc32是这个地址的payload。这个时候再算这个job,便可以实现任意地址DWORD SHOOT!
如图所示:
具体步骤为:(接上面的)
现在,我们有枪了,但是还没有确定我们要射的目标。DWORD SHOOT,射在哪里,射什么,是一门艺术。当时我想到了几个方案:
本来想通过一些gadget实现获取got表free地址并计算出system地址然后call的,然而这题ALU相关的gadget真是少的可怜。。。好吧,你算不出来,我帮你算。即,先调用puts (printf占用的栈空间实在太大,要0x2000多。。。实在是坑。。。),然后脚本来算出system地址,scanf把他存到ROP的后面的某个位置上。(本来我是通过fread的,其中FILE*直接给的就是__bss_start的地址,然而这样不行。。因为fread所需要的是__bss_start里面的内容,不是他的地址。。。)
下面就是ROP的具体内容
+0x00:
+0x10 ebp
0x8048520 puts addr
0x8048618 leave/ret addr
0x804A5E8 got addr of free
+0x10:
+0x24 ebp
0x8048590 scanf addr
0x8048618 leave/ret addr
"%x" addr
+0x28 ; address to be modified to system address
+0x24:
0 ;ebp
+0x28:
0 ;to be modified to address of system
0
+0x34 "/bin/sh" addr
+0x34:
"/bin/sh"
+0x3c\:
"%x\x00\x00"
+0x40:
很明显,执行到leave的时候,会把esp设为+0x00处地址,然后pop ebp,可以接着继续控制ROP。这是一个ROP技巧,只要能通过这种方式控制ebp,就可以一直通过将返回地址设为leave/ret,实现几个函数的连续调用(注意,根据调用约定,任何libc的函数都不会改变ebp的值)。
所以最后一步:(接着上面的)
还有一点,堆分配出来的地址,在一定情况下,是确定的。即,只要确定堆基址,然后堆分配操作顺序一定,出来的相对堆基址的偏移必然一样。
不过我在调试的时候,出现了问题:就是当我直接命令行运行程序或者用gdb调试,与用pwntools运行程序,堆分配的偏移会不一样。但是在服务器上,是一样的。不知道是什么原因,如果有大神知道,可以一起讨论。
好了,接着,我们用gdb调试,获取到主协程ucontext_t,第一个job和第二个job输入的地址,然后记录下worker1和worker2的地址,然后readelf找到system和free的偏移,就可以上手写exp了。
顺便说一下,这个题目是给一个shell,但是权限不够cat flag,所以要通过get这个程序的shell拿到flag。因为这个原因,我们可以用这个shell,下载并开启gdb peda,获取到以上的信息,写入exp
exp如下:
顺便提一下,在服务器的shell中直接python运行这个脚本的话,from pwn import *这里会卡死,不知道为什么,有大神知道的话,可以讨论一下。所以我说先python打开交互式界面,然后手动import我的exp,就可以getshell了。。。
其中,crack_crc32是用来爆破crc32的程序,因为是爆破,所以exp的运行时间可能会有些长。代码如下:
哈哈,我其实是在这个训练平台上第一个做出来这道题的,可以。最后,再次祝大家苟年大吉,万事如意,新的一年挖到更多0day!
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2018-2-17 08:48
被holing编辑
,原因: 修改错误,补充内容