Elevation of privileges from Everyone through Avast Sandbox to System AmPPL (CVE-2021-45335, CVE-2021-45336 and CVE-2021-45337)
0x00: Introduction
In March 2020 (during quarantine) I researched the security of Avast Free Antivirus ver. 20.1.2397 and I may have been one of the first external security researchers to explore the product’s newest feature – the antivirus (AV) engine sandbox. Today we will talk about it and I will show how by adding a cool security feature you can open a new attack path and, as a result, let the attacker through the chain of vulnerabilities (CVE-2021-45335, CVE-2021-45336 and CVE-2021-45337) elevate privileges from normal user to “NT AUTHORITY\SYSTEM” with Antimalware Protected Process Light protection level (link to description of impact in the now unavailable Avast Hall of Fame. @Avast, thanks for putting it on a list no one has access to 😉).
0x01: Insecure DACL of a process aswEngSrv.exe (CVE-2021-45335)
When searchinging for vulnerabilities my first step (probably like everyone else) is to examine the accessible from my privilege level attack surface. At that
time I logged in as a normal user (not a member of the Administrators group) and launched the TokenViewer
application from the well-known
NtObjectManager package. And I saw the following picture:
It immediately catches the eye that the current low-privileged user, among the obvious access to applications running in the same context, has access to the
token of the process running as “NT AUTHORITY\SYSTEM”. This is not the default behavior. What can be done with this token? In short, nothing. To elevate
privileges I would like to impersonate toket or create a process with such a token but due to the lack of privileges for a regular user (SeImpersonatePrivilege
or SeAssignPrimaryToken
) and another user (ParentTokenId
and AuthId
) in the token, we cannot do any of this.
Let’s then take a closer look at the process of interest and try to understand what it does:
It is clear from the description of the binary file that the logic of scanning files has been moved to this process. There are a lot of file formats (+packers), including complex formats, parsing takes place in C/C++ – not a memory safe language – and the developers wisely decided to sandbox the process which is very likely to be pwned. Thereby reducing the impact from the exploitation of a potential remote code execution (RCE).
NOTE: I don’t know what triggered the release of the antivirus engine sandbox in 2020 and how hastily it came out but perhaps the vulnerability report and the ported JS interpreter code from @taviso speeded up its release.
It is logical to assume that the high privileged AvastSvc.exe
process assigns the task of scanning the contents of the file via inter-process communication (IPC) to
aswEngSrv.exe
, and the latter, in turn, scans the data and makes a verdict like “virus” or “benign file”. Having dealt with the functionality implemented by this
process injecting into it does not seem senseless. After all if we can inject into the scanner process we can influence its verdicts and ultimately get the
ability to delete almost any (“almost” because AVs usually have the concept of system critical objects (SCO) of files that they will never delete. This is
implemented so that you do not accidentally remove system files) file.
If you look at the OpenProcessToken
documentation
you will see that in order to open a token you must have the PROCESS_QUERY_LIMITED_INFORMATION
access right on the process. Since TokenViewer
shows us a token
it means that it was able to successfully call OpenProcessToken
, which means that we have some kind of rights to the process. Usually there is no way for the user
to open processes running as “NT AUTHORITY\SYSTEM”. Look at the DACL of the aswEngSrv.exe
process:
.\accesschk64.exe -p aswEngSrv.exe -nobanner
[4704] aswEngSrv.exe
RW Everyone
RW NT AUTHORITY\ANONYMOUS LOGON
RW APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES
RW NT AUTHORITY\SYSTEM
Obviously with such a DACL you can make an inject for every taste (in the PoC I used the Pinjectra project).
Thus using the insecure DACL of the aswEngSrv.exe
process we can obtain a gadget for deleting arbitrary files as follows:
- Send the file we want to delete for scanning;
- Inject the code into the sandboxed process of the AV engine
aswEngSrv.exe
and “say” that the file is malicious; - After that the privileged
AvastSvc.exe
service will have to delete the corresponding file.
There is a vulnerability and it is clear how to exploit it but I still want to understand why there is such a permissive DACL on the process object. Is this a mistake of the antivirus developers or a strange behavior of the operating system (OS) when creating a child process with a restricted token?
The process and thread DACL are specified by DefaultDACL
of the primary token of the process. By default the DefaultDACL
is created by the system adequately and
developers usually do not need to configure it themselves (many people do not even know about its existence). When creating a restricted token the DefaultDACL
is
simply copied from a primary token, and in the case of the AvastSvc
service it is quite strict by default and contains literally 2 ACEs:
Only “NT AUTHORITY\SYSTEM” and “BUILTIN\Administrators” access is allowed, and for Administrators this is not full access. But then for some reason the developers themselves create the maximum permissive DACL and set it to the restricted token:
The comment in the code highlights in the SDDL format the value of the security descriptor used in runtime: Full Access for “Everyone”, “Anonymous Logon” and
“All Application Packages”. This actually explains why the aswEngSrv.exe
process has such a DACL.
I also want to make an assumption why the default behavior did not suit the developers and they decided to manually configure the DefaultDACL
. I have two versions.
The first is that when a process creates objects, the
DACL on them is assigned in accordance with the inherited ACEs of the parent container.
But if there is no container then DACL comes from the primary or impersonation token of the creator. And when aswEngSrv.exe
was launched with the default
DefaultDACL
then after creating its objects it could not reopen them due to the strict DACL. And the second version is that RPC, COM-runtime and other system code
often tries to open their own process token and if you do not configure the DefaultDACL
, as the Avast developers did, then the process cannot open its own token
and the code crashes with strange errors. And this is inconvenient.
0x02: Sandbox escape (CVE-2021-45336)
I’ve never liked arbitrary file deletion vulnerabilities because I don’t think the file deletion impact is that interesting in real life. And I want of course the
execution of arbitrary code in the context of a privileged user. To this end I decided to see what can be achieved by injecting into aswEngSrv.exe
besides deleting
files.
In fact this is counterintuitive – from a process with the rights of the current user get into the sandbox to elevate privileges. Because the sandbox by design provides the code executing in it uniquely less privileges than the normal user has. The same idea was in the Avast sandbox. Below is a picture with a process token:
It can be seen that this is a restricted token owned by SYSTEM. The developers did everything in accordance with chapter 1.2 “Restricting Privileges on Windows” of
the book “Secure Programming Cookbook for C and C++” by John Viega, Matt Messier.
If you do not know this concept I highly recommend that you familiarize yourself with the ideas from the book and now we will look at how restricted token is used
to create a sandbox in Avast AV. AvastSvc.exe
crafts restricted token by setting the “BUILTIN\Administrators” SID to
DENY_ONLY
, removing all privileges except
SeChangeNotifyPrivilege
, adding
restricted SIDs that characterize a normal unprivileged user (you can see it in the picture above), as well as lowering the integrity level to Medium. After that when
you try to access the securable object from the context of the sandboxed aswEngSrv.exe
the following process occurs (the algorithm is shown in a very simplified way,
only to explain how restricted token works):
The access check takes place in two rounds – for the normal list of SIDs and for restricted -, and the verdict is made based on the intersection of the permissions
issued in two rounds. The picture shows that in round 1 permission was obtained for RW
, in the second – only for R
, which means that the process will not be able
to get the desired access to RW
, since {R, W} ∩ {R} = {R}
.
But at the same time we see that the sandbox is somewhat unusual – launched from “NT AUTHORITY\SYSTEM”. What if you can get out of it and at the same time “reset” your restrictions and ultimately get the original privileged process token – parent token of the restricted. Let’s try to enumerate available resources such as files using the following command:
In the code listing above we used the Get-AccessibleFile
cmdlet to get all filesystem objects on the C:
drive,into which we can somehow write from the aswEngSrv.exe
privilege level. The result is a list of resources available for a normal user. Interestingly there are
such locations that are often used
to bypass SRP. But from the point of view of privilege escalation this is not notably promising since the straightforward attack of a system service by manipulating
accessible files or the registry or something else will definitely be very time consuming.
Thus the search for the possibility of elevation through securable objects such as files, registry, processes, thread is not immediately suitable due to the existing restrictions that are provided by the restricted token implementation. There remains the option of exploitation IPC – RPC, ALPC, COM, etc. Moreover it is necessary that during the IPC request the token is not impersonated, but only checked, for example, for the owner who is quite privileged in our case, and then privileged actions are already performed e.g. spawning a child process.
Even earlier I saw the post by Clément Labro – he wrote that with help of the
TaskScheduler
you can return dropped privileges by creating a new task. And even then I had a feeling that the TaskScheduler
could act as an entity that could
restore the original token from modified. The article did not explain why it worked there and therefore it was not clear whether this approach would work in our
case. But nevertheless a hypothesis appeared: what if the restricted token of the aswEngSrv.exe
can also be upgraded? And I decided to consider this vector as
a possible sandbox escape.
If you look at the low-level implementation of the TaskScheduler
interface you can see from the
specification that to register a task it is enough to call
the SchRpcRegisterTask
RPC method. I tried to do this using
powershell impersonating the aswEngSrv.exe
process token and in its context writing a task that should already be running as a non-restricted SYSTEM:
But Register-ScheduledTask
for some reason does not
use the impersonation token, probably the work is transferred to the thread pool which “does not know” about impersonation. And so the call happens in the context of the
process’ token. So this experiment failed and I did not find anything better than writing
my own native COM-client
to call SchRpcRegisterTask
under an impersonated restricted token.
And it worked! Using the TaskScheduler COM API from the restricted context of the
sandboxed aswEngSrv.exe
you can register any task which will then be executed in the SYSTEM context without any restrictions.
If you look at the code why TaskScheduler
allows you to do this trick you can see the following checks:
And if isPrivilegedAccount == TRUE
then the TaskScheduler
allows you to register and run almost any task with any principal regardless of the caller’s
current token. Inside User::IsLocalSystem
function there is just a check for user in the token and if it is equal to WinLocalSystemSid
then the function returns TRUE
.
So it is clear why the described approach with registering a task from the context of restricted aswEngSrv.exe
works and allows you to escape the sandbox.
Btw James Forshaw published two posts about TaskScheduler
features
(here and here)
where the similar idea and the same TaskScheduler
’s code are exploited.
NOTE: A month after I discovered this vulnerability James Forshaw wrote the article “Sharing a Logon Session a Little Too Much” which describes another interesting way to escape this type of sandbox.
0x03: Manual PPL’ing of a process wsc_proxy.exe (CVE-2021-45337)
When researching antiviruses,you often encounter the problem of debugging and obtaining information about product processes. The reason for this is that often antiviruses make their processes anti-malware protected. For it AV vendors use Protected Process Light (PPL) concept and set the security level of their processes to the Antimalware level (AmPPL). Because of this, by design, a malicious program even with Administrator rights cannot influence – terminate process (there are workarounds), inject its own code – on AV processes. But the downside of this feature is that security researchers cannot debug the code of interest, instrument it or view the process configuration.
Of cource a kernel debugger can be overcomethese difficulties. For example Tavis Ormandi patched
the nt!RtlTestProtectedAccess
function. This will allow you to interact with securable objects, such as opening a process with
OpenProcess
or a thread with
OpentThread
but will not allow you to load unsigned module from
disk into the process.
NOTE: There are also approaches like PPLKiller with installing a driver that modifies
EPROCESS
kernel structures and resets protection but this is too invasive for me.
And although the method described above certainly has its advantages, such as complete transparency for the product, I often reset the security by modifying the services
config which is set by the installer at the stage of installing the product. If you carefully read the
documentation on how to start AmPPL
processes you can see that at the service installation stage you need to call
ChangeServiceConfig2
with the handle of the configured service,
SERVICE_CONFIG_LAUNCH_PROTECTED
level and a pointer to the
SERVICE_LAUNCH_PROTECTED_INFO
structure, the “protection type” member
of which should be set to the value SERVICE_LAUNCH_PROTECTED_ANTIMALWARE_LIGHT
.
Intercepting and canceling the call to the ChangeServiceConfig2
function with the specified parameters on the installer side seems problematic since you don’t know in
advance from which process the protection of AV services is set. Therefore knowing that ChangeServiceConfig2
under the hood is just an RPC client of the
Service Control Manager (SCM) interface
, and accordingly
each call to ChangeServiceConfig2
from any process continues in RPC-method
RChangeServiceConfig2W
of process services.exe
, I decided
to set a conditional breakpoint on RChangeServiceConfig2W
and cancel on the fly attempts to do the service AmPPL.
Interestingly, there is no format in the documentation for
RChangeServiceConfig2W
parameters to set the protection of a service but this format is not hard to deduce from knowing the client format and the format for other types
of messages on the server. It turns out the following:
And then the conditional breakpoint which replaces the installation of the AmPPL service with a NOP-call, will look like this (set in the context of services.exe
after attaching to it):
bp /p @$proc services!RChangeServiceConfig2W ".if (poi(@rdx) == 0n12) { ed poi(@rdx + 8) 0 }; gc"
And it doesn’t really make much difference how you disable or bypass the PPL but this approach helped me find another bug. After the full installation of the product, you can make sure in Process Explorer that all AV processes are running without PPL protection:
The processes in the picture are sorted by the “Company Name” field and, as it seems, all Avast’s processes are without PPL protection. But among the processes there is
a wsc_proxy.exe
process (highlighted in the picture), it has AmPPL protection and is not supplied by default with the OS. So what is this process? It is also an Avast component,
for some reason PPL protection is on it and because of this Process Explorer
cannot read the company name of the binary from which the process is created.
At first I thought my method of not setting process PPL protection was incomplete. Well, for example, there are other SCM APIs that can be used to make a service PPL.
But not finding any I set a hardware breakpoint on the Protection
field of the EPROCESS
structure of the wsc_proxy.exe
process at its start and found that this
field is filled from the aswSP.sys
– the kernel self-defense module of the product:
The screenshot above shows that the aswSP.sys
driver directly modifies the EPROCESS
structure of the process and sets the Protection
field in it as follows:
Protection.Type = 0n1; // PsProtectedTypeProtectedLight (0n1)
Protection.Signer = 0n3; // PsProtectedSignerAntimalware (0n3)
Now we realize that Avast Free Antivirus somehow not quite honestly uses the PPL infrastructure and forcibly makes its processes PPL-protected bypassing Microsoft requirements. And as attacker we would like to use this functionality and make our own code AmPPL. Then we can influence other AmPPL-protected processes.
To do this you need to understand when and under what conditions the code above is reachable. After reversing aswSP.sys
I found out that the function with this code is called
from the process creation callback handler registered with
PsSetCreateProcessNotifyRoutine
. And in order for the driver
to directly execute this code and make the process PPL two conditions must be met:
- The process must be spawned from the binary file
"C:\Program Files\AVAST Software\Avast\wsc_proxy.exe"
; - The process must be running as “NT AUTHORITY\SYSTEM”.
These requirements (if they are checked correctly) severely limit the scope of applicability of this functionality for an attacker but still allow having SYSTEM privileges to
obtain an AmPPL protection level. This can be done by implementing the usual image hollowing of wsc_proxy.exe
when running it as child process in the SYSTEM context. Then
both conditions will be met and we can easily deliver our payload to the process thanks to the handle received from
CreateProcess
with ALL_ACCESS
rights to the created process
and the subsequent WriteProcessMemory
with the payload. Below is the PoC of
the proposed method:
In the screenshot above powershell is first launched with Administrator rights. It launches a powershell instance running under “NT Authority\System” (1)
. Next we start
wsc_proxy.exe
in the suspended state (2)
. And we demonstrate that there is no PPL protection yet (3)
but we as a parent have a handle of the child process with AllAccess
rights (4)
. Using the handle we overwrite the process memory with the necessary contents (5)
– in this case it is an infinite loop, and continue the execution of the process.
At this point process-creation callback implemented by aswSP.sys
checks for the above-mentioned conditions and changes the EPROCESS.Protection
of the process. Next we can
verify that the process has become AmPPL-protected (6)
and see in Process Explorer
that the process is executing our code and consuming CPU with its infinite cycle (7)
.
As a result due to this vulnerability we have a primitive that allows us, having SYSTEM privileges, to obtain for our process AmPPL-protection level.
By the way the EPROCESS
structure is an opaque structure and offset to the Protection
field is not something fixed and constant. Therefore for OSs it must be calculated.
Avast does this by searching by signature in the exported kernel function PsIsProtectedProcess
:
0x04: Exploitation chain
Building all three vulnerabilities in a chain we get the following exploitation scenario which allows you to increase privileges from Everyone to “NT AUTHORITY\SYSTEM” with the AmPPL protection level:
- As standard user inject into the
aswEngSrv.exe
process; - Inside sandbox create a
Task Scheduler
task to run your code under the full “NT AUTHORITY\SYSTEM” account and trigger the launch; - Executing in the “NT AUTHORITY\SYSTEM” context start the process spawned from the binary file
"C:\Program Files\AVAST Software\Avast\wsc_proxy.exe"
with theCREATE_SUSPENDED
flag, overwrite theEntryPoint
with your own code and continue the process execution; - Now the code is executed in the “NT AUTHORITY\SYSTEM” context inside the AmPPL-protected
wsc_proxy.exe
process.
Below is a demo video of the exploitation (in the end the input and output of the powercat.ps1
were slightly out of sync but I hope this does not interfere to understand the
main idea):
Note: Recently AV has been detecting “powercat” and quarantining it. So for the demonstration purposes, the script must be added to the exclusions, and to work in real life, the payload must be changed to something slightly less famous.
The full source code of the PoC can be found on my github.
0x05: Fixes retest
After almost 3 years (now the beginning of February 2023) after discovering vulnerabilities, reporting them to the vendor and even claiming that everything was fixed, I decided to see how developers fixed the vulnerabilities. To do this I installed Avast Free Antivirus 22.12.6044 (build 22.12.7758.769). So let’s go!
Fixing the insecure DACL of a process aswEngSrv.exe
(CVE-2021-45335) is pretty simple: the developers explicitly set the DefaultDACL
of the token as before but now it is a
more strict DACL of the form D:(A;;GA;;;BA)(A;; GA;;;SY)S:(ML;;NW;;;LW)
. The SDDL representation of DACL indicates that access is now allowed only “NT Authority\System” and
“Administrators”, while the integrity label is Low (a curious decision).
As result the token now looks like this:
DACL on the process corresponds to the above value from the token’s DefaultDACL
. We will not be able to inject as before so believe that the vulnerability has been fixed.
And then it’s more interesting – we move on to checking the sandbox escape (CVE-2021-45336). Back in 2020 I wrote in the report to the Avast developers that they had very
little chance of making a good sandbox running as “NT Authority\System”. But as we can see in the new version of the product the aswEngSrv.exe
process’ token has not changed
in this regard. So how did they fix it?
The developers did not change the “NT Authority\System” user under which the aswEngSrv.exe
process was originally executed, the set of groups and jobs too. So at first glance
it looks like they couldn’t fix the vulnerability. I manually injected the module demonstrating PoC but nothing worked as expected. It’s just not clear why.
As a result of debugging the code I found out that my COM-client crashes during the initialization of the COM runtime. Previously the runtime was probably already initialized
at the time of injection. There were quite a lot of errors and there was no desire to understand them but there was definitely an understanding that problems with the COM runtime
could not be a sufficient mitigation from escaping the sandbox. Moreover the entire COM binding of TaskScheduler
is client-side code implemented essentially for the convenience
of clients. And on the server side, as we said earlier, there is a single RPC method SchRpcRegisterTask
. Therefore I decided not to deal with errors and wrote my own RPC-client
of TaskScheduler
. When running the code started to fail again but when locating
problems it turned out that the RPC runtime often uses function
OpenProcessToken
with the
GetCurrentProcess
parameter to get its own token and ends
with ACCESS_DENIED
since the updated DefaulDACL
does not allow even itself to open it. I wrote a hook for such calls and replaced them with returning a pseudohandle using
GetCurrentProcessToken
. The pseudohandle is “pseudo” because
it does not need to be opened, so there were no more problems with access rights. And the code worked – again it turned out to register a task from the aswEngSrv.exe
sandbox which
runs as SYSTEM. I posted the CVE-2023-ASWSBX
PoC code on my github. Surprisingly the
developers fixed a specific implementation of the exploit but did not fix the root cause.
NOTE: In the
aswEngSrv.exe
code I saw that different hooks are being set and perhaps that is why the original approach with COM does not work. But obviously in-process hooks cannot be the solution.
As for the bug when manually modifying PPL Protection for the wsc_proxy.exe
process, the developers have now signed the binary with the appropriate certificate and made the
AvastWscReporter
AmPPL service in a documented way. But if you open the aswSP.sys
self-protection driver and look for functions that use the PsIsProtectedProcess
string, you will immediately find a function that just as it was shown earlier in the screenshot
looks for the offset of the Protection
member in the EPROCESS
structure. Further if you look at where this offset is used you can find a function that sets the value 0x31
in
the Protection
field of the process. And what is most interesting this function is reachable from the IOCTL handler:
So it seems that the developers have fixed this particular vulnerability but there are still execution paths in the code that can allow you to do the same thing but in a slightly different way (no longer hollowing or not only it).
0x06: Conclusions
Almost three years ago Avast released the awesome by purpose security feature – antivirus engine sandbox. Then I found 3 vulnerabilities and by connecting them in a chain I got the opportunity to elevate privileges from an unprivileged user to a process with the rights of “NT Authority\System” and AmPPL protection. Moreover discovered sandbox escape was a design problem that, by definition, cannot be fixed easily and quickly.
Then I explained to myself the “mistakes” of the solution by its novelty and hoped that over time this feature would become more mature and become an excellent investment in the resistance of the antivirus to bugs in the most valuable attack surface of the product.
But now I discovered that the exploitation chain was broken by fixing only one link from the chain (fortunately at least the first one 😊). The main problem is that the design of the sandbox has not been fixed. Which makes, sadly, all sandboxing completely useless. In addition, judging by the fact that the manual PPL’ing code is present in the driver, this issue may also not be completely fixed.
0x07: Disclosure Timeline
-
25-03-2020 Initial report sent to Avast.
-
26-03-2020 Initial response from Avast stating they’re being reviewed it.
-
23-04-2020 Avast triaged the issue reported as a valid issue and is starting work on a fix.
-
08-09-2020 Avast released patched version of product.
-
09-02-2023 This post has been published.