This is the first of a series of posts on a full Android exploitation chain: from browser, to sandbox escape to kernel breaking. This first article only looks at the Qualcomm kernel vulnerability.
The author took a look into the kgsl driver because it is directly accessible from userland. Very few drivers can be reached directly from the apps itself, which is what made this a juicy target. The vulnerability itself lies in the IOCTL_KGSL_GPUOBJ_IMPORT and IOCTL_KGSL_MAP_USER_MEM iotcl, which these calls are used by apps to create shared memory between itself and the kgsl driver.
While handing this iotcl call, there are multiple memory allocation methods that can be done which triggers different logic. By using a type-confusion on the type of memory being used, a failed path on the call can lead to a freeing of this memory that should NOT be freed. This created a use-after-free (UAF) in the kernel.
With the UAF on the sg_table object, any call to DMA_BUF_IOCTL_SYNC will trigger a UAF. This object is quite diverse in its usage, depending on the function called and the values of the table. The functionality of this iotcl is to synchronize the CPU view of the DMA buffer used by the GPU.
With the UAF, the author allocates a similar sized object in the kernel to get control of this memory. But, can this be useful in our vulnerability iotcl? One of the paths in the syncing function uses a list with a specified size. The pointer being referenced is validated for the location and the size is NOT validated. This looks like a semi-useful for a read/write primitive.
With control over the table but NO leak, we need a candidate object to replace the scatterlist field in the sg_table. Using a CodeQL query got them some candidate objects but not anything that would work. To find a proper object to fill this gap, another technique had to be used.
The original bug created a UAF that was not powerful enough. So, by using the original UAF the author created an additional UAF but only on the scatterlist. This was done via some trickery on threads but freed values that should not be freed; these are not bugs in the code but the first UAF made this an issue.
With this threat trickery came what appeared to be a newly impossible to win race condition. Using a
technique of queuing and scheduling made this race condition achievable though!
The Software Input Output Translation Lookaside Buffer (SWIOTLB) or bounce buffer is a not-so-popular section of memory that handles proxied request between the DMA buffer to allow access to it from another device. This buffer is located at a fixed point in memory, depending on some Android kernel settings. This is where the path for the UAF occurred at.
By using forcing SWIOTLB memory to be allocated via some trickery, we can point the DMA buffer to a location that is controlled by the attacker from the UAF. Now, we have the ability to read/write to anywhere we want BUT we do not know WHERE to write. I am a little fuzzy on how this part works to give us the read/write primitive though...
Using heap feng shui, we use the OOB read via the UAF to read memory from a DMA buffer that is controlled. By using patterns in the memory of the FILE structs in binder, we can determine where the proper spots in memory are at.
By setting a FILE struct to our controlled memory location, we can point this to a fake file_operation to run arbitrary functions of our choice. With the kernel though, code execution does not mean game over; these is still another step to take.
By calling the function __bpf_prog_run32 Berkley Packet Filter bytecode can be used for an arbitrary read/write/execute primitive in the kernel. There was some mitigations in place that make privilege escalation harder but they are bypassable (just not done in this post).