一个指针引发的“血案”
脚本引擎开发者在设计GC(Garbage Collect,简称GC)时追踪指针不善导致的UAF(Use-After-Free),是一类常见的漏洞。本文通过一个例子来向读者介绍这类漏洞的成因与分析思路。
漏洞描述
CVE-2018-8353是谷歌的Ivan Fratric发现的一个jscript漏洞,该漏洞在2018年8月被修复。这是一个UAF漏洞,Ivan Fratric在披露页清晰地描述了该漏洞的成因:
通俗一点说就是RegExp类的lastIndex成员没有被加入GC追踪列表,如果给它赋值,在GC时会导致lastIndex处存储的指针变为悬垂指针。后续再访问lastIndex时,即造成一个典型的Use-After-Free场景。
jscript模块目前已发现多个类似漏洞,例如CVE-2017-11793,CVE-2017-11903,CVE-2018-0866,CVE-2018-0935,CVE-2018-8353,CVE-2018-8653,CVE-2018-8389,CVE-2019-1429
本文试图通过CVE-2018-8353一窥这类漏洞的成因,并在此基础上分析谷歌PoC中的信息泄露利用代码。读者将会看到一个GC导致的UAF如何被转化为高质量的信息泄露漏洞。
PoC
以下为Ivan Fratric给出的PoC,下一小节将通过该PoC分析漏洞成因。
UAF
@0Patch团队已通过补丁分析发现,x86下lastIndex位于RegExpObj对象的+A8偏移处,如下:
现在RegExpObj::Create函数内下断点,在RegExpObj对象创建完成后,对其偏移+A8处下一个硬件写入断点,这个偏移处存储一个VAR结构体,此结构体在x86下大小为0x10。重点观察+B0处的数据变化。
为了更清晰地解释成因,笔者并没有开启页堆,但开启了用户模式下堆申请的栈回溯,以下为调试日志:
重占位
到这里已经获得了一个非常好的UAF,接下来的问题是:如何使用它?
从调试日志中可以看出,用来存储VAR变量的内存块是从GcBlockFactory::PblkAlloc申请的,x86下其申请大小固定为0x648(《Garbage Collection Internals of JScript》这篇文章有解释为什么x86下这个大小是0x648):
如果要重用被释放的内存,得在GC后迅速用大小为0x648的内存申请去占用之。如何做到?
一个比较好的方法是借助NameList。jscript对象在创建成员变量时,如果成员变量的名称过长(谷歌的文章中说这个长度阈值为4),会在NameList::FCreateVval函数内单独申请内存,以存储对应的成员变量,并且会以第一个成员名称的长度去申请特定大小的内存,而相关计算公式是固定的。
通过逆向调试,可以得到x86下的计算公式:
现在,令alloc_size=0x648,解上述方程,可得到x=0x178(0n376)。于是可以通过下面的代码重用被释放的内存:
在调试器中观察验证重用:
从UAF到信息泄露
前一小节已经在合适的时机控制了被Free的内存,接下来要哦那个过这个UAF漏洞实现信息泄露,以得到被重用内存的起始地址。
NameList::FCreateVval点
NameList::FCreateVval函数内在申请成员变量名内存时,若成员名长度超过一定值,就会额外申请内存去存储这些名称。第一个成员名可以用来控制申请的内存大小,相关计算过程已经在前面说明。后面的成员名称只要长度合适,就可以在第一个成员名称初始化时申请的内存中使用剩余的部分,从而用来布控内存。
在x86环境下,通过逆向NameList::FCreateVval函数,发现每个成员名称前面会额外留0x30大小的空间作为头部,用于初始化各种数据。每次成员名称进行申请时,还会按照下图的计算公式按4字节对齐并保存与返回相关偏移:
整个计算公式比较复杂,但设计思路很简单,笔者在这里描述一遍,读者大致了解即可:x86下,第一个成员名初始化时,先申请(2x+0x32)*2+4的内存大小,得到内存后,最初的0x30作为头部使用,用来初始化各种数据,包括本次字符串长度,指向下一个成员名头的指针(这个指针会后面的成员名初始化时被更新),然后因为是第一个成员,按照公式直接加4字节进行对齐,所以从前面的调试日志也可以看到,第一个成员名从+0x34开始被复制。只要第一次申请的内存空间够,第二个成员名按照base+offset+4的方式进行内存地址获取,然后前0x30又是头部,接着再开始复制,以此类推。
泄露被重用内存首地址
接下来是泄露被重用内存的首地址。
由于被重用的内容之前存储着lastIndex引用的VAR数据,所以只要用长度及内容合适的字符串设计类成员名称,就可以控制指定地址处的VAR结构。
从这里开始,使用Ivan Fratric在附件中给出的infoleak.html代码,为便于展示,去除了部分注释:
name1用来申请大小为0x648的内存。name2可调节,用来对齐。name3用来指定类型,以泄露特定偏移处的一个指针,这个后面再会提及。name4用来布控0x1337对应的VAR,用于jscript代码中的条件判断。
上面的小节中只关心了name1,现在开始来具体设计name4,name3,name2。
-
锁定偏移值
首先得计算垂悬指针指向的VAR结构在被重用内存的偏移值。Ivan Fratric的适配的是x64的版本,原poc在笔者的环境中运行后0x1337对应的i为十进制的115。
x64与x86的原理一致,以x86的版本进行说明。既然x64环境中对应的i为115。x32环境中,也以115为例进行偏移计算。在上述代码中在第115个RegExpObj对象创建时下断点,相关方法在前面UAF小结已经描述,这个偏移很容易计算得到。
笔者的环境中这个偏移每次固定为0x3d8,如下:
-
设计name
现在来设计name,在每个成员名称初始化时,都会有0x30的头部,在这个头部的+0x24处是一个指针(这个指针要到初始化下一个成员名时才会被初始化),指向下一个变量名的0x30头部,下图中字体为红色的即为这些指针。如果能读取其中一个指针,减去其相对内存起始地址的偏移,就可以得到被重用内存的首地址。
下图中字体颜色为橙黄的是被拷贝的成员名称,每个名称最后会多拷贝两个0x00。字体颜色为蓝色的是每个成员名称的实际长度(转化为unicode后的长度)。字体颜色为红色上面已经进行解释。字体背景为灰色的一个个0x30内存区域为name2、name3、name4三个成员名的头部。
字体背景为黄色高亮的区域,实验时发现会与name3的值相同(意思就是给3得3,给5得5)。后面需要借助这个值来读取它后面偏移8字节的一个红色指针。
-
最后一个注意点
因为要泄露某个红色指针,所以x86下必须保证这个红色指针之前8字节处的type为long型,这可以通过设计name3来实现。现在的问题是:VAR与某个特定的lastIndex对应起来?
幸运的是,通过调试观察发现,当连续申请VAR结构时,一个个大小为0x10的VAR似乎是从高内存往低内存次第排列。笔者用下图来通俗地解释一下VAR的分布(name2中b的数量被用来调节这里的对齐):
所以,在x86下,如果找到了0x1337对应的regexps[i].lastIndex,就可以通过读取regexps[i+5].lastIndex来泄露相关指针,减去固定偏移就得到被重用内存的起始地址了。如下:
到这里已经将这个UAF漏洞转为了信息泄露,泄露出一块(aaa...部分)完全可控的内存的首地址。如果读者之前看过笔者之前的一篇文章,就会明白这里已经将CVE-2018-8353转换为和CVE-2017-11906具有相同功能的信息泄露漏洞。
从信息泄露到RCE
此类信息泄露漏洞与其他堆溢出漏洞一起使用可以实现远程代码执行。笔者将这个漏洞的利用代码稍加改动,并配合CVE-2017-11907一起使用,可以在未打补丁的机器上完成概念验证。
考虑到CVE-2018-8653或CVE-2019-1429这类在野0day的利用方式,应该是用了更高级的利用手法,通过UAF直接实现了任意地址读写,通过单个UAF即可实现远程代码执行,并不需要其他漏洞进行辅助。
此类UAF漏洞后面一定还会出现,请大家做好防范工作。
参考文章
Issue 1587: Windows: use-after-free in JScript in RegExp.lastIndex
Garbage Collection Internals of JScript