• 本文作者: 天融信安全应急响应中心
  • |
  • 2020年6月3日
  • |
  • CVE
  • |

从CVE-2018-4441看jsc的OOB利用

作者最近研究了safari浏览器JavascriptCore引擎的一个OOB漏洞CVE-2018-4441,虽然这是一个比较老的漏洞,但是研究这个漏洞还是能学到不少东西。这里介绍了jsc环境搭建的方法和jsc一些基本调试技巧,详细分析了CVE-2018-4441的漏洞成因和lokihardt堆喷修改数组长度构成OOB的方法,希望读者读完能有所收获。

环境搭建

下载源码

​ 下载源码使用

​ 如下载的源码较旧需更新源码到最新日期则使用

​ 切换到包含漏洞的commit_hash,这里我没有找到很好的方法,我使用的方法是搜索CVE-2018-4441的修复日期fix_date,然后从webkit的github上搜索小于fix_date的commit,即committer-date:<fix_date,最后使用的是21687be235d506b9712e83c1e6d8e0231cc9adfd。切换的命令如下

命令的格式为git checkout -b {local_name} {commit_hash}。

编译安装

​ 安装依赖项

这里如果是在linux下使用时提示缺少pipenv包需要注释掉install-dependencies中函数installDependenciesWithApt里边的pipenv包。

​ 编译

调试

js断点

​ 这里介绍两个使jsc在我们编写的js代码里断下来的技巧(即类似V8的%SystemBreak())。

方法一

​ 在编写的js代码里定义断点函数

在调试器里设置arrayProtoFuncSlice的断点即”b arrayProtoFuncSlice”。这样在js代码里调用b()调试器就会断在这里了。

​ 这个方法的缺点是如果调试的漏洞会调用到arrayProtoFuncSlice的话可能会对漏洞分析调试产生影响。

方法二

​ 修改jsc的源码添加如下辅助函数

重新编译jsc代码,在js代码里定义如下断点函数

​ 这样在js代码里调用函数b()时调试器就会断在这里了。

对象调试

​ jsc中也有一些类似v8的%DebugPrint()的辅助调试输出函数,定义在JavaScriptCore/jsc.cpp里。jsc中输出对象的方法如下

漏洞分析

POC

​ poc中首先定义了一个CopyOnWriteArrayWithInt32的数组arr,

其中jsc的数组存储规则定义在/Source/JavaScriptCore/runtime/ArrayConventions.h里。elements的存储定义如下,

此时arr的元素下标小于MIN_SPARSE_ARRAY_INDEX(即100000U)会存储在butterfly的storage vector里,butterfly(0x7fe0000e4010)

​ poc中之后修改了arr的长度为0×100000,此时下标大于MIN_SPARSE_ARRAY_INDEX(100000U)数组类型变为ArrayWithArrayStorage

此时jsc开辟了新的ArrayStorage并把butterfly指向新的ArrayStorage。butterfly(0x7fe0000fe6e8)

​ 在执行arr.splice(0, 0×11),移除0×11个元素后butterfly变为

​ poc中重新设置arr的长度arr.length = 0xfffffff0,此时butterfly变为

​ 继续调用arr.splice(0xfffffff0, 0, 1)添加元素时发现jsc运行崩溃

poc_crash

崩溃时写的地址为0x7ff0000fe6e8+0xfffffff0*8+0×10=0x7FF8000FE678,0x7FF8000FE678不可写导致崩溃

漏洞根源分析

​ poc崩溃时栈回溯如下

我分析这个漏洞根本原因的方法是先从ECMAScript查了下Array.prototype.splice方法的实现,然后从崩溃的开始JSC::arrayProtoFuncSplice函数分析。

​ JSC::arrayProtoFuncSplice的大致逻辑是找到splice调用时的数组起点actualstart并根据参数个数来对数组进行删除元素或添加元素,删除或添加元素使用的是shift或unshift。

​ poc中第一次调用arr.splice(0, 0×11)删除元素时使用的是shift,并最终由于arr类型为ArrayWithArrayStorage调用到shiftCountWithArrayStorage。

在shiftCountWithArrayStorage进行了一些列判断来决定array是否使用ArrayPrototype中的方法处理splice调用中的删除元素操作,若这一系列判断条件全部为假则执行storage->m_numValuesInVector -= count对splice调用中的数组storage->vectorLength赋值的操作,实际上这一系列的判断是存在缺陷的,漏洞的根源也就出在这里。产生漏洞的原因即判断条件全部为假时m_numValuesInVector 和array.length我们可控,在随后的分析中我们可以看到这两个值可控会导致添加元素调用unshiftCountWithArrayStorage时实际storage->hasHoles()为真的数组返回为假,在memmove初始化新的storage时导致OOB。

​ shiftCountWithArrayStorage中首先判断了hasHoles,jsc中storage->hasHoles()实际上判断的是*(dword*)(&butterfly+0xc)==*(dword*)(&butterfly-0×4),即storageLength==vectorLength,hasHoles

此时由于m_numValuesInVector!=storage->length,hasHoles为真。butterfly(0x7ff0000fe6e8)

然后继续判断会调用到holesMustForwardToPrototype

holesMustForwardToPrototype中主要是遍历了array的原型链并判断了hasIndexedProperties和mayInterceptIndexedAccesses属性,如果这两个属性都为假会返回false。

​ 回到shiftCountWithArrayStorage的3个判断,即

按照lokihardt的说法由于poc中的arr在原型链中不含索引访问和proxy对象,第一个&&的判断中holesMustForwardToPrototype会为假。其余两个判断也为假。这样就导致shiftCountWithArrayStorage执行到如下代码

poc中的arr->m_numValuesInVector = 1,这样删除0×11个元素后1-0×11=0xFFFFFFFFFFFFFFF0,保存时取低4字节为0xfffffff0。

​ poc中执行到arr.splice(0xfffffff0, 0, 1)添加元素时使用的是unshift,并最终由于arr类型为ArrayWithArrayStorage调用到unshiftCountWithArrayStorage。

在unshiftCountWithArrayStorage中首先判断了arr的storage是否hasHoles,如果hasHoles为真则使用ArrayPrototype的其他方法去处理splice调用时删除或添加的元素。

​ 由于poc中我们修改了arr的length为0xFFFFFFF0,又由于第一次调用splice方法删除元素时在shiftCountWithArrayStorage中不正确地更新了m_numValuesInVector,此时length=m_numValuesInVector=0xFFFFFFF0,storage->hasHoles()返回为假jsc继续使用unshiftCountWithArrayStorage的方法处理splice调用中添加的元素。此时butterfly(0x7ff0000fe6e8)如下,*(dword*)(&butterfly+0xc)==*(dword*)(&butterfly-0×4)

​ unshiftCountWithArrayStorage随后设置了storage的gc状态为推迟,然后重新设置了array->storage。随后的漏洞利用分析中可以看到这里调用memmove处理新的storage就是导致OOB的根本原因。

​ 总结一下漏洞的逻辑:poc中的arr第一次调用splice删除元素时会调用到shiftCountWithArrayStorage,在shiftCountWithArrayStorage会遍历arr的原型链并存在可能使得原型链判断返回假,导致在shiftCountWithArrayStorage中arr的m_numValuesInVector 被不恰当地更新(本不该执行到这里);在第二次调用splice添加元素时调用到unshiftCountWithArrayStorage,如果设置arr.length=m_numValuesInVector 导致arr->hasHoles判断为假,进而在unshiftCountWithArrayStorage中使用memmove更新storage时导致OOB。

patch分析

​ patch地址https://github.com/WebKit/webkit/commit/51a62eb53815863a1bd2dd946d12f383e8695db0

​ patch中去掉了shiftCountWithArrayStorage中遍历原型链的判断,且不管array是否storage->hasHoles()都使用memmove去更新storage。这样在调用splice删除元素时只要数组vectorLength!=storageLength即hasHoles为真都会使用ArrayPrototype中的方法去处理,不会更新m_numValuesInVector ,这样这个漏洞就从根源上被修复了。

patch

​ 但是这里我没有想明白的一点是为什么修复漏洞之前还要多此一举的调用holesMustForwardToPrototype判断array的原型链,既然不判断既没有漏洞又省去了一次执行判断原型链的时间;)?

漏洞利用

​ exp来自那个男人,即lokihardt。

​ 首先整理一下通过这个漏洞我们可控的东西:splice在删除元素时不正确更新的vectorLength、array数组的长度storageLength,在调用splice添加元素时如果vectorLength=storageLength即storage->hasHoles为真会执行unshiftCountWithArrayStorage中更新storage的流程,并且更新storage的流程memmove时似乎存在利用的可能。

​ lokihardt的利用思路是通过堆喷利用unshiftCountWithArrayStorage更新storage时memmove修改数组长度构成OOB进而构造addrof、fakeobj原语,构造ArrayWithArrayStorage类型的fakeobj记hax并使hax的butterfly指向ArrayWithDouble类型的victim,通过修改hax[1]即victim.butterfly为addr和victim.prop完成任意地址读写,通过任意地址读写修改wasm模块rwx的内存区来执行shellcode。

heap spray

​ lokihardt堆喷的数组spray[i]为ArrayWithDouble,spray[i+1]为ArrayWithContiguous,且spray[i]和spray[i+1]均为10个元素,这里堆喷的数组元素类型和个数都是固定的。

​ 首先解释下元素的类型,这里的元素类型是为了方便利用修改长度后的堆喷数组构造fakeobj和addrof原语,测试如下代码

​ a1为ArrayWithDouble类型,jsc中存储如下

可以看到a1即ArrayWithDouble的元素在butterfly的storage中直接存储。

​ a2为ArrayWithContiguous类型,jsc中存储如下

a2中的元素{}在butterfly中以类似object的形式存储,即butterfly中存储的是指向{}内存区的指针,指针指向a2的真正内容。即a2.butterfly->*p->content。

​ 再看一遍lokihardt堆喷的数组,spray[i]为ArrayWithDouble,butterfly:0x7fe00028c078;spray[i+1]为ArrayWithContiguous,butterfly:0x7fe00028c0e8。

构造addrof:spray[i+1][0]=obj,jsc会在0x7fe00028c0e8的位置保存obj的地址指针,在**0x7fe00028c0e8的位置保存obj的内容,这样我们通过读spray[i][14]的内容即可实现读对象的地址。

构造fakeobj:spray[i][14]=addr,此时spray[i+1][0]的位置即为addr,由于spray[i+1]为ArrayWithContiguous类型即spray[i+1][x]中保存的是类似obj的对象,这样spray[i+1][0]即为我们构造的fakeobj对象。

​ 再解释下堆喷的数组元素个数是10个。要理解堆喷元素个数首先要理解的一点是lokihardt利用的思路,如果我们可以修改堆喷的数组长度使spray[i]可以访问到spray[i+1][xx]就可以构造fakeobj和addrof原语,而正常情况下不修改数组长度spray[i]肯定是不能访问到spray[i+1]的,那么如何修改堆喷数组的长度呢?可能的思路有两个:1.堆喷后手动触发GC调用splice添加元素使调用splice时新添加的storage的butterfly正好落在spray[i]里(即在spray[i]处伪造一个butterfly并修改spray[i]的length),但是这个方法明显的缺陷就是触发GC的时机和新的butterfly太难控制了,控制不当jsc肯定会崩溃;2.调试发现exp中splice添加元素的过程会触发创建新的butterfly的操作,新创建的butterfly会落在最后一个堆喷数组的后面(spray[0x3000].butterfly的后面),配合unshiftCountWithArrayStorage中的memmove可以达到修改堆喷数组长度的效果,这也是这个漏洞为什么会被描述为OOB的根本原因(难道这就是那个男人强大的力量吗;p)。

​ 第一次arr.splice(0, 0×11)删除元素时arr的存储

​ 堆喷后调用arr.splice(0×1000,0,1)添加元素,unshiftCountWithArrayStorage处理exp中的arr时会调用到unshiftCountSlowCase,并在tryCreateUninitialized中创建新的storage,大小为88=0×58allocate

字节对齐后为0×50,为了防止随后的memmove移动内存过程中破坏内存,堆喷的数组元素个数申请了10个。

​ unshiftCountWithArrayStorage在创建完新的storage后会初始化新的storage,即memmove的过程,exp中会执行到以下流程memmove

这里dst=0x7ff000287a78即arr新的butterfly+0×10的位置,src=0x7ff000287a80,n=0×8000即将0x7ff000287a80开始0×8000的内存整体前移8字节,这里会使堆喷数组中某个spray[i][0]的元素覆盖到*(dword*)(&spray[i]-8)的位置,即0×1337覆盖到spray[i]的length域

​ 被覆盖前的堆喷数组

​ 被覆盖后的堆喷数组

​ 到这里我们就可以控制一个可以越界访问的ArrayWithDouble类型数组spray[i]了,通过搜索内存找到length不为0xa的堆喷数组进而可以构造addrof和fakeobj原语。

arbitrary code execute

​ lokihardt构造任意地址读写原语的思路是构造一个ArrayWithDouble的数组victim,利用漏洞版本jsc相同数据类型structureID并不会随机化并根据i32和f64在内存中存储位置相同构造fake structureID,构造ArrayWithArrayStorage类型的fakeobj记为hax使hax的butterfly指向victim,通过修改hax[1]即victim的butterfly为addr同时修改victim的prop实现任意地址读写。

fake structureID

​ 构造victim

victim = [1.1]此时构造的victim的类型为CopyOnWriteArrayWithDouble,victim[0] =3.3重新分配butterfly并修改victim类型为ArrayWithDouble。jsc中这两种类型并不一样。ArrayWithDouble的victim存储如下,可以看到prop存储在*(dword*)(butterfly-0×10)的位置。

​ 构造fakeobj

​ 需要注意在jsc中构造fakeobj时需要绕过structureID,structureID相同的才具有相同methodTable并被jsc视为相同类型。漏洞版本的jsc并不会在每次启动时随机化相同数据类型的structureID,这里lokihardt把structureID初始化为了0×64即arr的ArrayWithArrayStorage类型。这里fakeobj的类型是固定的,构造ArrayWithArrayStorage类型hax的原因是ArrayWithArrayStorage的数据直接存储在butterfly里,我们可以访问到的hax[1]即为victim的butterfly。

​ 漏洞版本的jsc在解析如下代码时保存i32和f64内容的位置实际上是相同的(这里我是调试发现的,可能是因为WastefulTypedArray类型?下文有jsc中WastefulTypedArray类型的存储方式解释)。

存储结构

这里i32和f64的butterfly存储的都不是它们的实际内容,实际存储i32和f64内容的位置位于*(dword*)(i32+0×10)即0x00007fe8000ff000里

而且经过调试可以发现container中保存exp中jscell位置的值比i32中高8位的值大0×10000,所以exp中i32高8位-0×10000。

​ 关于i32和f64使用相同内存存储,在JSArrayBufferView.h中有解释WastefulTypedArray类型的存储,WastefulTypedArray类型的butterfly并不包含vector。

arbitrary read/write

​ 这样构造的container如下

即*(dword*)(container+0×10)的位置为伪造的ArrayWithArrayStorage类型数组,fakeobj(container+0×10)构造butterfly为victim的fakeobj记hax。

​ 这时内存的存储结构为hax.butterfly->victim,其中ArrayWithArrayStorage类型的数据直接存放在butterfly里,hax的butterfly可以通过hax[1]访问修改,victim.prop也可以修改,由于ArrayWithDouble类型数据的prop存放在*(dword*)(butterfly-0×10)的位置,我们修改hax.butterfly为addr+0×10即可实现addr处的任意地址读写。

这里的addrof和fakeobj的作用实际上是读写相应位置的数和进制转换。

​ 有了任意地址读写的原语我们就可以通过覆盖wasm的rwx内存执行shellcode。

这里的wasm_code作用是调用wasm模块生成一个用于保存机器码的rwx的页,内容并不重要。js引擎实现wasm的方法一般是先用汇编初始化wasm模块,然后跳转到rwx的页面执行真正用户调用的内容;js引擎在执行用户调用的wasm时需要找到保存这段字节码的页面,rwx的页面地址会或隐式或显示地保存在内存里,我们只需要调试找到rwx页面的地址并覆盖其内容即可。

完整exp

​ 这里的exp较lokihardt的原版有修改,去掉了lokihardt利用unboxed2和boxed2指向相同内存构造第二个fakeobj和addrof原语的部分(作者认为这一部分或许是lokihardt为了显示OOB这类漏洞的另一种通用构造fakeobj、addrof原语的方法,但是并不是必要的,去掉更容易理解而且并不影响exp的稳定性)

lokihardt的原exp:

https://github.com/rtfingc/cve-repo/blob/master/0×05-lokihardt-webkit-cve-2018-4441-shiftCountWithArrayStorage/exp.js

pwned

参考链接

saelo的jsc利用知识

http://phrack.org/papers/attacking_javascript_engines.html

js引擎的shape和inline cache

https://mathiasbynens.be/notes/shapes-ics

lokihardt提交的漏洞

https://bugs.chromium.org/p/project-zero/issues/detail?id=1685

lokihardt的exp

https://github.com/rtfingc/cve-repo/blob/master/0×05-lokihardt-webkit-cve-2018-4441-shiftCountWithArrayStorage/exp.js

Written by 天融信安全应急响应中心