We discovered this vulnerability in the Avast Virtualization driver (aswSnx.sys) that handles some of the ‘Sandbox’ and ‘DeepScreen’ functionality of all the Avast Windows products. We initially found this issue in versions 10.x (10.4.2233.1305) of those products and later confirmed that the latest 11.x versions were still affected by this issue up to, and including v11.1.2245. Upon successful exploitation of this flaw, a local attacker can elevate privileges from any account type (guest included) and execute code as SYSTEM, thus completely compromising the affected host.
- Avast Internet Security v11.1.2245
- Avast Pro Antivirus v11.1.2245
- Avast Premier v11.1.2245
- Avast Free Antivirus v11.1.2245
Earlier versions of the aforementioned products are also affected.
The Avast virtualization kernel mode driver (aswSnx.sys) does not validate the length of absolute Unicode file paths in some of the IOCTL requests that receives from userland, which are later copied on fixed length paged pool memory allocations. This allows exploit code to overflow the associated kernel pagedpool allocated chunk and corrupt an adjacent kernel object that the attacker controls (Figure 1).
In the following figure we can see a call to nt!memmove performed by the aswSnx.sys driver without validating the size of the data to be copied against the available size in the allocated pagedpool chunk. This information was taken from version 10.x, but you can easily locate the same call in version 11.x of the driver.
Exploiting Heap Overflows
As with most cases dealing with dynamic memory allocation based buffers, also known as heap overflows, we firstly need to be able to predict where the allocation will occur so that we can take control of the execution flow as reliably as possible. This is even more important when we exploit bugs in code running in the kernel address space, as usually if the exploit fails then the whole system goes down with it. Corrupting a random kernel object that you don’t control, is indeed a really bad idea.
In order to achieve this, we need to overcome another challenge which is to create a desirable layout of dynamic memory allocations based on the size of the chunk that we can overflow. If we can control the size of that chunk, then it is easier to achieve this since we don’t have to limit ourselves to a much smaller subset of objects. However, when we deal with fixed-size chunks (0x418 bytes in this case) it can be very challenging to find a suitable object of that size in order to spray the heap reliably. Finding a kernel object that could fit this requirement was a bit tricky, but thanks to this article by ‘j00ru’, I managed to get the one I needed.
Spraying the Kernel Paged Pool
Private Namespaces are indeed a useful way of creating paged pool objects of which we can control the size and this fact makes them ideal for exploiting this bug.
By creating multiple private namespaces with boundary descriptor names that have a well-crafted length, we can achieve the following memory layout:
So in this case, we can’t control the size of the paged pool chunk that we can overflow, but we can control the size of a paged pool object to a certain extent. What is shown in Figure 3 actually refers to just one memory page (of size 4kb) in order to demonstrate what the paged pool starts to look like.
As you can see, we have one object that we control at the beginning of the memory page, then we have some free space of size 0x3b8 bytes, and finally two contiguous objects that we control until the end of the memory page. By crafting boundary descriptor names with variable length, we can even occupy the entire memory page with objects that we control:
However, since the buffer that we can overflow is of a fixed-size (0x418 bytes) and we are targeting for corruption the last allocated object in the memory page, it doesn’t really matter what there is inside the space at page_allocation_base + 0x418 since the size of this chunk is of size 0x3b8, which is not of our interest. In other words, we can allow the kernel to use it at will.
By using Process Explorer from Sysinternals we can have a better view of how the paged pool memory allocations look like after spraying the heap, but before punching memory holes to create room for ‘SnxN’ tagged allocations (Figure 5).
The following figure shows the layout of a memory page after successfully spraying the heap and punching memory holes. Our exploit triggers the bug that allows us to overflow the ‘SnxN’ tagged buffer and corrupt the adjacent object that we control.
Private namespaces, which are implemented as directory objects, are not only interesting because of the ability that they give to the attacker to manipulate the size of the allocated paged pool chunk. They also allow us to control the execution by overwriting the pointer stored in the LIST_ENTRY field of the NAMESPACE_DESCRIPTOR structure. This field links the aforementioned structure into a linked list of all the private namespaces available in the system.
Assuming that we have successfully managed to corrupt the LIST_ENTRY field of the NAMESPACE_DESCRIPTOR structure of a specific private namespace, then upon deletion of this we are able to trigger a write-what-where condition.
However, this method will not work from Windows 8 and above because the kernel implements safe unlinking of LIST_ENTRY structures which mitigates this method of exploitation.
I noticed during the process of creating the exploit that it seems to be possible to take advantage of this bug using the same objects but without relying on this specific method. That being said, I still decided to do it that way for the sake of writing a working proof-of-concept exploit for this vulnerability targeting Windows 7 SP1 x86.
In the following figure, we show a directory object of a private namespace before and after corruption. Notice that we have overwritten the LIST_ENTRY field of the NAMESPACE_DESCRIPTOR structure with a userland address (0x41414141) that we control.
Exploitation – Controlling the EIP
After corrupting the directory object (Figure 9), we need to take control of the execution flow and redirect it to our payload.
We used the write-what-where condition to overwrite a function pointer in HalDispatchTable, and more specifically the pointer to hal!HaliQuerySystemInformation function which is stored at HalDispatchTable+sizeof(ULONG_PTR). We can then redirect the execution on our payload by calling ntdll!NtQueryIntervalProfile from userland.
The calling function sequence is: ntdll!NtQueryIntervalProfile à nt!NtQueryIntervalProfile à nt!KeQueryIntervalProfileà call [nt!HalDispatchTable+sizeof(ULONG_PTR)] (0x41414141).
What we know so far is that we can overwrite an arbitrary pointer in kernel address space, and control the EIP via this hijack. However, this is not enough to have a working exploit.
The write-what-where condition is triggered upon unlinking a private namespace from the list of private namespaces available in the system. We are expecting this to happen once we try to close the handle, or in other words free the directory object of a particular private namespace.
After examining how ClosePrivateNamespace behaves in userland, I noticed that it is basically a combination of calling the undocumented ZwDeletePrivateNameSpace and ZwClose (CloseHandle in userland) kernel functions in this order. So basically, the kernel first unlinks the private namespace, then destroys the directory object that is ‘hosting’ it. This is actually a very interesting detail because it can help us to build a more stable exploit.
Remember, that at the moment of unlinking the private namespace in order to trigger the write-what-where condition the directory object and its pool chunk header have been corrupted due to the heap overflow. This means that by starting to free directory objects until we meet the corrupted one and proceed with the exploit the following situation might occur. If we free an object of which the pool chunk allocation header references the previous (corrupted) object, we are going to get a BSoD screen. This is because the kernel compares the actual size of the previous object (stored in the corrupted pool chunk header) with the size value stored in the object that we currently trying to free.
Furthermore, we don’t want to free our corrupted object before our payload has been executed and we have successfully fixed it, otherwise the host will go down again because of these kernel security related checks.
We can avoid this situation by separating these two stages. Indeed, we can first trigger the what-where condition just by calling ZwDeletePrivateNameSpace for all the potentially corrupted objects. This will trigger the unlinking of the private namespace that we are targeting, but doesn’t destroy the directory object itself. The kernel will also overwrite the LIST_ENTRY field of the NAMESPACE_DESCRIPTOR with a NULL pointer in order to indicate that this namespace has been unlinked already, but we can restore this later during the post-exploitation clean-up stage.
Finally, in our payload we can safely fix the corrupted directory object and the associated NAMESPACE_DESCRIPTOR structure so that the private namespace can be finally correctly unlinked upon terminating the exploit process.
Did you spot the difference? If not, don’t worry. Indeed it is not very clear, but if you look closely you will notice an added call sub_C161C instruction which basically calls a subroutine that is not visible here. Its purpose is to verify that the size of the supplied data fits the fixed-size ‘SnxN’ tagged allocation. If it doesn’t, then the driver calls ExAllocatePoolWithTag in order to allocate a new paged pool chunk where the data can be safely copied.
As a final note, it is very important to mention that the allocation of the ‘SnxN’ tagged buffers is not directly controlled by us. In a common scenario the memory allocation that we overflow in kernel address space occurs during the IOCTL request. However, this is not the case and we will leave it as an exercise to the reader to find a way to control this exploitation stage.
To contact Nettitude’s editor, please email firstname.lastname@example.org.