This blog entry is a special anti-malware edition showcasing how the most common bugs security products suffer from can allow a standard user to escalate into a privileged user. What we found through our research was that every major anti-malware vendor that we tested could be exploited. This post is intended to help developers, blue/red/purple teamers, and security researchers alike.
The irony of abusing anti-malware solutions to increase privilege is not lost on me; anti-malware solutions that are supposed to protect the user may unintentionally assist malware in gaining more privileges on the system. The vast number of affected machines is troublesome; probably every Windows machine out there has had at least one software that could be abused to gain elevated privileges via file manipulation attacks. Many vendors fall for the same types of bugs and, in particular, anti-malware products seem to be a lot more vulnerable to exploitation because of their high privileges. The sheer number of bugs within anti-malware products can be staggering, but many bugs that are found within such products can be easily eliminated.
The plethora of bugs found within the products are incapable of being discussed with any brevity, but we will look at a few examples and their root cause. In the end, I am sure you will be able to easily find these bugs, avoid similar mistakes and, ultimately, make some file-system exploitation methods obsolete.
Don’t Trust ProgramData
We begin with the first cause of many bugs, which is the default DACLs of the C:\ProgramData directory. On Windows, the ProgramData directory is used by applications to store data that is not specific to a user. This means that processes\services that are not tied to a specific user would probably use ProgramData instead of the %LocalAppData%, which is accessible by the current logged in user. I assume this is the reason why ProgramData has permissive DACLs by design so that every user can access directories there freely.
Figure 1 – Standard DACL of C:\ProgramData – The root of all evil
You can see that every user has both write and delete permission on the base level of the directory. It means every user can create new files or directories there that would be owned by the current user who has full control over those resources. So, if a non-privileged process created a directory in ProgramData that would be later used by a privileged process, we might have a security issue on our hands. The newly created directory will, of course, have a permissive DACL, which is typical if you call CreateFile with LPSECURITY_ATTRIBUTES argument set to NULL.
Two problems arise to the surface because of this behavior:
1. What happens if a non-privileged process creates directories\files that would be later used by a privileged process?
2. What happens if you create a directory\directory-tree before a privileged process? Would it change the DACL?
We will see soon how these two problems potentially lead to many EoP bugs.
The way we opted to abuse the insecure access of privileged processes was through the combination of NTFS Mount Points and Object Manager symbolic links. In the past, we could also use hard links, but a March 2020 update made the use of hard links obsolete for those kinds of attacks. Windows now enforces the user to have write permission over the target file.
Shared Log File Bug
To answer what happens if a non-privileged process creates directories\files that would be later used by a privileged process, we will look at Avira’s AV. Avira’s AV has two processes that write to the same log file.
The design is simple; we have a regular user-owned process that writes to a log file at C:\ProgramData\Avira\Launcher\Logfiles\oewindbg.log:
Figure 2 – Two processes write to the same file, one of then runs in NT AUTHORITY\SYSTEM
The process Avira.Systray.exe runs in the context of the local, unprivileged user. It also created the directory Logfiles and the log file oewindbg.log with the following DACL:
Figure 3 – DACLs of the logfiles directory and of the file we manipulate
The DACL of the log file oewindbg.log allows us to delete it, and the DACL of Logfiles allows us to change the directory to a mount point that would point on \RPC Control. We can also create a symlink that would point to any desired path. The root cause of this bug is that they have a shared resource that is being accessed for write from two different security contexts. We can learn from that that every file resource that goes by these criteria is a potential EoP that is meant to happen.
Let’s see a snippet of the execution sequence in Avira’s anti-virus:
Figure 4 – The file blah.txt is created in a protected directory on behalf of the service
Here we can see one of Avira’s services acting inappropriately. The engine is executing multiple services, one of them, Avira.ServiceHost.exe, runs in the context of NT AUTHORITY\SYSTEM, which is perfectly fine to carry out its duties, but it does not perform any user impersonation at all. The lack of impersonation and the different security context of Avira.Systray.exe that runs in the current user privilege level means that we’ve got a bug.
The Avira.ServiceHost.exe service should be extra careful when it accesses files. In this example, we see how we can easily redirect the output of the write operation to any desired file by using the symlink attack mentioned above, which is nice but not super useful. However, we can sometimes exploit this behavior to get arbitrary delete and arbitrary file creation with arbitrary content. This is possible if the service changes the DACL or performs a delete operation.
It’s important to note that while this example showcases Avira’s anti-virus solution, it is not limited to this product or vendor alone.
The Early Bird Gets the Worm
If we look at the second question, what happens if we create the directories beforehand? Well, it turns out, in 99% of the cases, the privileged process won’t change the DACL of an existing directory. Typically, the privileged process will set the directory to be admin ACLed if the directory does not exist. This is the default behavior as it uses a restricted DACL that is based on his access token.
However, it seems developers skip the logical step of calling SetNamedSecurityInfoA or any equivalent API in case the directory exists. The fact that developers miss the step of changing the permissions on the directory\file causes many, many bugs. Although these bugs are weaker in terms of exploitation because they sometimes require the user to install software, you can count on the fact that your local updating program of your PC vendor is vulnerable to those types of bugs that you can activate on demand.
At the beginning of the blog, I mentioned AV vendors, so let’s continue by examining a bug in the McAfee anti-virus.
The McAfee anti-virus installer is a solid example of that. Upon installing the AV, the installer creates installation related files inside the path C:\ProgramData\McAfee. If we don’t create the directory McAfee beforehand, it will have a default of a directory that is created by a privileged process. This behavior is to be expected, but problematic.
Figure 5 – Default situation; shows the permissions if the McAfee doesn’t exist
However, if we create the McAfee before running the installer, it will have permissive permissions; the standard user has full control over the directory.
Figure 6 – The standard user has full control
In this case, we take advantage of the C:\ProgramData\McAfee\MCLOGS DACL. This directory serves as the log’s hub of any McAfee software. Therefore, every log file that is written to this directory could be abused easily by performing a symlink attack. Moreover, the AV installer lacks some checks before accessing the logs directory:
Figure 7 – Code Snippet 1
The problem in that code is that it assumes that the McAfeeLogDir is a regular directory, and not a reparse point to \RPC Control. The installer only cares if the directory exists, not its DACL or its flags; therefore, it doesn’t understand it could be a mount point. If we pass the mere test of existence, we get to the DeleteFile operation, which we can cause it to delete any desired file using a symlink, because of the high privilege level of the installer. Therefore, we have arbitrary delete vulnerability here. The solution to this problem is simple: the test of the MCLOGS directory is a reparse point. If we call the FindFirstFile API on McAfeeLogDir, the value of dwFileAttribute will be 0x2410.
The value 0x2410 is a sum of the constants: FILE_ATTRIBUTE_REPARSE_POINT + FILE_ATTRIBUTE_NOT_CONTENT_INDEXED + FILE_ATTRIBUTE_DIRECTORY. If the installer would have compared it against it. Alternatively, a better user impersonation should be done.
Using an Old Installation Framework = DLL Hijacking
Installers, programs that install new software on your machine, often supply the best opportunities for malicious users to escalate their privileges via DLL hijacking. The main reason attackers will look for these opportunities is because vendors update the inside packages, but they often forget to update the installer package. In other words, no one updates the installers only the code within them, and therefore, many software products that rely on installation frameworks are vulnerable to DLL Hijacking. What do I mean by vulnerable to DLL Hijacking? Well, it means that a standard user can abuse DLL loading of a privileged process and successfully inject code into it. Privilege escalation via DLL Hijacking must not rely on writeable entries in the %PATH%. Here is a partial short list of installation frameworks (MSIs seems pretty safe) that were found vulnerable to such an attack:
It is important to note that as far as I am concerned, if you use the latest version of the installation frameworks above, you should be in the clear. Still every vendor that was tested had software that was packaged in a non-updated version. Therefore, those installers are automatically vulnerable to DLL Hijacking upon installation because individuals don’t update their installers.
Let’s see how we abuse it.
Here, we see the problem in an installer of the TrendMicro anti-virus.
Figure 8 – That’s what happens when you run an installer from an unprotected directory
As you can as see, the installer is being executed from the download’s directory, which the local user has access to. Now, if we put a DLL named iertuitl.dll in the directory that does a DLL proxy to the real iertuitl.dll, we can get ourselves a code execution inside the anti-virus installer.
Some of you might claim that Microsoft classifies it as a defense-in-depth, and I quote “an issue that will be considered for updates in future versions only” because this is a case of application dir Hijacking. In my eyes and multiple vendors’ point of view, application dir Hijacking should be fixed and treated like any other type of vulnerability. My main argument is that even if you follow Windows’ idea of least privilege and use a non–privileged account for your work, you will still be vulnerable because you must run the installer with admin privileges (unless it doesn’t alter the system’s settings), and the execution directory would probably be the Downloads directory.
If you read my findings on CVE-2019-3726, you can see how unsafe ProgramData and DLL Hijacking work together. In there, and in CVE-2019-14688 (Trend Micro again), the installer also suffered from DLL Hijacking. But this was not limited to the current directory, because the installer copied itself into a directory inside ProgramData without having the ability to change its DACL.
After that, it required elevation, hence, admin privileges, which made it exploitable by a regular user by putting a malicious DLL inside. There are more examples besides these, but they do showcase the problem well.
How Can We Fix It?
This part is for the developers out there. Let’s strive for simple solutions, and in this case, they are indeed easily applicable:
1. Changing DACLs Before Usage – In case we need to create a directory that would be accessed by every user, hence creating it in ProgramData. We should change the DACL during each file creation operation performed on a directory; we must apply a restrictive ACL if a privileged code is accessing it. Altering the DACLs should be done in case we have a cleanup code on the uninstallation\update procedure. If not, we are vulnerable to arbitrary delete vulnerability.
2. Correct Impersonating – If you really need to access a file from two different security contexts, make sure the privileged process impersonation is on point throughout all code paths (easier said than done in some cases), or just use two different files. Having a file that can’t be deleted would prevent the creation of a mount point to \RPC Control.
3. Updating Installation Framework – If you are writing a new installer, updating the installed code, please update it to the latest version. Also, consider switching to Windows MSIs as they are safer. This vulnerability class also holds for custom installers, which are not using any third-party vendors.
4. Using LoadLibraryEx – Using this instead of the old LoadLibrary API allows you to specify the flag or to change the search and load order. By calling LoadLibraryEx, we can set the dwFlags argument to 0x800, which is LOAD_LIBRARY_SEARCH_SYSTEM32, we eliminate the chance of loading a shady DLL altogether. In case your application is 32-bit, then it would load the correct DLL from C:\Windows\SysWow64.
The implications of these bugs are often full privilege escalation of the local system. Due to the high privilege level of security products, an error in them could help malware to sustain its foothold and cause more damage to the organization. The exploits that were presented here are easy to implement, but also easy to patch against. We have seen that blocking symlink attacks or blocking the load of malicious DLLs require only a small touchup in the code. Knowing that, AV vendors should be able to eliminate this widespread bug class.
While each of these vulnerabilities have now been fixed, I would to specifically recognize the Kaspersky PSIRT team, who were quick to respond to the bug reports and issue a patch for the vulnerabilities.
Kaspersky CVE-2020-25045, CVE-2020-25044, CVE-2020-25043 McAfee CVE-2020-7250, CVE-2020-7310 Symantec CVE-2019-19548 Fortinet CVE-2020-9290 Checkpoint CVE-2019-8452 Trend Micro CVE-2019-19688, CVE-2019-19689 +3 Avira – CVE-2020-13903 Microsoft-CVE-2019-1161 Avast + F-Secure – Waiting for Mitre