# Blogpost: CVE-2024-6769 Poisoning the activation cache to elevate from medium to high integrity 

This blog is about two chained bugs: Stage one is a DLL Hijacking bug
caused by the remapping of ROOT drive and stage two is an Activation
Cache Poisoning bug managed by the CSRSS server.

The first stage was presented in detail at Ekoparty 2023 in [the
presentation called "I'm
High"](https://ekoparty.org/eko2023-agenda/im-high/) by Nicolás Economou
from [BlueFrost
Security](https://labs.bluefrostsecurity.de/windows-system-drive-remapping-elevation-of-privileges).
He explained how to exploit the vulnerability which, at the time, had
not yet been patched by Microsoft. This allowed a MEDIUM INTEGRITY user
to be elevated to have limited HIGH PRIVILEGES, but without the complete
access to be a full Administrator.

The second stage was not presented at that conference, though some steps
were suggested to start researching it.

To begin, we will review the first stage to provide introductory
context. From there, we’ll dive into my research on the second stage,
going into the details of achieving full escalation from limited HIGH
INTEGRITY to full Administrator. This includes a complete working PoC
for both stages for all Windows versions, which has been successfully
tested in Windows 10, Windows 11, Windows Server 2022, and Windows
Server 2019 with all updates applied.

# Index:

- Review of the first stage
- Steps to follow to exploit the second stage.
- What is the Activation Cache?
- Using ALPC Attack Vector to Poison the Activation Cache
- How will the system accept our Activation Context?
- How to poison the Activation Cache?
- How is my embedded XML Manifest read?
- How is the embedded XML manifest parsed?
- How did my fake imm32.dll ends up loaded?
- Video Demo.
- Functional Proof of Concept 
- TL;DR Brief description of exploitation steps

## Review of the first stage

![A red square with white text and a number on it Description
automatically generated](./media/image1.png)

*The only requirement for this stage is that the initial process begins
at a MEDIUM INTEGRITY LEVEL and the user belongs to the Administrator
group.*

The first stage of exploitation can be summarized in the following
steps:

1.  Remapping of ROOT Drive using the **NtCreateSymbolicLinkObject**
    function.

For example: remapping disk from

**"C:**\\**"** to **"C:\users\public"**

This also will remap the "**system32**" folder from

"**C:\windows\system32**" to "**C:\users\public**\\**windows\system32**"

2.  After remapping, some Services are affected and will attempt to load
    libraries from the new, fake user-controlled system32.

One of these affected programs is **CTFMON**, which runs at a HIGH
INTEGRITY LEVEL but without Administrator privileges.

Normally, it tries to load the module called **MsCtfMonitor.dll** from
the real system32 folder, but since the ROOT drive was remapped, it
looks for **MsCtfMonitor.dll** in our fake controlled system32, where we
can create and place a crafted DLL with the same name.

3.  Create MsCtfMonitor.dll

At this point, by placing our version of **MsCtfMonitor.dll** in the
fake system32 folder, its **DoMsCtfMonitor** function is called and
executes our code at a HIGH INTEGRITY LEVEL.

![](./media/image2.png)

4.  Place a **MessageBoxA** on the **DoMsCtfMonitor** function. When
    **MsCtfMonitor.dll** is loaded, it will display the **MessageBoxA**
    "TRIGGER".

![](./media/image3.png)

5.  Verify that the DLL was loaded into the CTFMON process that runs at
    the HIGH INTEGRITY LEVEL:

![](./media/image4.png)

![](./media/image5.png)

At the same time, we can corroborate that the process, despite being at
a HIGH INTEGRITY LEVEL, does not have Administrator privileges:

![](./media/image6.png)

## 

## Steps for Exploitation of the Second Stage ![A red square with white text and a number on it Description automatically generated](./media/image7.png)

In his Ekoparty presentation, Nicolas suggested the following steps to
complete the exploitation:

![](./media/image8.png)

![](./media/image9.png)

While this seems simple, it requires a lot of time to reversing and
debugging.

Upon digging a little into this attack vector story, it became clear
that the poisoning of the Activation Context Cache has been used in some
exploits. Consequently, it is worthwhile to learn how the exploitation
has been done previously to provide additional context and insights.
Details on this exploitation are available through the Zero Day
Initiative’s writeup, *[Activation Context Cache Poisoning: Exploiting
CSRSS for Privilege
Escalation](https://www.zerodayinitiative.com/blog/2023/1/23/activation-context-cache-poisoning-exploiting-csrss-for-privilege-escalation).*

## What is the Activation Cache?

The usage of the Activation cache happens when a program is going to
load a library requiring a specific version.

For example, if an application is going to load
**C:\Windows\System32\comctl32.dll**, there is no guarantee that the
**comctl32.dll** in that location is the version that the application
needs. This is a basic use case of the Activation Contexts Cache. The
program can send a request to the CSRSS server to process a new
Activation Context Entry to be entered into the cache, so this program
can load the specific library version needed.

For this purpose, the so-called manifest is used, which is in XML
format. It is usually embedded as a resource in an EXE or DLL file.
Alternatively, Windows will search for a manifest file in the same
folder where the program's executable is located.

The URL mentioned above has some examples of Manifest files used by old
exploits, such as tricking the system to load the library advapi32.dll
from a controlled directory by the attacker that was reached through
PATH TRAVERSAL technique.

![](./media/image10.png)

Of course, some used attack vectors were patched, and some new
techniques were discovered. Additionally, in the October 2022 patch for
Windows 11 22H2, a new check was added.

After this patch was implemented, the check when an **Activation Context
(ACTX)** is registered can only be bypassed if the process which adds
the new entry in the cache has the same or higher RID than the process
which will use it.

In **winnt.h** we can see the RID values:

![](./media/image11.png)

The proposal for bypassing this check is to create a request with an
**Activation Context** from the **CTFMON** process where the crafted DLL
runs. This crafted DLL has **RID=0x3000** and after the entry is added
to the cache, **TCMSETUP** with **RID=0x3000** will load **tapi32.dll**.

During my attempt to follow the steps, I made all the possible
combinations to register the **ACTX** using
**[CreateActCtx](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createactctxa).**
This proved impossible since there was always a check that avoids it.

It is important to note that this function is located in userland,
exported by kernel32.dll. The checks can be avoided by patching the DLL
in memory, which is not very elegant, but it is possible and should
work.

![](./media/image12.png)

The presentation slide from Nicolas suggests using LOW LEVEL. However,
noting the winky face, it was clear that using **CreateActCtx** is *not*
the better option when exploiting this bug without a patch.

![](./media/image13.png)

## 

## Using ALPC Attack Vector to Poison the Activation Cache

An **Advanced Local Procedure Call (ALPC)** is a cross-process
communication mechanism used for sending messages at a high speed within
the Windows operating system. Unlike the standard Windows API, **ALPC**
is not directly available to applications. Instead, it is an internal
mechanism that can only be accessed by components of the Windows
operating system. (And us .)

Upon further research, I noticed that some old Cache Poisoning exploits
used **ALPC** to communicate directly with the server. An example can be
seen in Philip Tsukerman’s article, *[Activation Contexts—A Love
Story](https://medium.com/philip-tsukerman/activation-contexts-a-love-story-5f57f82bccd).*

The **CsrClientCallServer** function implements the **ALPC** interface
between Win32 processes and the **CSRSS** process.

So, a call attempt should be made to the **CSRSS** process that acts as
server using **CsrClientCallServer**.

While looking for examples in older exploits, I found a page on Packet
Storm about a [relevant heap buffer overflow
issue](https://packetstormsecurity.com/files/168069/Windows-sxssrv-BaseSrvActivationContextCacheDuplicateUnicodeString-Heap-Buffer-Overflow.html).

When the **CSRSS** server is called with the correct package, it is
received in the **BaseSrvSxsCreateActivationContextFromMessage**
function, which belongs to the module **sxssrv.dll**.

The function has only one argument: the pointer to the received packet.
To reverse it, I created a custom **TotalMessage** structure.

The **TotalMessage** structure packet has its first 0x40 bytes of
**HEADER**, followed by the embedded **Activation Context Message**,
whose structure is **\_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG**.

The **TotalMessage** structure can be seen below:

![](./media/image14.png)

And here is the **\_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG**
structure**:**

![](./media/image15.png)

Inside this structure there are six **UNICODE_STRINGS** corresponding to
the language or **CultureFallbacks**, **AssemblyDirectory**,
**TextualAssemblyIdentity**, **AssemblyName,** and two
**\_BASE_MSG_SXS_STREAM** structures that contain one **UNICODE_STRING**
each one inside.

Below is the **\_BASE_MSG_SXS_STREAM** structure:

![](./media/image16.png)

Given the difficulty in creating a valid packet accepted by the server,
it is worth detailing how to do it.

The **Flags** field value inside
**\_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG** is very important since
there are many combinations. Without the correct flag value, the bug
cannot be taken advantage of.

For example, take my **MsCtfMonitor.dll** code. After many attempts, I
concluded that the one correct **flags** value for this bug exploitation
is **0x41**:

![](./media/image17.png)

The combination of different values could result in a wrong path flag
value:

![](./media/image18.png)

The same **TotalMessage** structure will have a header with
size=**0x40** bytes. The remaining **0x1f8** bytes are reserved for the
**\_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG** structure:

struct TotalMessage

{

signed \_\_int64 pad\[8\];

\_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG message;

};

The size for allocation is **0x40+0x1f8**:

![](./media/image19.png)

I then put together the strings and performed an Activation Cache
Context for **tapi32.dll**. This is a very rarely used **DLL** which is
loaded by a process called **TCMSETUP**. It has a **HIGH PRIVILEGES
INTEGRITY LEVEL** (**RID=0x3000**) with the same privileges as an
Administrator.

![](./media/image20.png)

In my DLL code, the **CaptureUnicodestring** function is called. This
ends up calling **CsrCaptureMessageString:**

NTSTATUS CaptureUnicodeString(LPVOID CaptureBuffer, PSTR OutputString,

PCWSTR String, ULONG Length = 0) {

if (Length == 0) {

Length = lstrlenW(String);

}

return CsrCaptureMessageString(CaptureBuffer, (PCSTR)String, Length \*
2,

Length \* 2 + 2, OutputString);

}

This step is necessary to prepare the package correctly, allowing the
system to copy the strings of my packet to the process **CSRSS**. This
keeps the strings valid and replaces my pointers for valid pointers in
its context.

I also added an **embedded XML manifest**, with the "**Tasks**" language
in it, This is a non-existent language, but it will be the key to
exploitation (Kudos to Nico for this):

![](./media/image21.png)

Another important detail in my code is when **CaptureBuffer** is
created. The function **CsrAllocateCaptureBuffer** has an argument that
defines how many **UNICODE_STRINGS** it should manage and copy to the
server.

In my case I used “4” strings:

![](./media/image22.png)

The argument with value “4” is shown below:

![](./media/image23.png)

To reach the activation server, the **CsrClientCallServer** function
will send my packet from my **MsCtfMonitor.dll** with the same ApiNumber
**0x1001001E** as the old exploits mentioned above.

[Geoff Chappell’s
blog](https://www.geoffchappell.com/studies/windows/win32/ntdll/api/csrutil/clientcallserver.htm)
provides more details on CsrClientCallServer:

![](./media/image24.png)

Here is the call to **CsrClientCallServer**:

![](./media/image25.png)

And here is the package to be sent, built in my DLL:

![](./media/image26.png)

The **Manifest.Offset** value points to my **embedded XML Manifest**:

![](./media/image27.png)

An interesting command to log the activation process is **sxstrace,**
which is used in an administrator console within the target.

This command enable tracing and save the log results in
**sxstrace.etl.** (Press ENTER to finalize tracing.)

**sxstrace trace -logfile:sxstrace.etl**

The raw **sxstrace.etl** file can then be converted into a readable
format:

**sxstrace parse -logfile:sxstrace.etl -outfile:sxstrace.txt**

## 

## Does the System Accept Our Activation Context?

If the package is correct, it should reach the function
**BaseSrvSxsCreateActivationContextFromMessage** in **sxssrv** module of
**csrss** process. So when debugging remote kernel, the context needs to
be switched to this process. Then, the user mode symbols need to be
reloaded to put a breakpoint on it:

![](./media/image28.png)

I used IDA PRO for debugging kernel with the Windbg plugin:

![](./media/image29.png)

Once it stops at **BaseSrvSxsCreateActivationContextFromMessage**, RCX
will point to **TotalMessage** structure:

![](./media/image30.png)

After the initial 0x40 **HEADER** bytes (filled by the system with some
values as the PID of the client process, etc...), my activation message
that belongs to **\_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG** structure
can be seen:

![](./media/image31.png)

Note that the pointers to strings do not have the same value as when I
sent them:

![](./media/image32.png)

But they correctly pointed to the strings:

![](./media/image33.png)

When the packet was sent from client to server, the system copied the
strings from my process to the process **CSRSS** and changed the
pointers in my packet to be valid in its context.

After that, the function
**BaseSrvSxsCreateActivationContextFromMessage** checks if the strings
are valid.

![](./media/image34.png)

In a loop it checks six strings, but it passes the check perfectly. In
my case I only passed four strings and the other two are zero.

After other minor checks, it calls
**BaseSrvSxsCreateActivationContextFromStructEx**, which is the most
important function in the activation process:

![](./media/image35.png)

## How Do You Poison the Activation Cache?

Once getting to **BaseSrvSxsCreateActivationContextFromStructEx**,
**r8** will point to **\_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG,** which
is the activation message:

![](./media/image36.png)

It evaluates the value of flags. In my case, the value was **0x41**
against **0xD**:

![](./media/image37.png)

The **test** function can be bypassed using the flag option that
corresponds to validate the processor architecture (1).

![](./media/image38.png)

After that, it gets the RID of the calling process and stores for
further comparison. In this case, the RID is **0x3000** as **CTFMON**
has HIGH INTEGRITY LEVEL.

![](./media/image39.png)

The most important part of this function is the call to
**BaseSrvActivationContextCacheLookupEntry:**

![](./media/image40.png)

It searches the **Activation Context Cache** to determine if there is
any entry for **tapi32.dll**.

It calls a function named
**BaseSrvActivationContextCacheCompareEntries**, which compares certain
parts of the **Activation Message** entry against all the existing
entries in the cache:

![](./media/image41.png)

It compares the **LastWriteTime** value sent in my packet with the same
value in all entries.

I had previously calculated this value using **GetFileTime** in
**tapi32.dll** and sent it inside my activation package:

![](./media/image42.png)

As there is no entry for **tapi32.dll**, the comparisons will not match.
As expected, it returns error **0xC0000225**. After that it will check
my **ACTX** to see if it’s suitable to be added to the cache:

![](./media/image43.png)

The server needs to read my **XML embedded manifest,** and the
**Manifest.Offset** address that pointed to it. However, in this new
context, it is not yet a valid pointer. It’s worth putting a breakpoint
in this value to see how and when my **XML embedded manifest** is read
using this value.

## How is My Embedded XML Manifest Read?

To verify where **CSRSS** reads my **embedded XML manifest** that was
sent in my **ACTX** request, breakpoints should be put in the
**Manifest.Offset**. Additionally, breakpoints should be added every
time it stops, if it copies to another address.

![](./media/image44.png)

It stops in the breakpoint when reading the **Manifest.Offset** value
address.

It will use this address to read my **embedded XML manifest** from the
**CTFMON** process using **NtReadVirtualMemory**, since the address
placed in the **Manifest.Offset** field belongs to that context:

![](./media/image45.png)

My **embedded XML manifest** is read and copied to the destination
buffer:

![](./media/image46.png)

Switch to the **CTFMON** process context and verify that my **embedded
XML Manifest** is in the **Manifest.Offset** address that I previously
sent. In my case, it was **0x7ff93a261470**.

![](./media/image47.png)

The **embedded XML Manifest** read is called from
**SxSGenerateActivationContext**. As it is not found in any valid entry
in the cache, it tries to “generate” it using the embedded manifest:

![](./media/image48.png)

From there, it starts parsing my **embedded XML Manifest**.

## How is the Embedded XML Manifest Parsed?

Looking in the last call stack, I decided to put a breakpoint in the
call to **RtlReadOutOfProcessMemoryStream** to stop when the buffer was
completely filled.

![](./media/image49.png)

Now a breakpoint can be placed on access on the string "**Tasks**" to
stop when it is read or processed by the server.

![](./media/image50.png)

Here is the **tasks** string inside the embedded XML manifest:

![](./media/image51.png)

It stops several times reading and copying:

![](./media/image52.png)

It stops in **CharEncoder::wideCharFromUtf8** when it converts the
string “**tasks**” to wide char:

![](./media/image53.png)

It then stops in the **XML parser:**

![](./media/image54.png)

It continues parsing the XML attributes, as the function name
**parseAttributes** implies.

![](./media/image55.png)

Then, it stops in **memcpy** called from **ValidateElementAttributes:**

![](./media/image56.png)

Another breakpoint can be placed where it copies:

![](./media/image57.png)

It validates the language attribute, as the function name
**SxspValidateLanguageAttribute** implies:

![](./media/image58.png)

It stops in memcpy again but is called from
**SxspCreateAssemblyIdentityfromIdentityElement**:

![](./media/image59.png)

Once more, it stops in memcpy, this time called from
**SxsInsertAssemblyIdentityAttribute**+0xc48:

![](./media/image60.png)

Then it stops in **SxsInsertAssemblyIdentityAttribute**:

![](./media/image61.png)

It calls memcpy a final time, in this instance from
**BufferedStream::prepairForInput**:

![](./media/image62.png)

It then reads the string **tasks** here:

![](./media/image63.png)

Then, it reads it from here:

![](./media/image64.png)

![](./media/image65.png)

It continues reading from here:

![](./media/image66.png)

![](./media/image67.png)

The names of these functions caught my attention. In the name
**ProbingCandidate**, it includes the same words (**probing manifests**)
used in the **SXS** txt log file.

![](./media/image68.png)

It stops again here:

![](./media/image69.png)

Next, it uses **GetFileAttributesExW** to check if the first file
mentioned in the **SXS** txt log exists**.** Since it does not exist, it
returns zero.

![](./media/image70.png)

The order of the file check can be seen in the log file:

![](./media/image71.png)

The second file does not exist because it is the path in the **tasks**
folder to tapi32.dll:

![](./media/image72.png)

From there, it seems to be “**probing”** for **tapi32.manifest** in
**tasks**:

![](./media/image73.png)

Then it reaches **CProbedAssemblyInformation::ProbeManifestExistence:**

![](./media/image74.png)

![](./media/image75.png)

It checks if my manifest file exists in tasks folder. Since it does
exist, it returns no error:

![](./media/image76.png)

Well, the **tapi32.manifest** in the “**tasks**” folder was found.

The server was forced to search for a manifest file in the “**tasks”**
subfolder of **system32** by my **embedded XML manifest** with the
“**tasks**” language value inside:

![](./media/image77.png)

![](./media/image78.png)

If breakpoints continue to be placed to see where it uses the path, it
stops in **EncodingStream::Read** where the **tapi32.manifest** file
content is read.

![](./media/image79.png)

Next, it will parse the **TAPI32.manifest** file content. If there is an
error it will show it in the **SXS TRACE** log, which makes it easier to
correct.

![](./media/image80.png)

If my **TAPI32.manifest** file is parsed correctly, it will return to
**BaseSrvSxsCreateActivationContextFromStructEx** without error. This
avoids printing a message with the string **FAILED**.

In my case, the Activation Context Generation was successful, using my
**TAPI32.manifest** file**.**

![](./media/image81.png)

![](./media/image82.png)

I then reached the call where my entry will be inserted into the cache.

It is passed without any problem, returning zero. This is the correct
value and the entry with the crafted **TAPI32.manifest** on it is
successfully inserted.

![](./media/image83.png)

My entry is included in the Activation cache and the server returns an
OK response to the call from the DLL from **CTFMON**.

The log txt file shows the complete process.

It reads the **embedded XML manifest**. Since its language is
"**Tasks**" it searches for a new manifest file in the "**Tasks**"
subfolder of **system32**, just as it would search for a manifest in the
subfolder of **system32** called "**en-us**" if the language was set to
"**en-us.**"

![](./media/image84.png)

The SXS log txt file shows the message “**Activation Context generation
succeeded**”!

## How Did My Fake imm32.dll End Up Loaded?

![A pair of boxing gloves Description automatically
generated](./media/image85.png)

After my **ACTX** entry is added to the cache, if **tcmsetup.exe** is
run, it will load **tapi32.dll**, and it should use my manifest file to
load **imm32.dll**.

However, it is not that simple, as it cannot load **imm32.dll** because
there are some checks that can prevent it from loading.

The checks are done on a posterior call to the same
**BaseSrvSxsCreateActivationContextFromStructEx** function, so remove
all the breakpoints and leave just one on it.

From there we can run **TCMSETUP.EXE** from a console, although my PoC
executes TCMSETUP from **MsCtfMonitor.dll** after the Activation Cache
poisoning is completed:

![](./media/image86.png)

It stops on the breakpoint many times. Every time it stops, look at the
structure pointed by r8 to see if it corresponds to a request related to
**tapi32.dll**.

![](./media/image87.png)

After many stops for other modules, a request for TCMSETUP.exe appears:

![](./media/image88.png)

We see in the call stack that it comes from the moment the process is
created. It is called to check if it has any entry in the activation
cache for **TCMSETUP**.

Keep it running until the call arrives for **TAPI32.dll**. Before this
occurs, there will be several calls for **TCMSETUP**.

![](./media/image89.png)

Finally, the packet that arrives must be *very* similar to the one made
previously from my DLL when I inserted the entry in the cache. However,
now it stops when **TCMSETUP** tries to load **TAPI32.dll**.

![](./media/image90.png)

At this point I noticed some important values in this package.

![](./media/image91.png)

Extending from the beginning of the
**\_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG** structure **0x40** bytes
upwards, assign the **TotalMessage** structure. The PID of the process
that makes the request for **TAPI32.dll** is **TCMSETUP** since it wants
to load the DLL.

![](./media/image92.png)

Changing the context to the **TCMSETUP** process, the
**Manifest.Offset** value can be seen pointing to some **embedded XML
manifest**.

![](./media/image93.png)

![](./media/image94.png)

Open tapi32.dll in NOTEPAD to see that the received XML embedded
manifest is the same as the one included in the file.

**TCMSETUP** reads the file resource previously to read the manifest and
put it in the packet as the **embedded XML manifest**.

After that, the comparison is performed again by the
**BaseSrvActivationContextCacheCompareEntries** function, which is
called from **BaseSrvSxsCreateActivationContextFromStructEx**. Now my
entry for tapi32.dll is also in the cache.

![](./media/image95.png)

**BaseSrvActivationContextCacheCompareEntries** is called within a cycle
to compare the actual request with every entry of the **Activation
Context Cache** (as is mine).

At first it compares both **LastWriteTime** values, as they are equal,
it continues comparing more values.

This **LastWriteTime** value is crucial. If there are different values
it will discard my cached entry and my **imm32.dll** will not be loaded.

It goes ahead and stops at the next check.

![](./media/image96.png)

Now, it checks the **ResourceName** value which must be **0x7c** in
both.

![](./media/image97.png)

Then it compares the language of the actual **ACTX** packet, which is
"**en-us**", with the language of my cached entry. The language of my
cached entry is also "**en-us**".

![](./media/image98.png)

My package has the same language value:

![](./media/image99.png)

Then, it compares the processor architecture, which in this instance
will be 9 in both cases:

![](./media/image100.png)

![](./media/image101.png)

Then, it compares both **Manifest.path.**

![](./media/image102.png)

I built the same path without hardcoding by using the System Directory
value:

![](./media/image103.png)

Then it compares the **AssemblyDirectory**, which is also the same:

![](./media/image104.png)

![](./media/image105.png)

If all comparisons are correct, it returns zero. This means that it
found my entry in the **activation cache** and it will be used.

Remember that when I sent my request the first time to add the entry,
the comparison returned an error since there was no entry in the cache
for **TAPI32.dll**. Since my entry was previously added, now it returns
zero.

After that, it compares RIDs of **TCMSETUP** with **CTFMON**, as both
have **RID = 0x3000** the process continues.

A full explanation of the RID patch is available in [a blog from the
Zero Day
Initiative](https://www.zerodayinitiative.com/blog/2023/1/23/activation-context-cache-poisoning-exploiting-csrss-for-privilege-escalation).

![](./media/image106.png)

This is the code for this patch:

![](./media/image107.png)

**R15** has the **RID** of the caller **TCMSETUP = 0x3000** and the
**buffer** has stored the **RID=0x3000** of the **CTFMON** process.

As was stated earlier, Microsoft added this **RID check** patch in
October 2022.

After that patch has been implemented, if you want to try to add the
**tapi32.dll** entry to the cache using the same **MsCtfMonitor.dll**
from a **MEDUIM INTEGRITY LEVEL PROCESS (0x2000)** the entry will be
added to the cache, but it will fail. This is because the RID of the
caller process **0x2000** is stored and when you try to execute
**TCMSETUP** with **RID=0x3000** for load imm32, the **RIDs** are
compared and the entry is removed.

In that hypothetical case, **R15** will have the **RID=0x3000** of the
**TCMSETUP** process that requested to load **tapi32.dll**, the
“**buffer**” variable will have stored the **RID=0x2000** of the process
that added the entry to the cache that has a **MEDIUM INTEGRITY LEVEL**.

![](./media/image108.jpg)

On the newest versions of Windows, cache poisoning will not work if the
process requesting the entry to be added is inferior to the executor and
the entry is removed. Previous versions that were released prior to this
patch will work without problems with any RID.

![](./media/image109.jpg)

Returning to this case, the RID check is passed and both processes have
the same **RID=0x3000**. Consequently, the entry is not deleted and it
continues without any errors.

The server returns the response to TCMSETUP. When it loads
**tapi32.dll** it will use my entry with the **tapi32.manifest** file,
which will load **imm32.dll** from **tasks** folder.

This is the complete way from **LoadLibrary** to where **TCMSETUP**
makes the request to the activation cache

when loading **tapi32.dll**.

![](./media/image110.png)

**BasepCreateActCtx** is the one that makes its request to the **CSRSS**
server. An attempt needs to be made to see when it ends up loading the
**IMM32.dll** module.

Looking at **kernel32.dll**, it calls **CsrBasepCreateActCtxCommon**.
Inside, there is a server call similar to the one made from my **DLL**
to insert my cache entry.

![](./media/image111.png)

It uses the same **ApiNumber** as mine.

When executing **TCMSETUP**, a breakpoint can be placed there when it
returns from the server, after my **tapi32.manifest** file is accepted.

![](./media/image112.png)

This is the entire call stack until the call to the server in
**CsrBasepCreateActCtxCommon** is produced.

![](./media/image113.png)

Breakpoints are placed on the return of some functions of the call
stack.

![](./media/image114.png)

When it stops, it can be observed that **imm32**.**dll** was loaded from
the "**tasks**" folder:

![](./media/image115.png)

Validation can be achieved using **PROCESS MONITOR** that **TCMSETUP**
loads **IMM32.dll** from the "**tasks**" folder.

![](./media/image116.png)

The just executed **CMD** process has **HIGH** privileges.

![](./media/image117.png)

Also, it has the same privileges as **Administrator**.

With these privileges we can now install any program that needs
elevation to administrator and write to any folder. For example, writing
to SYSTEM32 or any program installation folder, as can be seen in the
VIDEO DEMO below.

Here are the privileges previous to the exploitation (Integrity level
Medium not Administrator):

![](./media/image118.png)

And now here are the privileges after the exploitation (Integrity level
High FULL Administrator):

![](./media/image119.png)

At this point it is a good opportunity to easily elevate to SYSTEM,
dropping some crafted DLL into a system folder.

## Video Demo and PoC ![A cartoon of a tv with boxing gloves Description automatically generated](./media/image120.png)![A gold medal with a red ribbon Description automatically generated](./media/image121.png)

[Watch the video here](https://github.com/fortra/CVE-2024-6769/blob/main/CVE-2024-6769.mp4)
and the [functional Proof of
Concept is here](https://github.com/fortra/CVE-2024-6769) 

## TL; DR: Brief description of exploitation steps

- I sent a crafted **ACTX** message to the **CSRSS** server.

- This **ACTX** message had an **embedded XML Manifest** with an
  **offset** that pointed to it.

- When the server received it, it used that offset to read the
  **embedded XML manifest** from the **CTFMON** process context.

- The **embedded XML manifest** was parsed. If accepted, it would try to
  load a second external manifest from an external folder.

- The folder to read depended on the language field in the **embedded
  XML manifest** controlled by me.

- In my case the **embedded XML manifest** had “**tasks**” as its
  language. For that reason, it searched in the “**tasks**” subdirectory
  of system32 for an external manifest and found it.

- It parsed the **tapi32.manifest** file created by me, and accepted it,
  allowing it to load the external **IMM32.dll** from the same
  **“tasks”** folder.

Thanks to Nicolas Economou as his presentation was the starting point
for my research and the publication of this blogpost.

Ricardo Narvaja
