~10 min read
The Key to COMpromise - Abusing a TOCTOU race to gain SYSTEM, Part 2
Recap
In the first post of this blog series, we explored the architectural design of various security products and demonstrated how COM hijacking can be leveraged to exploit them: We examined a vulnerability that allowed us to replay a modified message over a named pipe, highlighting a potential attack vector.
As discussed previously, many security products have frontend processes operating in the context of an unprivileged user, which are capable of initiating privileged actions — such as adding exclusions — by interacting with a backend service running at higher privileges. To prevent abuse, most vendors implement mechanisms to ensure these actions originate from trusted processes and take steps to protect those processes from tampering.
However, because frontend processes execute with limited user privileges, COM hijacking presents an opportunity to load a malicious DLL into the process. In our research, we found that this attack vector was viable across all the products we targeted, allowing us to exploit the security product’s inherent trust in its own processes.
To capitalize on this trust relationship, we needed to reverse engineer the communication protocols between the frontend and backend processes. This helped us identify interactions that could be manipulated to escalate privileges.
In this post, we will delve into how we exploited this trust in AVG Internet Security (CVE-2024-6510 ) to gain elevated privileges. But before that, the next section will detail how we overcame an allow-listing mechanism that initially disrupted our COM hijacking attempts.
Circumventing an allow list
For this part of our research, we employed the same basic technique as before, but with one key difference: this time, the COM interface was triggered each time we opened a file dialog to block an application. However, we encountered a restriction — we could not load our DLL from just any folder.
When trying to load the DLL from our custom folder at C:\poc
, we could not observe any successful DLL load in the Process Monitor
.
In contrast, the original DLL path worked without issue.
Through trial and error, we discovered that placing our DLL in the C:\Windows\system32
directory allowed it to load successfully.
This behavior revealed that the product validates the DLL’s directory against an allow list, likely as a defense against DLL hijacking attacks.
While loading from C:\Windows\system32
bypassed the allow list, this approach was impractical for our privilege escalation since an unprivileged user cannot write to this directory.
However, based on our prior experience bypassing AppLocker configurations, we knew that some subdirectories within C:\Windows\system32
were writable by unprivileged users.
One such directory is C:\Windows\System32\spool\drivers\color
.
By placing the DLL used for the COM hijacking in this writable subdirectory, we successfully bypassed the allow list and achieved code execution in the frontend process.
With this DLL injection method established, the next step was to analyze the communication with backend processes. In the following section, we will discuss how we leveraged this primitive to manipulate the trust relationship and escalate privileges.
Reverse engineering the RPC communication
Reverse Engineering RPC communication can be a daunting task, especially in the beginning. Fortunately, tools like RpcView are invaluable for enumerating and identifying RPC interfaces. However, the process ultimately requires in-depth reverse engineering efforts. For our work with AVG, we used the excellent Akamai Research RPC Toolkit to identify and analyze various RPC interfaces across the different AVG binaries.
Our focus was on RPC server interfaces, as these are the endpoints exposed by high-privileged processes.
While the AVGSvc.exe
executable does not contain RPC server bindings, we found that the ashServ.dll
DLL, which is loaded by the service, does expose such interfaces!
The Akamai RPC Toolkit produced the following output:
"ashServ.dll": {
// [...]
"908d4c23-138f-4ac5-af4a-08584ae7c67b": {
"number_of_functions": 22,
"functions_pointers": [
"0x1654e0700",
"0x1654e0790",
// [...]
],
"role": "server",
"flags": "0x6000000",
"interface_address": "0x165f96020",
// [...]
"eb915940-6276-11d2-b8e7-006097c59f07": {
"number_of_functions": 106,
"functions_pointers": [
"0x1655c8180",
"0x1655c8290",
// [...]
"role": "server",
"flags": "0x6000000",
"interface_address": "0x165fca670"
},
"1118fbbd-02ee-4910-9d86-9940537ee146": {
"number_of_functions": 23,
"functions_pointers": [
"0x1655c08d0",
"0x1655c6be0",
// [...]
],
"role": "server",
"flags": "0x6000000",
"interface_address": "0x165fccfb0"
}
From this output, we can observe three major interfaces with 22, 106, and 23 exposed endpoints.
The largest interface is the [Aavm]
RPC interface, which has been the subject of previous research and exploitation.
Searching the interface GUID on the web reveals some other interesting blog posts back in the year 2015..
Reverse engineering and renaming the functions within the RPC interface is tedious but relatively straightforward.
Through this analysis, we discovered an RPC function named AavmRpcRunSystemComponent
that uses the CreateProcess
API without RPC impersonation:
.rdata:0000000165FCA550 dq offset sub_1655C5580
.rdata:0000000165FCA558 dq offset sub_1655C55D0
.rdata:0000000165FCA560 dq offset AavmRpcRunSystemComponent
.rdata:0000000165FCA568 dq offset DecryptData
.rdata:0000000165FCA570 dq offset AddNetAlert
When the RPC client is not impersonated, any new process spawned through this function will run with SYSTEM
privileges, creating a critical opportunity for privilege escalation.
However, before this process is initiated, a DSA_FileVerify
check takes place:
__int64 __fastcall AavmRpcRunSystemComponent(__int64 a1, unsigned int whitelist_id, __int64 arguments, DWORD *out_pid)
{
// [...]
char out_string[32];
// [...]
v8 = GetFileById(out_string, whitelist_id); // [1]
// [...]
FileW = CreateFileW((LPCWSTR)out_string, 0x80000000, 1u, 0i64, 3u, 0x8000000u, 0i64);
v12 = FileW;
v21 = (__int64)FileW;
if ( FileW == (HANDLE)-1i64 )
{
// file not found
}
if ( !GetFinalPathNameByHandleW(FileW, szFilePath, 0x104u, 0) )
{
// File path could not be resolved
}
if ( whitelist_id != 2 && !(unsigned __int8)DSA_FileVerify(szFilePath, 0i64, 18i64) ) // [2]
{
LastError = 87; // ERROR_INVALID_PARAMETER
CloseHandle(v12);
return LastError;
}
// [...]
snprintf(combined_arguments, v15, L"%s %s", szFilePath, arguments); // [3]
// [...]
if ( CreateProcessW(szFilePath, combined_arguments, 0i64, 0i64, 0, 0, 0i64, 0i64, &StartupInfo, &ProcessInformation) ) // [4]
{
// Win ?
The DSA_FileVerify
function performs several validations:
- Based on the integer argument in
[1]
, it returns a filename. Most of the executable files in this list are repair or setup tools, such asaswOfferTool.exe
,SupportTool.exe
andAvEmUpdate.exe
, which limits the options to those predefined binaries. - A file signature verification is performed in
[2]
to ensure only trusted binaries can be executed. This check prevents an attacker from inserting their own malicious binary into the process. - Finally, the program arguments are constructed in
[3]
, and the process is created withSYSTEM
privileges in[4]
.
Although this function appears to be a promising privilege escalation vector, the constraints of the allow-listed binaries and file signature verification present significant roadblocks. Without the ability to exploit any of the allow-listed programs, this avenue may seem like a dead end.
To overcome this limitation, we decided to experiment with the RPC client bindings found in the aavmrpch.dll
library.
Using this approach, we began testing the functionality of various RPC interfaces, with particular emphasis on the AavmRpcRunSystemComponent
function, to explore potential exploitation paths.
Abusing the update mechanism
The most promising target for exploitation was the AvEmUpdate.exe
executable, which accepts a range of command-line arguments.
This executable is responsible for installing updates provided as cab
or DLL
files.
Since we could control the arguments passed to it, this presented a compelling opportunity for further exploration.
One particularly interesting argument was /applydll
, which allows the process to load a specified DLL.
Crucially, because the process runs with SYSTEM
privileges, this argument could potentially be abused to escalate privileges.
However, the update mechanism includes an additional safeguard: it verifies that the provided DLL is signed by the manufacturer.
This signature check prevented us from directly supplying a custom DLL to gain SYSTEM
privileges.
TOCTOU race
Despite this limitation, we were confident that we could bypass the integrity check by carefully analyzing and exploiting the logic of the process. We finally found a time of use vs time of check (TOCTOU) issue in the logic, which made the integrity checks bypassable. To exploit this reliably, we employed a combination of OpLocks (opportunistic locks) and junctions.
To control the timing of the file accesses during exploitation and exploit our race reliably we needed a way to put the update process in a waiting state. Here we used OpLock to block access to the DLL file and force the update process to wait for us releasing the OpLock. This works even on processes running as SYSTEM
, while operating as an unprivileged user.
This gives us time to prepare for the next step.
We also want to be able to switch out the DLL file while holding our OpLock. This is where junctions come in. Junctions are symbolic links that can redirect file system access to a different location. Since an unprivileged user can create junctions, we used this capability to redirect file accesses during the exploitation process while holding our OpLock. We can point the junction to an other location for the next file access and there precisely control which file is accessed for each single file access. For more information on OpLocks and junctions, refer to the code provided by James Forshaw and this article from ZDI.
Here’s how the exploit worked:
- The
AvEmUpdate.exe
process made multiple file accesses before loading the DLL, likely to verify its legitimacy. - Using a junction, we redirected the process to a valid, signed DLL for the first three file access attempts.
- On the fourth file access, when the process attempted to load the DLL, we redirected the junction to our malicious DLL containing the privilege escalation payload.
Because we were holding an OpLock on the initial three file accesses, we could dynamically change the target of the junction while the SYSTEM
process was waiting for access to the previous file.
After updating the junction’s target, we released the OpLock, allowing the process to move on to the next file.
We repeated this until the fourth access successfully loaded our malicious DLL.
Note that the process always accesses the same file; however, using the junction, we change the files accessible under this path.
While this technique successfully allowed us to bypass the signature verification and load our malicious DLL, it wasn’t sufficient on its own to fully escalate privileges. In the next section, we will delve into the additional steps required to achieve high privileges on the system and the challenges we encountered along the way.
Disabling self-defence
Even after successfully executing the TOCTOU (time-of-check-to-time-of-use) race, our malicious DLL was not loaded into the process. Upon further investigation, we discovered that the process only loaded DLLs with valid signatures. This added layer of protection significantly complicated our exploitation attempts. We suspect this behavior was due to the process being launched as a PPL (Protected Process Light) process.
After some trial and error, we found that this restriction was enforced only when the product’s self-protection feature was enabled.
Fortunately, we identified an RPC function, AavmRpcDisableSelfDefense
, that could disable this self-protection mechanism.
This function was exported by the same DLL (ashServ.dll
) we had already interacted with in our previous RPC calls.
By calling this function, we successfully disabled the product’s self-defense feature.
With self-defense disabled, our malicious DLL was successfully loaded into the process running with SYSTEM
privileges, finally completing the privilege escalation.
To summarize, the exploitation in this case worked as follows:
- Initial entry with COM hijacking:
- We used COM hijacking to load a DLL into the frontend process.
- To bypass the allow-listing mechanism, the DLL was placed in
C:\Windows\System32\spool\drivers\color
.
- Disabling self-defense:
- The loaded DLL then called the function
AavmRpcDisableSelfDefense
to deactivate the product’s self-protection feature.
- The loaded DLL then called the function
- Triggering the update mechanism:
- The DLL triggered an update by calling
AavmRpcRunSystemComponent
. - Using a junction in combination with OpLocks, we tricked the update process into loading an unsigned DLL.
- This allowed us to escalate our privileges to
SYSTEM
.
- The DLL triggered an update by calling
Summary
In this blog post, we demonstrated how COM hijacking was leveraged to gain SYSTEM
privileges for exploiting AVG Internet Security to gain privileges.
Unlike the previous case, we encountered additional obstacles, namely an allow-listing mechanism, that initially blocked our DLL.
We described how we bypassed this restriction by placing the DLL in a writable system directory.
We detailed our reverse engineering of the product’s RPC calls, which uncovered functions that allowed us to disable self-protection and trigger the update mechanism.
By combining a junction and OpLocks, we bypassed the signature check and successfully loaded an unsigned DLL, enabling us to escalate privileges to SYSTEM
.
In the next post, we will explore two additional vulnerabilities related to COM hijacking and describe how we exploited them to achieve privilege escalation.