关于CVE-2017-8291利用的几点思考

2019-06-14 约 85 字 预计阅读 1 分钟

声明:本文 【关于CVE-2017-8291利用的几点思考】 由作者 xiao_c 于 2019-06-15 06:01:00 首发 先知社区 曾经 浏览数 1 次

感谢 xiao_c 的辛苦付出!

背景

最近看到的这个两年前的洞,仔细看了下觉得很有意思。有意思的地方在于这个类型混淆漏洞,虽然需要用到控制堆栈等手段,但是不需要去构造rop链,也不需要考虑alsr之类的麻烦问题,即使之前只学过web也能看得懂这个洞与利用方式。另外它最后的实现的效果是真实的rce,不是c++中溢出那种存在rce的风险但是又有一堆问题需要解决。

参考

分析文章参考
https://paper.seebug.org/310/
poc参考
https://raw.githubusercontent.com/rapid7/metasploit-framework/master/data/exploits/CVE-2017-8291/msf.eps
利用场景参考
http://blog.neargle.com/2017/09/28/Exploiting-Python-PIL-Module-Command-Execution-Vulnerability/

光靠seebug的分析文章还不够,显然有些地方作者明白但是没有写出来,建议先稍微了解一下postscript语法,之后对着poc,用gdb调试来加深理解。poc总共不到100行,理解了poc基本上也就理解这个漏洞的原理与利用方式了。

postscript和ghostscript

poc脚本语法是postscript(ps)+ ghostscript(gs)指令,ps主要通过操作数栈(os)来进行操作,用栈的思想来理解所有操作指令就很简单了。栈顶记作osp,栈底记作osbot。
ps基本语法:

操作数1 ... 操作数n 指令

gs是一个ps的解释器,gs解释ps指令时,从左到右操作数依次入栈,遇到指令从栈上取相应数量的操作数,poc涉及到的指令包括
def、sub、exch、get、put、for、aload等,具体用法参考ps语法。

poc分析

按照poc执行顺序解释,忽略注释。

变量定义,从名字可以看出是和大小大小有关系,并且from、step、to很明显是for循环控制的3个参数,enlarge是后面用来扩充os大小的单元数。
ps:分析完整个脚本后再回过头看这个定义也觉得很有意思,为什么选500、10000、65000、1000这几个数呢,可以思考一下。

for循环执行111次,
定义buffercount = 111
定义buffersizes[111]

定义buffersizes[0] = 10000, buffersizes[1] = 10500 ... buffersizes[110] = 65000
定义buffers[111]

定义buffers[0]=string:10000, buffers[1]=string:10500 ... buffers[110]=string:65000,且每个字符串最后16位为全f。

这里稍微麻烦一点,涉及到漏洞利用的几个关键点。从loop循环整体来看,loop循环之前有3条指令,loop循环里套了repeat循环、两个if分支,按前后顺序记作if1和if2。
根据之前的分析,单纯看loop循环,结束条件是进入if2分支。进入if2分支的条件是buffersearchvars[2]的值为1,buffersearchvars[2]的值只在if1分支中改变,所以需要进入if1分支。进入if1分支的条件是254 le表达式为真,表达式之前的操作取的是buffers[i][-16],根据之前的分析buffers[i][-16]是0xff,显然不可能进入这个条件分支,但无论经过调试还是运行确定这不是个无限循环,问题在哪呢。
首先第一个关键点是aload,enlarge根据之前的定义是1000,这里一下子入栈了1001个单位,貌似这样超出了此时os大小的限制,所以系统gs重新给os分配内存,分配到哪去了呢,通过下图一目了然。

buffers中有111个字符串,从索引0开始计数,在我本地调试时,新的os始终是在第8个string到第9个string之间,通过gdb调试也能看到,下图是111个字符串从buffer[0]开始的部分截图,此外还可以看到此时的os和111个字符串存储区是不重合的,虽然位置很接近(0x1e5b048和0x1e8f9f4)。

当执行完aload后再看os的位置,已经跑到字符串存储区中了。

第2个关键点也是修复的地方,.eqproc。这个可能是gs的命令,我没有找到它的文档说明。作用是从os上取两个操作数比较,结果布尔值存入os,即取2存1。这也没啥问题,addsub指令也是这么干的。它和其他取2存1的指令不同的地方在于当os上操作数不够,比如os中只有1个操作数,它不做边界检查,仍然取2存1,这就是溢出了。
结合以上两点,可以想象,只要循环执行.eqproc,必然会重写111个字符串中的1个字符串的最后16字节(为什么,思考一下),这就是loop循环能停下的原因,整个过程简单用下图表示(不考虑指针指向的地址了,自己脑补)

检测到溢出发生后,记录下溢出位置,也就是buffersearchvars数组的作用。


这段代码起初我不太理解。首先3次.eqproc和最后的put很明显是为了去满足某种格式要求在做堆栈平衡,直到我简单画了图模拟了栈上变换就清楚了。

至于之后的

16#7e put
16#12 put
16#ff put

则是通过currentdevice与osp此时重合的特点,利用string操作osp指向的currentdevice。这一段是在修改currentdevice的类型信息,也就是为什么这个漏洞是由于类型混淆引起的。

上面是我观察栈中数据的gdb截图,每一行16个字节是一个栈对象。第2行表示的是1个字符串,低8位字节(左边)表示类型信息,高8位表示字符串内容存储的起始地址。低8位字节中,低2位0x127e表示类型是字符串,0xf710没有意义,1f表示大小。所以currentdevice后面的三条指令,通过字符串来操作osp指向的数据,把它从currentdevice本来的类型改成了字符串类型,大小255。而最后的put又把这些数据清出os,把改变后的currentdevice存储到sdevcie[0]中。
之后重新执行aload,但是回到的是loop循环开始前下面3-1=2个单元的位置,为什么要这样据说是因为避免gc是崩溃,很有道理,但是为啥不直接回到原位置,这点我也不理解。
由于之前把sdevcie[0]即currentdevice改成了字符串类型,所以这里可以用put来改变它的内容,此处分别把偏移0x3e8、0x3b0、0x3f0置0,偏移是相对os上高8位那个内容指向的地址而言,即分析文章中说的把LockSafetyParams安全属性从true变成false。这里还有个问题没搞清楚,属性只有1个,但是这里改变了3处偏移。

最后的这段脚本代码就是在控制台上能看到的echo vulnerable,这一段很多带点的指令我都找不到文档说明,但是到这里基本上也就结束了。

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now