背景

最近看到的这个两年前的洞,仔细看了下觉得很有意思。有意思的地方在于这个类型混淆漏洞,虽然需要用到控制堆栈等手段,但是不需要去构造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,这一段很多带点的指令我都找不到文档说明,但是到这里基本上也就结束了。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖