This vulnerability was used as a challenge in the Real World CTF at the beginning of 2021. The challenge came with a hint that sent people to the SCSI driver.
The SCSI device implements a simple state machine with a global heap allocated buffer. When initiating the state machine, we can set the buffer size and the state machine will set a global buffer pointer to point to the start of said buffer. From there on, we can either read one or more bytes, or write one or more bytes. Every read/write operation will advance the buffer pointer. This means that after reading a byte from the buffer, we can’t write that same byte and vice versa, because the buffer pointer has already been advanced.
This driver has some simple operations. Mainly, read/write a byte and read/write a string. The driver has logic in place to ensure that this global heap buffer is not written out of, but the logic is flawed.
There is a sanity check to ensure that no operation exceeds the maximum size (read/writes). However, this is the wrong check. Instead, it should validate from what is left! An example is that there are 40 bytes in the buffer. If we are at offset 30, it should validate that we do not read/write more than 10 bytes (for a total of 40).
A repercussion of this bad input validation is an integer underflow on the cbBufLeft(size left in buffer). Because this wraps around to a very large number (when it overflows), this creates a buffer overflows on read/write operations after this point. With this overflow, we have a linear heap write/read.
In order to exploit this, a special driver was made. To make their lives easier, the driver just acted as a way to funnel data from userland.
With this primitive, it was time to get all of the leaks necessary for code execution. The first order of business was to find an object that was suitable for exploitation. The author was looking for a pointer to a DLL, heap addresses and function pointers; the HGCMMsgCall was used to spray this.
This works well for the code leak from a function pointer in a VTable to break ASLR for the object. The heap leak required viewing two objects within a linked list in memory in order to understand their relative location in the heap.
To gain control over the RIP, we just alter the VTable pointer that we used earlier for the code leak. However, code execution and RIP control are two different things.
JOP (jump oriented programming) is the only option because there is no control over stack registers. This requires good register control, in most cases. Here, the authors of this exploit only had control over the r8 register.
With control over the r8 register, a JOP chain was used in order to get control over the RSP register to control the flow of execution for future iterations. Eventually, this led to a ROP chain that leaked an address of kernel.dll in order to call WinExec().
This VM escape was pretty amazing! It goes into many common issues with Windows exploitation and how to get around them.