by Nicolas Delhaye, Vulnerability Researcher
CVE-2019-18568 impacts a kernel component of the Avira Free Antivirus software. The “avipbb.sys” driver is responsible for enhancing the local security. In this context, we will see that any executables started on the host machine will be examined from a kernel driver. Unfortunately, this component is prone to an integer overflow vulnerability resulting in local privilege escalation. In this article, we will explore the root cause of this vulnerability. The article will then illustrate how to trigger the vulnerability and how to get full control of the exploitation. Finally, we will recall a DKOHM technique for easily exploiting the memory pool on Windows 7, this is the 0xbad0b0b0 technique.
For this analysis, the version of the avipbb.sys driver is 15.0.1906.72:
Figure 1:avipbb.sys Properties
One of the purposes of the avipbb.sys driver is to assign a confidence level and specific rights as soon as a new process is started. As such our entry point to challenge this driver is not based on the use of IOCTLs but on a portable executable.
Thus, the Avira Antivirus will be able to set internal flags according to the following criteria:
- Needs particular access on some resources
- Belongs to a white list of Windows process name (hardcoded into the driver)
- Requires administrator rights
- Comes from the Avira Antivirus software suite
You may attempt to impersonate the name of a Windows process, but you will fail. The avipbb.sys driver checks the full path name of the executable launched with the “Device\HarddiskVolume1\Windows\system32” prefix.
Avira executable are shipped with an embedded signature and the avipbb.sys driver relies on it to do some relevant checks, which means an authentication. In fact, this signature is part of a more complex Avira blob, composed of a magic number, a version, a structure size and other useful information.
The driver retrieves the signature from the file location on the file system. For this purpose, it looks for the “AVCS4F3A4200C37O” and “B62F3AB0132FAVCSE” strings which respectively define the beginning and the end of the signature.
The signature calculation needs to exclude some PE fields (« checksum », « security directory ») as well as the blob itself. These exclusions are inserted into a linked list before being sorted in ascending order of beginning address.
An integer overflow vulnerability leading to a privilege escalation occurs if the embedded structure (blob) is especially malformed. Please see below screenshot of the disassembled code highlighting where the integer overflow is located:
Figure 2: Integer overflow location
First, we noticed that the 32-bits register, r8d, is used for storing the sum of two 64-bits registers (rcx and r12). According to the values into these two registers, the instruction can lead to an integer overflow. In fact, the r8d register corresponds to the end offset of the data to be excluded. The latter is used for the AVIPBB_IGNORED_DATA_DESC_T structure and has the followings fields:
- An offset towards the begin of excluded data
- An offset towards the end of excluded data
- A “next” pointer to the next element (NULL for the last one)
The r12 register (FileSize) is initialised with the size of the executable file. The rcx register (dwSignatureSectionSize) comes from the embedded signature and is set with the 0x168 default value. That represents the signature length. Nevertheless, a missing check lacks on this length and we can modify it with a large value. Consequently, the”rcx+r12-1” instruction could lead to an integer overflow.
Finally, this disassembly code results in the following formula:
A buffer of the executable file size is allocated from the non-paged pool. It is used as a destination buffer to copy all of the useful data. Conceptually, if we take the excluded sections into account, we have more available memory than we need. Then, a loop is responsible for copying useful data to the allocated buffer from the exclusion list.
Please see below screenshot of a part of the disassembly code showing data reads from the executable file:
Figure 3: Signature computation – Copy the first useful data from PE
What we must keep in mind from the above screenshot is that the begin offset must be neither null nor greater than the executable file size. But above all, we will retrieve data from:
- Offset 0 to 0x108, which is a size of 0x108 bytes
- Offset 0x10C to 0x148, which is a size 0x3C bytes
- Offset 0x150 to 0x455, which is a size of 0x305 bytes
We can see that data located between the end of the Avira signature and the end of the executable file have not yet been retrieved. This is done later, after a final check on the begin offset field of the last AVIPBB_IGNORED_DATA_DESC_T entry.
Finally, the code causing the pool overflow will be called as follows:
Figure 4: Signature computation – Copy the last useful data from PE
At this moment, we get a pool overflow which is waiting to be exploited.
Getting a corrupted linked list
As discussed earlier, Avira excludes some PE fields (« checksum », « security directory ») as well as the signature itself before computing a hash. Then avipbb.sys sorts ascending all the useless section from an internal structure, named AVIPBB_IGNORED_DATA_DESC_T. So, if you corrupt the end offset of excluded data, the re-ordering algorithm will be affected.
The final objective is to put a corrupted structure at the end of the exclusion list. For that, we will need to deal with the integer overflow. This will result later in copying more data than the expected buffer size.
Please see below screenshot of the disassembled code highlighting the re-ordering algorithm:
Figure 5: Re-ordering algorithm to sort AVIPBB_IGNORED_DATA_DESC_T structures
As we deal with the integer overflow, the intended purpose is to set the r8 register to be smaller than the dwOffsetBegin field of all the excluded sections. This register will be adjusted from the previous formula described in the Vulnerability location chapter.
Please see below definition of the fields belonging to the AVIPBB_IGNORED_DATA_DESC_T structure:
- dwSignatureSectionSize, which is the size of the structure (0x168 bytes by default);
- FileSize, which is the size of the portable executable;
- dwOffsetOfSignatureFromEndFile, which represents an offset towards the beginning of the structure from the end of the executable file;
- dwSizeOfUnk2, which is currently set to 0;
Before attempting to insert the corrupted AVIPBB_IGNORED_DATA_DESC_T structure, the last excluded section is the security directory (RVA and Size). As shown in the below screenshot from the PE editor:
Figure 6: Offset of Security Directory RVA and Size
We have to take into account the integer overflow to set the r8 register greater than 0x148. Let’s take the following values:
- dwSignatureSectionSize equals to 0xffffff34 bytes;
- FileSize equals to 0x680 bytes;
- dwOffsetOfSignatureFromEndFile equals to 0x22b;
All these values are under our control. We see that dwOffsetEnd equals to 0x100000388. Hence this results in 0x388 bytes due to the 32-bits registers assigned. 0x388 comes from the following calculation: (0xffffff34 + (0x680 – 0 – 0x22b) – 1).
From now, we have an exclusion list especially crafted. Please see below screen shot of scheme showing the content of the linked list:
Figure 7: Linked list of AVIPBB_IGNORED_DATA_DESC_T structures
Please see the end offset with the 0x388 value. The latter is smaller than the begin offset. A nice bug!
We have produced an executable file of 0x680 bytes with the previously determined values. Let’s see what happens in a nominal and corrupted behavior.
Please see below screenshot of the executable file:
Figure 8: Extract from a specially designed executable (PE)
In the red border, we have all the data that must be ignored for the signature computing.
Nominal behavior consists of copying data as follows:
- From 0 to 0x108;
- From 0x10C to 0x148;
- From 0x150 to 0x455;
- From 0x5BD to the end of file, that is 0xC3 bytes;
A total of 0x50C bytes are copied. No overflow occurs.
Corrupted behavior consists of copying data as follows:
- The first steps are the same as previously
- The last copies data from 0x388 to the end of file, that is 0x2F8 bytes
A total of 0x741 bytes are copied for an allocated buffer of 0x680 bytes. This time the overflows occurs.
Why did I choose 0x680? We will answer this question and study why the exploitation closely depends on the size of the executable.
As the overflow occurs in the non-paged pool, we need to massage the memory for a successful exploitation. The aim is to allocate the buffer, which contains useful data for the signature calculation, before another allocation. Obviously, these allocations should be under our control. But how can we allocate kernel memory from a user process? In fact, you just need to create securable objects such as a file, an interprocess synchronisation objects (events, mutexes, semaphores, and waitable timers) and so on. From the userland, you retrieve a HANDLE but the kernel relies on the object manager to allocate an object type of a predetermined size.
After some experimentation on the targeted OS, the FILE object was a good candidate. The latter is 0x150 bytes length and is allocated from the non-paged pool. So, an executable file with a 0x690 bytes length is 5 times the size of a FILE object. However, as each pool allocation is prefixed by a POOL_HEADER of 0x10 bytes length, our executable file should be 0x680 bytes length.
The scenario will be to:
- Spray with FILE objects
- Free 5 adjacent FILE objects but keep one FILE object just after
- Start the especially crafted executable to exploit the vulnerability
Non-paged pool overflow
Please see below a memory dump of the nonpaged pool layout before the overflow occurs.
Figure 9: Nonpaged pool layout bebore the overflow
We can see that Avira has allocated a buffer with the tag AV0z. As the pool header is 0x10 bytes length, the real buffer is 0x680 bytes. So we should rewrite the following adjacent chunk at the 0xfffffa80038a2900 address.
Please see below screenshot of the memory as soon as the overflow occurs:
Figure 10: Nonpaged pool layout after the overflow
The adjacent chunk now makes reference to a FILE object because we have overwritten both the POOL_HEADER and the OBJECT_HEADER from the embedded data into our executable file.
Please see below screenshot of the memory dump from the 0xfffffa80038a2900 address:
Figure 11: Memory dump from the chunk overwritten
Look at the extract of the executable file upper (Figure 8), you will see that it is the same data in green border from offset 0x5C0 to the end of file! 🙂
0xbad0b0b0 technique for Win7 exploit
The 0xbad0b0b0 technique relies on DKOHM (Direct Kernel Object Header Manipulation) and has been described several times. Multiple sources are available on the internet. A good reference is the talk by Nikita Tarakanov at the NoSuchCon and BlackHat conferences.
So, our exploit is composed of:
- An executable which comes with an especially crafted signature
- Another executable with the purpose to prepare the 0xbad0b0b0 exploitation before starting the buggy executable
As we have a pool overflow, we could overwrite a kernel object. Each kernel object has an OBJECT_HEADER with the following structure:
Figure 12: OBJECT_HEADER structure
We focus on the TypeIndex field which is an index of pointer to an OBJECT_TYPE structure into the ObTypeIndextable array.
Let’s take a look at the ObTypeIndextable memory content:
Figure 13: bad0b0b0 address into nt!ObTypeIndexTable
Right now, a memory address sounds goods for a kernel privilege escalation. You will have noticed that 0x00000000bad0b0b0 could be mapped from userland.
After overwriting the TypeIndex with 1, we could allocate this memory area from a user process and craft any data inside. But what should we write? In fact, each entry of the ObTypeIndexTable is an OBJECT_TYPE object with the following structure:
Figure 14: OBJECT_TYPE structure
Finally, we will focus on the TypeInfo field:
Figure 15: OBJECT_TYPE_INITIALIZER structure
We were very lucky to get a lot of function pointers. As you might guess, we just need to create a fake OBJECT_TYPE structure from the 0xbad0b0b0 user address. Then, we insert a fake OBJECT_TYPE_INITIALIZER and replace one of the function pointers with our shellcode address. The latter will be called from the kernel and will be executed as nt authority/system. For example, we can put the shellcode address instead of the SecurityProcedure field. We trigger the final payload as soon as we call the NtQuerySecurityObject() function from a user process. Payload could steal the token of a SYSTEM process and replace it in the calling process.
This proof of concept exploits the vulnerability on Windows 7 Home Basic 64-bit with Service Pack 1 from a standard user and starts a new command shell with nt authority/system rights. Please see below screenshot highlighting the result after executing the POC:
Figure 16: Exploit execution – Local Privilege Escalation
Airbus CyberSecurity follows the widely accepted 90-day vulnerability disclosure policy; meaning Airbus CyberSecurity won’t engage any public communication about the reported vulnerability during that time frame without any prior public communication or fix. Please note that Airbus CyberSecurity’s general position is that as soon a working and active relationship is established, there is no need to blindly push for the 90-day vulnerability disclosure if it’s not necessary.
Avira demonstrates a good response time. They have fixed the vulnerability in August 2019, which is only one month after its internal disclosure.
July 03th, 2019 Airbus reported the vulnerability; Avira asked for more detailed information about the vulnerability
July 04th, 2019 Avira forwarded the report and exploit to the responsible departments
August 06th, 2019 Avira asked Airbus to confirm the fix in the new released version; Airbus confirmed the fix
Oct 10th, 2019 CVE process is initiated
Dec 31th, 2019 CVE-2019-15568 is under review
Jan 17th, 2020 CVE-2019-15568 is officially accepted
March 12th, 2020 Airbus publishes the report associated with the CVE