In the Patch Tuesday update of April 2024, Microsoft released a fix for CVE-2024-20693, a vulnerability we reported. This vulnerability allowed manipulating the cached signature signing level of an executable or DLL. In this post, we’ll describe how we found this issue and what the impact could be on Windows 11.
Last year, we started a project to improve our knowledge of Windows internals, specifically about local vulnerabilities such as privilege escalation. The best way to get started on a new target is to look at recent publications from other researchers. This gives the most up to date overview of the security design, allows looking for variants of the vulnerability or even bypasses for the implemented fixes.
The most helpful prior work we found was the presentation “The Print Spooler Bug that Wasn’t in the Print Spooler” at OffensiveCon 2023 by Maddie Stone and James Forshaw from Google. This talk describes a Windows privilege escalation exploit discovered in the wild.
Privilege escalation using an impersonated device map and isolation-aware DLLs
In case you haven’t watched this presentation, we’ll summarize it here: a highly privileged Windows service that handles requests on behalf of lower-privileged processes can impersonate the requesting process, in order to make all operations performed by the highly privileged service be performed with the permissions and privileges of the lower-privileged process. This is a great security measure, as it means the highly privileged service can never accidentally do something the lower-privileged process would not be able to do itself.
One thing to note is that a (lowly privileged) process on Windows can change its device map, which can be used to redirect a device letter such as C:
to a different location (for example a specific subfolder, like C:\fakeroot
). This changed device map is one of the aspects included in impersonation. This is quite risky: what if the impersonating service attempts to load a DLL while impersonating another process which has set a different device map? That issue was already reported in 2015 by James Forshaw and fixed.
However, the logic for determining which file to load for LoadLibrary
can be quite complicated if it involves side-by-side assemblies (WinSxS). On Windows, it’s possible to install multiple different versions of a DLL and manifest files can be used to specify which version to load for a specific application. DLL files can also include an embedded manifest to specify which version of its versioned dependencies to load. These are called “isolation aware” DLLs.
The core of the exploited vulnerability is the fact that when an isolation aware DLL file is loaded, the impersonated device map would be used to find the manifests of its dependencies. By combining this with a path traversal in the manifest file, it was possible to make a privileged service load a DLL from a folder on disk specified by the lower privileged process. Loading this malicious DLL would then lead to privilege escalation (impersonation by design no longer provides any security when malicious code is loaded, because it can revert the impersonation). For this attack to work, the impersonating service must load an isolation aware DLL, which depends on at least one other DLL.
The fix applied by Microsoft to address the issue covered in the Maddie Stone and James Forshaw presentation was to apply a new mitigation to disable the loading of WinSxS manifests using the impersonated device map, but only for processes that have specifically opted-in. This flag was only set for a few services that were known to be vulnerable. This means that a lot of privileged services were left that could be examined for the same issue. Very helpfully, Maddie and James explained how to configure Process Monitor on Windows how to find these issues:
So, we set to work finding issues like this. We made a list of isolation aware DLLs with at least one dependency on another library, set up the Process Monitor filters as described and wrote a simple PowerShell script (using the NtObjectManager PowerShell module also from James Forshaw) to enumerate all RPC services and attempt to call all methods. Then, we cross-referenced the libraries loaded under impersonation with the list of DLLs using a manifest.
We found a single match: wscsvc.dll
!
When calling the RPC endpoint with number 12 on this service (which takes no arguments), it indirectly loads gpedit.dll
. This is an isolation aware DLL, which depends on (among others) comctl32.dll
. We replicated the setup from the in-the-wild exploit, creating a new directory at C:\fakeroot
, added the required manifest and DLL files, redirecting C:
to C:\fakeroot
and then sending this COM message.
And it works… almost. Process Monitor shows that it opens and reads our fake DLL file, but never gets to “Load Image”, which is the step where the actual code execution starts. Somehow, it was resolving our DLL but refusing to execute its code.
Then we found out that the process associated with the wscsvc.dll service, namely the “Windows Security Center Service”, is categorized as a PPL (Protected Process Light). This means that it places restrictions on the code signature of DLL files it loads.
Protected Process (Light)
Windows recognizes a number of different protection levels to protect processes from being “modified” (such as terminating it, modifying memory, adding new threads) by a process at a lower protection level. This is used, for example, to make it harder to disable AV tools or important Windows services.
As of Windows 11, the protection levels are:
Level | Value |
---|---|
App | 8 |
WinSystem | 7 |
WinTcb | 6 |
Windows | 5 |
Lsa | 4 |
Antimalware | 3 |
CodeGen | 2 |
Authenticode | 1 |
None | 0 |
Whether an operation is allowed is determined by a table known as RtlProtectedAccess
. We have summarized it as follows:
→Target ↓Requesting | Authenti- code | CodeGen | Anti- malware | Lsa | Windows | WinTcb | WinSystem | App | ||
---|---|---|---|---|---|---|---|---|---|---|
Authenticode | ✅ | |||||||||
CodeGen | ✅ | |||||||||
Antimalware | ✅ | ✅ | ||||||||
Lsa | ✅ | |||||||||
Windows | ✅ | ✅ | ✅ | ✅ | ✅ | |||||
WinTcb | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |||
WinSystem | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
It can roughly be summarized as follows: Windows, WinTcb and WinSystem form a hierarchy (Windows < WinTcb < WinSystem). Authenticode, CodeGen, Antimalware and Lsa are separate groups that only allow access from processes in the same group or the Win-* hierarchy. We are not sure how “App” is used, it is new in Windows 10 and has not been documented very well.
In addition, there is the difference between a Protected Process (PP) and a Protected Process Light (PPL): a Protected Process can modify a Protected Process Light (based on the table above), but not the other way around. The some examples are Antimalware PPLs for third-party security tools and WinTCB or Windows at PP for critical Windows services (like managing DRM). Keep in mind that these protection levels are also in addition to all other authorization checks (such as integrity levels) in Windows. For more information about protected processes, see https://itm4n.github.io/lsass-runasppl/.
Note that this is not considered a defended security boundary by Microsoft: a process running as Administrator can load an exploitable kernel driver, which can be used to modify all protected processes. As admin to kernel is not a security boundary according to Microsoft, protected processes can also not be a security boundary for Administrators.
Aside from the restrictions on being manipulated by other processes, protected processes are also limited in what DLLs they may load. For example, anti-malware services may only load DLLs signed with the same codesigning certificate or by Microsoft itself. From Protecting anti-malware services:
DLL signing requirements
[A]ny non-Windows DLLs that get loaded into the protected service must be signed with the same certificate that was used to sign the anti-malware service.
For protected processes in general, they are only allowed to load a DLL signed with a specific Signature Level. Signature levels are assigned to a DLL based on the certificate and its issuer used for the code signature. The exact rules for when which PPL level may load a DLL with a specific signature level are quite complicated (and these rules can even be customized with a secure boot policy) and we’ll not go into those here. But to summarize: only certain Windows-signed DLLs were allowed to be loaded into our target service.
At this point we had two options: find a different service with the same WinSxS under impersonation vulnerability, or try to bypass the signing of Windows DLL files. The most likely to yield results would of course have been to look for a different service, but the goal of the project was to understand Windows internals better, so we decided to spend a little bit of time on understanding how DLL files are signed.
DLL signatures
The codesigning process for PE files is known as Authenticode. Just like TLS, it is based on X.509 certificates. An Authenticode signature is generated by computing the hash of the PE file (leaving out certain fields that will change after signing, such as the checksum and the section for the signature itself), then signing that hash and appending it to the file with the certificate chain (and optionally a timestamp).
Because signature verification can be slow and loading DLLs happens often on Windows, a caching method has been implemented for code signatures. For a signed DLL or EXE file, the result of the certificate verification can be stored in an NTFS Extended Attribute (EA) named $KERNEL.PURGE.ESBCACHE
. The $KERNEL
part of this name means that only the Windows kernel is allowed to set or change this EA. The PURGE
part means that the EA will be automatically removed if the contents of the file are modified. This means that it should not be possible to set this EA from usermode or to modify the file without removing the EA. This only works on journaled NTFS partitions, as the PURGE
functionality depends on the journal. Note that nothing in this EA binds it to the file: these attributes contain the journal ID, but nothing like a file path or inode number.
In 2017, James Forshaw had reported that it was possible to race the application of this EA: by making the file refer to a catalog, it was possible to slow down the verification enough to modify the contents of the file in between the verification of the signature and the application of the EA. As this was already found a while ago, it was unlikely that doing this was going to work.
We experimented with placing the file on an SMB share instead and attempting to rewrite the contents in between the verification and image loading, but this wasn’t working either (the file was only being read once). But looking at our Wireshark capture and the decompiled code in CI.DLL that parses the $KERNEL.PURGE.ESBCACHE
extended attribute we noticed something standing out:
A $KERNEL.PURGE.ESBCACHE
extended attribute should only be trusted on the local volume, as a filesystem on (for example) a USB drive or mounted disk image could have been manipulated by the user. There was a check in the code we assumed was meant to check for this and only allow the local boot disk using the function CipGetVolumeFlags
.
__int64 __fastcall CipGetVolumeFlags(__int64 file, int *attributeInformation, _BYTE *containerState)
{
int *v6; // x20
BOOL shouldFree; // w21
int ioctlResponse; // w9
unsigned int err; // w19
unsigned int v10; // w19
__int64 buffer; // x0
int outputBuffer; // [xsp+0h] [xbp-40h] BYREF
int returnedOutputBufferLength; // [xsp+4h] [xbp-3Ch] BYREF
int fsInformation[14]; // [xsp+8h] [xbp-38h] BYREF
outputBuffer = 0;
returnedOutputBufferLength = 0;
memset(fsInformation, 0, 48);
v6 = fsInformation;
shouldFree = 0;
// containerState will be set based on the response to the ioctl with ID 0x90390LL on the file
if ( (int)FsRtlKernelFsControlFile(file, 0x90390LL, 0LL, 0LL, &outputBuffer, 4LL, &returnedOutputBufferLength) >= 0 )
ioctlResponse = outputBuffer;
else
ioctlResponse = 0;
outputBuffer = ioctlResponse;
*containerState = ioctlResponse & 1;
// attributeInformation will be set based on the IoQueryVolumeInformation for FileFsAttributeInformation (5)
err = IoQueryVolumeInformation(file, 5LL, 48LL, fsInformation, &returnedOutputBufferLength);
if ( err == 0x80000005 )
{
// Retry in case the buffer is too small
v10 = fsInformation[2] + 8;
buffer = ExAllocatePool2(258LL, (unsigned int)(fsInformation[2] + 8), 'csIC');
v6 = (int *)buffer;
if ( !buffer )
return 0xC000009A;
shouldFree = 1;
err = IoQueryVolumeInformation(file, 5LL, v10, buffer, &returnedOutputBufferLength);
}
if ( (err & 0x80000000) == 0 )
*attributeInformation = *v6;
if ( shouldFree )
ExFreePoolWithTag(v6, 'csIC');
return err;
}
This was being called from CipGetFileCache
:
__int64 __fastcall CipGetFileCache(
__int64 fileObject,
unsigned __int8 a2,
int a3,
unsigned int *a4,
_DWORD *a5,
unsigned __int8 *a6,
int *a7,
__int64 a8,
_DWORD *a9,
_DWORD *a10,
__int64 a11,
__int64 a12,
_QWORD *a13,
__int64 *a14)
{
__int64 eaBuffer_1; // x20
unsigned __int64 v17; // x22
unsigned int fileAttributes; // w25
unsigned int attributeInformation_FileSystemAttributes; // w19
unsigned int err; // w19
unsigned int err_1; // w0
__int64 v22; // x4
__int64 v23; // x3
__int64 v24; // x2
__int64 v25; // x1
int containerState_1; // w10
unsigned int v28; // w8
__int64 eaBuffer; // x0
_DWORD *v30; // x23
unsigned __int8 *v31; // x24
int v32; // w8
char v33; // w22
const char *v34; // x10
__int16 v35; // w9
char v36; // w8
unsigned int v37; // w25
int v38; // w9
int IsEnabled; // w0
unsigned int v40; // w8
unsigned int ContextForReplay; // w0
__int64 v42; // x2
_QWORD *v43; // x11
int v44; // w10
__int64 v45; // x9
unsigned __int8 containerState; // [xsp+10h] [xbp-C0h] BYREF
char v47[7]; // [xsp+11h] [xbp-BFh] BYREF
_DWORD *v48; // [xsp+18h] [xbp-B8h]
unsigned __int8 *v49; // [xsp+20h] [xbp-B0h]
unsigned __int8 v50; // [xsp+28h] [xbp-A8h]
unsigned __int64 v51; // [xsp+30h] [xbp-A0h] BYREF
unsigned int v52; // [xsp+38h] [xbp-98h]
int attributeInformation; // [xsp+3Ch] [xbp-94h] BYREF
int v54; // [xsp+40h] [xbp-90h] BYREF
int lengthReturned_1; // [xsp+44h] [xbp-8Ch] BYREF
int lengthReturned; // [xsp+48h] [xbp-88h] BYREF
int v57; // [xsp+4Ch] [xbp-84h]
__int64 v58; // [xsp+50h] [xbp-80h]
__int64 v59; // [xsp+58h] [xbp-78h]
__int64 v60; // [xsp+60h] [xbp-70h]
_QWORD *v61; // [xsp+68h] [xbp-68h]
int *v62; // [xsp+70h] [xbp-60h]
int eaList[8]; // [xsp+78h] [xbp-58h] BYREF
char fileBasicInformation[40]; // [xsp+98h] [xbp-38h] BYREF
[...]
if ( (*(_DWORD *)(*(_QWORD *)(fileObject + 8) + 48LL) & 0x100) != 0 )
{
containerState_1 = 0;
}
else
{
lengthReturned_1 = 0;
memset(fileBasicInformation, 0, sizeof(fileBasicInformation));
err = IoQueryFileInformation(fileObject, 4LL, 40LL, fileBasicInformation, &lengthReturned_1);
if ( (err & 0x80000000) != 0 )
{
[...]
goto LABEL_8;
}
fileAttributes = *(_DWORD *)&fileBasicInformation[32];
// Calling the function above
err_1 = CipGetVolumeFlags(fileObject, &attributeInformation, &containerState);
v17 = v51;
err = err_1;
if ( (err_1 & 0x80000000) != 0 )
{
*a4 = 27;
LABEL_7:
v22 = *a4;
goto LABEL_8;
}
attributeInformation_FileSystemAttributes = attributeInformation;
containerState_1 = containerState;
}
// If the out variable containerState was non-zero, all of the checks don't matter and we go to LABEL_19 to read the EA.
if ( (*(_DWORD *)(*(_QWORD *)(fileObject + 8) + 48LL) & 0x100) != 0 || containerState_1 )
goto LABEL_19;
if ( (g_CiOptions & 0x100) == 0 )
{
if ( (attributeInformation_FileSystemAttributes & 0x20000) == 0 || (fileAttributes & 0x4000) == 0 )
{
*a4 = 5;
v17 = fileAttributes | ((unsigned __int64)attributeInformation_FileSystemAttributes << 32);
err = 0xC00000BB;
goto LABEL_7;
}
goto LABEL_23;
}
if ( (attributeInformation_FileSystemAttributes & 0x20000) != 0 && (fileAttributes & 0x4000) != 0 )
{
[...]
}
LABEL_19:
eaBuffer = ExAllocateFromPagedLookasideList(&g_CiEaCacheLookasideList);
eaBuffer_1 = eaBuffer;
if ( !eaBuffer )
{
v28 = 28;
err = 0xC0000017;
goto LABEL_12;
}
v33 = v50;
eaList[0] = 0;
LOBYTE(eaList[1]) = 22;
if ( v50 )
{
v34 = "$Kernel.Purge.CIpCache";
*(_OWORD *)((char *)&eaList[1] + 1) = *(_OWORD *)"$Kernel.Purge.CIpCache";
}
else
{
v34 = "$Kernel.Purge.ESBCache";
*(_OWORD *)((char *)&eaList[1] + 1) = *(_OWORD *)"$Kernel.Purge.ESBCache";
}
v35 = *((_WORD *)v34 + 10);
*(int *)((char *)&eaList[5] + 1) = *((_DWORD *)v34 + 4);
v36 = v34[22];
*(_WORD *)((char *)&eaList[6] + 1) = v35;
HIBYTE(eaList[6]) = v36;
err = FsRtlQueryKernelEaFile(fileObject, eaBuffer, 380LL, 0LL, eaList, 32LL, 0LL, 1LL, &lengthReturned);
if ( (err & 0x80000000) != 0 )
{
*a4 = 2;
LABEL_34:
v30 = v48;
v31 = v49;
LABEL_35:
ExFreeToPagedLookasideList(&g_CiEaCacheLookasideList, eaBuffer_1);
v17 = v51;
goto LABEL_36;
}
err = CipParseFileCache(eaBuffer_1, v33, (int *)a4, &v51, eaBuffer_1 + 488);
if ( (err & 0x80000000) != 0 )
goto LABEL_34;
v37 = v57;
err = CipVerifyFileCache((__int64 *)(eaBuffer_1 + 488), eaBuffer_1, fileObject, v57, v58, &v54, (int *)a4, &v51);
[...]
return err;
}
What we assumed to be an ioctl that would be handled by the SMB driver (using code 0x90390
, which isn’t documented officially, but may refer to FSCTL_QUERY_VOLUME_CONTAINER_STATE
, based on Microsoft’s Rust headers) turned out to be an ioctl that gets forwarded over SMB to the server. (While we called it NTFS Extended Attributes, these extended attributes in fact work over SMB too.)
If that icotl results in a value with the lowest bit set, containerState
/containerState_1
in CipGetFileCache
become non-zero and the code jumps to LABEL_19
above (skipping a lot checks on the file type, device type and a g_CiOptions
global we don’t fully understand either).
In other words: the $KERNEL.PURGE.ESBCACHE
extended attribute on a file on a SMB share is trusted if the SMB server itself responds to this ioctl that it should be trusted! This is of course a problem, as by default non-admin users can mount new network shares.
We started out with samba and patched it to always respond 0x00000001
to this ioctl (it is not implemented currently) and implemented two more ioctls: 0x900f4
(FSCTL_QUERY_USN_JOURNAL
) for reading the journaling information and 0x900ef
(FSCTL_WRITE_USN_CLOSE_RECORD
) for flushing the journal. We configured Samba to use the ext3 extended attributes to store the EAs used for SMB.
And it worked! From our Linux server running samba, we could apply any $KERNEL.PURGE.ESBCACHE
attribute on a file and Windows would trust it. On Linux, the extended attributes used by Samba can be set using setfattr
. 1
setfattr -n 'user.$KERNEL.PURGE.ESBCACHE' -v '0skwAAAAMAAg4AAAAAAAAAAIC1e18kqdkBQgAAAHUAJwEMgAAAIGliE1R8dXRmTogdh511MDKXHu0gQC2E1gewfvL5KmZ+JwAMgAAAIGOIg7QdUUiX461yis071EIc4IyH1TDa1WkxRY/PW8thJwQMgAAAIDSDabKZZ2jBOK8AdcS2gu8F0miSEm+H/RilbYQrLrbj' "$1"
We could now create fake EAs that could specify any code signing level we wanted. How can we abuse this?
Now we got to the next challenge: how do we combine these two vulnerabilities? We could make wscsvc.dll
load our own DLL using path traversal, but we can’t path traversal from C:
into an SMB share. A symbolic link could work, but by default non-admin users on Windows are not allowed to create these. Directory junctions and other symlink-like constructs that Windows supports can not point to SMB shares.
We could perform the attack if the user plugged in a NTFS formatted USB device with a symlink to the SMB share. The user could then create a directory junction from the new C:
mountpoint in their devicemap to the USB disk.
C:\fakeroot --(directory junction)--> E:\ --(symlink)--> \\sambaserver\mount
But this required physical access to the machine. We preferred something that would also work remotely.
So we needed a third vulnerability: creating a symlink as a non-admin user.
We tried various things, like mounting disk images or unpacking zip files with symlinks, but before we had found a way to do this, Microsoft had rolled out a more extensive fix for the WinSxS manifest loading under impersonated device maps in August 2023 (as CVE-2023-35359): instead of being opt-in for processes, the device map was now always ignored for reading the manifest.
This meant that our DLL loading vulnerability in wscsvc.dll
was no longer working, but we still had the signature bypass. So, next question: what can we do with just cached signature level manipulation on Windows?
Privilege escalation to SYSTEM using .theme files
In the previous post “Getting SYSTEM on Windows in style” we showed how we managed to elevate privileges on Windows by racing a signing check for a DLL included from a Windows .theme
file. In that post, we used a race condition, but we originally found it by setting a manipulated $KERNEL.PURGE.ESBCACHE
attribute on the *.msstyles_vrf.dll
file. This worked in essentially the same way: we set a new theme which refers to a specifically crafted .msstyles
file. Next to the .msstyles
file, we place a .msstyles_vrf.dll
file. When the user logs in (or sets DPI scaling to >100%), WinLogon.exe (which runs as SYSTEM) will check the signature level of this DLL file, and if it is at least signed at level 6 (“Store”), it will load it, elevating our privileges.
As Microsoft completely removed the loading of *.msstyles_vrf.dll
files from themes for CVE-2023-38146, this issue was also fixed.
Bypassing WDAC
One place where cached signatures were used for executables is for Windows Defender Application Control (WDAC), which is an allowlisting technology for executables on Windows. This functionality can be used (typically in a corporate environment) to limit which applications a user is allowed to run and which DLLs may be loaded. Binaries can be allowlisted based on file path, file hash but also the identity of the code signer. WDAC policies can be very powerful and granular, so each company using it probably has their own policy, but the default templates allow all software signed by Microsoft itself to run.
Assuming the WDAC policy allows all software signed by Microsoft, we can add an EA indicating Microsoft as the signer to any executable and run it.
Injecting code into protected processes
The signature bypass can also be used by administrators to inject code into a protected process (regardless of the level). For example, by replacing a DLL from system32 with a symlink to a SMB share and then launching a service that runs as a protected process.
Keep in mind that this is not considered a security boundary by Microsoft, which also means that known techniques that abuse this do not get fixed. So for our demonstration we combined it with the approach used by ANGRYORCHARD to mark our thread as a kernel mode thread and then map the device’s physical memory into our process.
Combining all steps
- We use the modified EA on a
.msstyles_vrf.dll
file to bypass the signature verification in Winlogon.exe to elevate privileges toSYSTEM
. - We replace a DLL file from
system32
with a symlink to a file with a manipulated cached signature on the SMB share. Then, we launch a protected process running at levelWindowsTCB
(we choseservices.exe
). - We use our code running in
services.exe
to inject code intoCSRSS.exe
and apply the technique from ANGRYORCHARD to gain physical memory r/w.
Combined with the Mark-of-the-Web bypass found by gabe_k for .themepack files, this attack could have been triggered with just the user opening a downloaded file. Depending on the WDAC policy, we could also have bypassed that.
So, how did Microsoft fix this?
We had hoped they would disable the reading of $KERNEL.*
extended attributes from SMB completely. However, that was not the approach that was taken. Instead, the instances we exploited were fixed:
- The fix for CVE-2023-38146 already disabled the loading for
*.msstyles_vrf.dll
files completely, fixing the privilege escalation. - When WDAC is enabled, the function to retrieve the cached signature level of a file now always returns an error (even for local files!).
- When loading a DLL into a protected process, the cached signature level is no longer used. (This was fixed despite Microsoft not considering it a defended security boundary.)
- August 25, 2023: Issue reported to MSRC.
- September 12, 2023: The fix for CVE-2023-38146 was released, breaking our privilege escalation exploit.
- September 20, 2023: MSRC indicates that they have reproduced the issue and that a fix is scheduled for January 2024.
- December 11, 2023: MSRC informs us that a regression was found and asks to reschedule the fix to April 2024.
- April 9, 2024: Fix released for the WDAC and PPL bypass as CVE-2024-20693.
- April 25, 2024: MSRC asks Microsoft Bounty Team for an update, CCing us.
- April 26, 2024: Microsoft Bounty Team sends back a boilerplate reply that the case is under review.
- May 17, 2024: MSRC asks Microsoft Bounty Team for an update, CCing us again.
- May 22, 2024: Microsoft Bounty Team replies that the vulnerability was out of scope for a bounty, claiming it didn’t reproduce on the right WIP build.
This attack depends on the victim connecting to a malicious SMB server. Therefore, blocking outgoing SMB to the internet would make this attack a lot harder (unless the attacker already has a foothold on the local network). Preventing users from mounting new SMB shares could also be done as a mitigation, but could have more unintended results.
Examining SMB traffic for exploitation of this issue should also be possible by looking for responses to the ioctl 0x90390
or responses for the EA $KERNEL.PURGE.ESBCACHE
.
We set out to increase our understanding of Windows internals by adapting research into DLL loading into impersonating services using WinSxS, but we got sidetracked into examining the code signing method used for DLL files and we found a way to bypass it. While we were unable to apply it in the scenario we started out with, we did find other places where we could use it to elevate privileges, bypass WDAC and inject code into protected processes. Just like our previous research “Bad things come in large packages: .pkg signature verification bypass on macOS” about a signature bypass for macOS .pkg files, we see here that vulnerabilities in cryptographic operations can often be applied in a multitude of ways, allowing different security measures to be bypassed. Just like that example, this vulnerability could go from a user opening a downloaded file to full system compromise.