返回 TI 主页

Overview

The vulnerability samples for a period of time ago Chianxin Threat Intelligence Center daily in the wild vulnerability monitoring operation was found, its earliest was uploaded when only 6 check.

After analysis it was confirmed that the vulnerability should have been fixed in the August Microsoft patch and is a fixed unknown nday exploit that runs with the specific effects shown below.


Vulnerability Sample Analysis

Here's a first pass at the whole sample, the sample starts by first launching a cmd, after which the core fun_vulstar is called.

The fun_vulstar determines the relevant version of the current machine.

After that, dynamically obtain the function addresses of some system api's.

Open a new thread and call the exploit function fun_expProc.

fun_expProc calls fun_IoRingandPipeinit.

This function determines whether the version of the target system supports the I/O ring lifting method, and if so, completes the related initialization work and returns the

var_ioringRegBuffers/var_ioringRegBuffersCount, this way specific utilization details can see the following article (https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-) full-read-write-exploit-primitive-on-windows-11/), in short this is a kind of Windows 11 22H2+ after the unique utilization of the original language, the Windows kernel can be arbitrary writes or even arbitrary incremental errors into the kernel memory full read/write, in i/o ring In the utilization of i/o ring, the following two fields (var_ioringRegBuffers/var_ioringRegBuffersCount) of the _IORING_OBJECT object are modified by arbitrary address writes, so as to realize global memory read/write.

Afterwards, the first relevant initialization work is done according to whether or not the I/O ring lifting authority is used.

As an example of using the I/O ring lifting method, this case would be to SPREAD 0x2000 length var_ioringRegBuffers-0x2c address on address 0.

Fun_init is used to allocate memory of length 0x10000 at address 0x1000000000 and to get the address var_KWorkerHandleaddr of the WorkerFactory object returned by NtCreateWorkerFactory.

Then go down, into a big loop, where fun_NtAlpcConnectPort is used to call NtAlpcConnectPort to create an Alpc connection object, the connection object is created, open the two threads respectively call the function fun_NtRegisterThreadTerminatePort/fun_ expWorker.

The function of fun_NtAlpcConnectPort is simply to call NtAlpcConnectPort to connect with the system's pdc alpc port service and return the corresponding alpc porthandle.

The following figure, after the two threads are opened, call fun_setEvilmessage to set a section of self-constructed memory, after which the thread 1 corresponding to fun_NtRegisterThreadTerminatePort is monitored by WaitForSingleObject to see if it is finished, and if it is finished, then it enters the part of the red box in the figure, here The core of the program is the function fun_NtCreateEvent.

fun_setEvilmessage completes the construction of a section of memory, which goes into a different version of memory construction depending on the version of the system acquired at the beginning.

The final result is shown below, the constructed memory all starts from the location 66130, here we test the system version of the constructed memory as shown in the red box below, you can see that no matter which version, the final location is placed in the address of var_KWorkerHandleaddr obtained earlier plus an offset.

You can see that after the fun_setEvilmessage call, a section of memory starting at 7FF7F21671B0 is initialized again, and the 7FF7F2166130 constructed in fun_setEvilmessage is placed at location 7FF7F21671D0 at 7FF7F21671B0 +0x20.

7FF7F21671B0 The final memory construct is shown below.

The fun_NtCreateEvent function will go into two branches based on the third parameter, or if non-zero, into the following branch, which loops through the call to NtQueryLicenseValue.

Otherwise into the following branch, you can see that the main core is to call NtCreateEvent, note the second big red box in the same in the setting of the address at 7FF7F21671B0, set the content and the outer function in the same, while 7FF7F21671B0 is set to NtCreateEvent parameters ObjectAttributes. ObjectName.

Next, look at the role of the two threads in detail, thread one call function fun_NtRegisterThreadTerminatePort, the function is very simple, the front of the alpc porthandle var_alpcConnectionHandle create a successful, it is on its call function NtRegisterThreadTerminatePort.

NtRegisterThreadTerminatePort This function is an undisclosed function, but there is a lot of relevant information on the Internet, in short, the function's role is to associate an alpc porthandler with the current thread, when the thread exits, the kernel will have sent a LPC_TERMINATION_MESSAGE to the corresponding alpc service port after the kernel calls NtTerminateThread. LPC_TERMINATION_MESSAGE to the corresponding alpc service port.

actually look at the function, call ObReferenceObjectByHandle to get the porthandle corresponding to the kernel alpcport object, after allocating a length of 0x10 memory pool, the object will be saved in the memory pool 0x8 offset, after that the memory pool and the current thread _ETHREAD object mutually It is interesting to note that the function NtRegisterThreadTerminatePort in k0shl's response to the CVE-2022-22715 vulnerability (https://whereisk0shl.top/post/break-me-out-of-sandbox-in-old-pipe). -cve-2022-22715-windows-dirty-pipe) exploit by k0shl as a utility function to implement object sprays of length 0x20.

This is followed by a second thread calling the function fun_expWorker, which internally calls fun_loopNtSetInformationWorkerFactory based on the tagged bits.

The first call in fun_loopNtSetInformationWorkerFactory is fun_setEvilmessage

After that after that call NtAlpcSendWaitReceivePort, which sends a message to the pdc alpc port service through the pdc porthandler obtained by the NtAlpcConnectPort function earlier, with a message content of v6.

Interestingly when the NtAlpcSendWaitReceivePort call is finished, it seems that the previous WorkerFactory has been modified, which leads to the call of NtSetInformationWorkerFactory through this WorkerFactory can realize arbitrary address writing, the code is divided into two types Utilization, if it is by way of I/O ring, then accordingly by modifying the key var_ioringRegBuffers/var_ioringRegBuffersCount address in the utilization of I/O ring so as to obtain the ability to read and write globally, we can see that The third parameter of NtSetInformationWorkerFactory is the content of the write, and the target address of the write is sprayed on 0x1000000000, that is to say, at this time, through the NtSetInformationWorkerFactory can realize the ability to save and write data based on the range of 0x100000000-0x1000002000. 0x1000002000 range to save the random address of the write, and another way of authorization is to write through the arbitrary address directly modify the PreviousMode, PreviousMode address is also sprayed in the 0x100000000 - 0x1000002000, the NtSetInformationWorkerFactory call to set the PreviousMode after the NtReadVirtualMemory/NtReadVirtualMemory to get the ability to read and write globally.

Modifying the utilization of PreviousMode ends up in fun_eopCmdProcess with NtReadVirtualMemory/NtReadVirtualMemory to implement the lifting.

The I/O ring is utilized in fun_tokenChangewithSystem by replacing the token of the cmd process directly with the token of the system through the global read/write capability.

I/O ring arbitrary address read.

I/O ring arbitrary address write.

Then the deformed WorkerFactory is modified through the write function to facilitate the smooth closing. It can be seen that the modified positions are WorkerFactory-0x28/-0x30.


Detailed vulnerability analysis

By analyzing the above samples, we can basically conclude that the corresponding var_KWorkerHandleaddr kernel object should have been modified after the NtAlpcSendWaitReceivePort call, which leads to the use of that var_KWorkerHandleaddr to call the function NtSetInformationWorkerFactory can be done to write the contents of the pointer on the address range 0x100000000-0x1000002000, but here at the moment is also a guess (just from my years of intuition is very sure), so at this point we summarize the following core issues.

  1. Is var_KWorkerHandleaddr in NtSetInformationWorkerFactory modified, and why would it cause NtSetInformationWorkerFactory to be able to write on the address range of 0x100000000-0x1000002000 on the pointer contents are written.
  2. If var_KWorkerHandleaddr is modified how is this accomplished?
  3. in the case that based on the above two questions holds true, what is the role of NtRegisterThreadTerminatePort/NtAlpcSendWaitReceivePort, our guess is that NtAlpcSendWaitReceivePort leads to a modification of var_ KWorkerHandleaddr modification.
  4. fun_NtCreateEvent What is the role of the large number of NtCreateEvent calls in fun_NtCreateEvent.
  5. What is the purpose of the memory constructed in 7FF72DE66130 and peripheral 7FF72DE671B0 in fun_setEvilmessage?

For the first question we look directly at the implementation of the NtSetInformationWorkerFactory function, here we know that the third parameter of the function is written value, so directly in the function to find the parameter assignment location, you can see that the more reasonable only here, directly under the break.

After running and breaking down, the assignment target rcx is a TpWorkerFactory kernel object through the !object look, its address is also consistent with the exp runtime to get the address of var_KWorkerHandleaddr, you can see that the location of var_KWorkerHandleaddr+0x10 here has been modified to 0x10000000110.

And the location 0x10000000110 was later sprayed on by exp to become var_ioringRegBuffers.

After the assignment is completed var_ioringRegBuffers is modified to fff0000. after that the global read/write atom of I/O Ring is achieved by setting fff0000 to 0. Therefore, it is confirmed here that NtSetInformationWorkerFactory achieves the writing of pointers in the range of 0x100000000-0x1000002000 because the pointer at var_KWorkerHandleaddr+0x10 is set to the range of 0x100000000-0x1000002000. 0x1000002000 location range of the pointer to write, is because the var_KWorkerHandleaddr + 0x10 location of the pointer is set to an address in the interval of 0x100000000-0x1000002000, which is why var_KWorkerHandleaddr needs to be SPRAY to this interval.

The second question is, how is var_KWorkerHandleaddr modified? We directly on the exp to get the var_KWorkerHandleaddr + 0x10 under the memory write breakpoint, run exp after the break as follows, this time is not yet modified before you can see the 0x10 offset at the address through the !object and can not be recognized.

After continuing to run, its modification occurs in the kernel's KeSetEvent function, it should be noted that the modification here is not a one-time event, KeSetEvent execution process of this pointer was modified many times, here only list the more important two times, the following is the first.

Second.

As you can see in ida, you are actually modifying the header in the event object in KeSetEvent, for the first time as follows.

The second modification is as follows, from here it can be confirmed that our var_KWorkerHandleaddr address's object +0xd/var_KWorkerHandleaddr address's object +0x11 was passed directly into the KeSetEvent function to be handled as an event object, which ultimately caused a modification of the pointer at 0x10 of the object at that var_KWorkerHandleaddr address. KWorkerHandleaddr address of the object at 0x10 pointer modification, due to the var_KWorkerHandleaddr address of the object is not consistent each time, so the pointer at 0x10 is also changing, which causes the pointer at 0x10 was eventually modified address is an interval value (in the range of 0x100000000 - 0x1000002000), therefore, the pointer at 0x10 was modified address is an interval value (in the range of 0x100000000 - 0x1000002000), therefore, it is not a good idea to modify the pointer. 0x1000002000), so write the target address needs to be sprayed in the interval.

At this point the stack when calling KeSetEvent is as follows, you can see that the source of its call is exactly NtAlpcSendWaitReceivePort, so the previous guess will not have any problem, due to the vulnerability caused by the NtAlpcSendWaitReceivePort modified var_KWorkerHandleaddr address of the object, thus allowing writes to save pointers at any location in the range 0x100000000-0x1000002000 of the NtSetInformationWorkerFactory implementation.

The full call stack is shown below

So what kind of vulnerability causes NtAlpcSendWaitReceivePort can modify the var_KWorkerHandleaddr address of the object? From the above analysis can be basically confirmed and NtRegisterThreadTerminatePort/NtAlpcSendWaitReceivePort these two alpc functions related to the simplest way of analyzing the idea that the direct reverse derivation, monitoring and debugging NtAlpcSendWaitReceivePort to the KeSetEvent of the entire process can know var_KWorkerHandleaddr object is how to realize the modification of the object, but before this we need to have an understanding of the mechanism of ALPC in Windows.

ALPC

ALPC is a fast, powerful and very widely used inter-process communication mechanism in the Windows operating system (internally). The main component of ALPC communication is the ALPC Port object. The ALPC port object is a kernel object whose use is similar to the use of network sockets, in which the server opens a socket that the client can connect to in order to exchange messages. The ALPC communication scenario involves three ALPC port objects, the first of which is the ALPC Connection port created by the server process to which the client can connect (similar to a network socket) The first is the Connection port (similar to a network socket) created by the server process to which clients can connect. Once the client connects to the server's ALPC connection port, the kernel creates two new ports called ALPC Server Communication Port and ALPC Client Communication Port.

Once the server and client communication ports are established, both sides can use the ntdll.dll public function NtAlpcSendWaitReceivePort to send messages to each other, and the client can use the function NtAlpcConnectPort to start the connection once, so as the client's use of the following two functions will be enough.

  • NtAlpcConnectPort
  • NtAlpcSendWaitReceivePort

The first is NtAlpcConnectPort, this function is used to connect alpc server side, after a successful call will return a PortHandle, which in the kernel is the ALPC client communication port mentioned earlier.

After completing Connect and obtaining the corresponding portHandle, you can send and receive messages through the NtAlpcSendWaitReceivePort, where you need to pay attention to the function can be sent and received at the same time, in addition, the client sends a message through the function is not sent directly to the server, which needs to be carried out through the kernel for a layer of In addition, messages sent by the client through this function are not sent directly to the server, they need to be forwarded through the kernel, which is responsible for routing all messages, placing them in the message queue, notifying all parties of the message received, and verifying the message and its properties, among other things.

As shown below you can see the NtAlpcSendWaitReceivePort function call stack that triggers the modification of the object with the var_KWorkerHandleaddr address is divided by the red line, first is the send message portion of the NtAlpcSendWaitReceivePort, and after that, it notifies the corresponding The pdc alpc port service actually handles the program pdc.sys, and completes the related processing in pdc.sys, so we directly skip NtAlpcSendWaitReceivePort and enter pdc to see how pdc.sys handles the received message.

First of all, the core function of processing alpc in pdc is in PdcpAlpcProcessMessages, which is a while loop, and its internal call ZwAlpcSendWaitReceivePort accepts the message from the kernel, ZwAlpcSendWaitReceivePort is a wrapper around NtAlpcSendWaitReceivePort, which we mentioned earlier. ZwAlpcSendWaitReceivePort is a wrapper for NtAlpcSendWaitReceivePort, as we mentioned earlier, the mechanism of alpc sends and receives messages through the function NtAlpcSendWaitReceivePort, and sends and receives messages not directly to the kernel for routing, and ultimately in the PdcProcessMessage. for message processing, its two parameters are ReceiveMessage; MessageAttribute, we combined with the previous call stack to see how the var_KWorkerHandleaddr address is passed into the modified, here in the comments have been given the answer is in the poi (poi (poi (poi ( MessageAttribute)+0x20)+0x20)+0x6c8) which comes from MessageAttribute. messageAttribute is then poi(ReceiveMessageAttributes(v5)+8) position.

Let's actually take a look at the whole incoming process, the function PdcProcessMessage calls PdcProcessReceivedUserMessage.

Call PdcpTaskClientReceive in PdcProcessReceivedUserMessage.

PdcpDereferenceTaskClient is called in PdcpTaskClientReceive.

PdcpTaskClientAcknowledge is called in PdcpDereferenceTaskClient.

PdcSendKernelMessage is called in PdcpTaskClientAcknowledge.

PdcPortQueueMessage is called in PdcSendKernelMessage.

KeSetEvent is called in PdcPortQueueMessage and eventually the incoming poi(poi(poi(poi(poi(MessageAttribute)+0x20)+0x20)+0x6c8) will be modified.

See here careful readers may find that there are problematic places, that is, MessageAttribute is how to come, to know that our utilization of the sample call NtAlpcSendWaitReceivePort only the first three parameters, and only set the SendMessage, and the corresponding SendMessageAttributes parameter is empty, why we can receive the corresponding v5 ReceiveMessageAttributes in PdcpAlpcProcessMessages, and also extract the MessageAttribute from it. Where does MessageAttribute come from?

This question actually started to bother me for a long time, but this is actually a misunderstanding of thinking, we do not set the corresponding SendMessageAttributes when we send, but because the sender and receiver in alpc are not directly docked, here the receiver docked is actually the kernel, and the pdc receiver in the acceptance of ZwAlpcSendWaitReceivePort is set with the corresponding ReceiveMessageAttributes, so this parameter will be generated by the kernel during kernel routing.

Here we look at the acceptance branch code of NtAlpcSendWaitReceivePort, we can see that the premise of the AlpcpExposeAttributes call is to determine whether the ReceiveMessageAttributes exist or not, and ZwAlpcSendWaitReceivePort in pdc sets this parameter, so the kernel will automatically set the corresponding ReceiveMessageAttributes when routing this message. ZwAlpcSendWaitReceivePort in pdc sets this parameter, so the kernel will automatically set the corresponding ReceiveMessageAttributes when it routes this message.

The call stack for this procedure is as follows:

After figuring out where the messageattribute comes from, we now need to identify how poi(poi(poi(poi(poi(MessageAttribute)+0x20)+0x20)+0x6c8) has been modified. Through the above analysis we can confirm that the problem should not be in the location of NtAlpcSendWaitReceivePort, in this case there is only another function, that is, NtRegisterThreadTerminatePort.

Here we found through testing that the exploit sample will fail after installing the August 2024 patch, for this reason we compared the Windows kernel files from July/August 2024 through bindiff and found that the NtRegisterThreadTerminatePort function used by the exploit sample was removed in the newer version of the kernel files!

The role of this function, as previously analyzed, is to associate an alpc porthandler with the current thread, when the kernel calls NtTerminateThread will have sent a LPC_TERMINATION_MESSAGE to the corresponding port, the call logic is as follows

Will eventually call PspExitThread, PspExitThread has the following processing, the function will look at the current thread and get the alpc port previously bound through the NtRegisterThreadTerminatePort corresponding to the kernel object, and through the function LpcRequestPort to the corresponding alpc server (which is the pdc alpc port service in the utilization code) with a message that begins with 300008006, which is the LPC_TERMINATION_MESSAGE described earlier.

LpcRequestPort is shown below, and the final send is realized through AlpcpSendMessage, in fact, Lpc is a mechanism for internal processes to communicate in Windows before Vista, and was replaced with the more efficient Alpc after Vsita, in order to maintain compatibility, you can see that all Lpc calls essentially end up turning to the Alpc

And our alpc porthandler here is actually the same alpc port corresponding to the pdc alpc port service, whose corresponding driver is pdc.sys.

And into the PdcProcessMessage, which has a branch for processing LPC_TERMINATION_MESSAGE, as follows its judgment is exactly what we have just sent the message 300008006 in the +4 of the 6 position, and here PdcFreeClient will be used to release poi(poi( MessageAttribute)+0x20), and the location of the release should be occupied by exp, and modified to a malicious memory, the malicious memory poi(poi(evil+0x20)+0x6c8) points to a section of the var_KWorkerHandleaddr, and thus in the function KeSetEvent pass poi(poi(poi(poi(poi(MessageAttribute)+0x20)+0x20)+0x6c8) in the function KeSetEvent and modify the

Then our next problem is that we need to make sure

  1. Whether the release was caused by PdcFreeClient and reused afterwards
  2. If question 1 is valid, what is this freed memory, how is it generated, and why is it not modified between the LPC_TERMINATION_MESSAGE message sent by the system and the message we send via NtAlpcSendWaitReceivePort?
  3. How to achieve the memory occupation, our guess is NtCreateEvent, after all, the code in the middle of NtCreateEvent spray operation is too obvious.

When the corresponding binding thread exits, the call to LpcRequestPort is triggered, and the kernel sends an LPC_TERMINATION_MESSAGE message beginning with 300008 to the corresponding pdc alpc port service.

The pdc alpc port service processes accepted messages in the PdcpAlpcProcessMessages function in pdc.sys, as mentioned earlier, messages in alpc are routed by the kernel, and here ZwAlpcSendWaitReceivePort is called to accept the message, and since here ReceiveMessageAttributes(v5) is specified in ZwAlpcSendWaitReceivePort, so the kernel generates that data when it routes the message, even if the actual sender sends it without sending it.

Before calling ZwAlpcSendWaitReceivePort in PdcpAlpcProcessMessages, create an object of ReceiveMessageAttributes through AlpcInitializeMessageAttribute.

The ZwAlpcSendWaitReceivePort call actually goes to the NtAlpcSendWaitReceivePort in the kernel anyway, and goes to AlpcpReceiveMessage and calls AlpcpReceiveMessagePort.

As shown below, the core of AlpcpReceiveMessagePort is to return the _KALPC_MESSAGE corresponding to the received message.

The corresponding server connection port port object here is shown below.

The overall structure of nt!_ALPC_PORT is as follows.

0: kd> dt nt\!\_ALPC\_PORT  
   \+0x000 PortListEntry : \_LIST\_ENTRY  
   \+0x010 CommunicationInfo : Ptr64 \_ALPC\_COMMUNICATION\_INFO  
   \+0x018 OwnerProcess : Ptr64 \_EPROCESS  
   \+0x020 CompletionPort : Ptr64 \_KQUEUE  
   \+0x028 CompletionKey : Ptr64 Void  
   \+0x030 CompletionPacketLookaside : Ptr64 \_ALPC\_COMPLETION\_PACKET\_LOOKASIDE  
   \+0x038 PortContext : Ptr64 Void  
   \+0x040 StaticSecurity : \_SECURITY\_CLIENT\_CONTEXT  
   \+0x088 IncomingQueueLock : \_EX\_PUSH\_LOCK  
   \+0x090 MainQueue : \_LIST\_ENTRY  
   \+0x0a0 LargeMessageQueue : \_LIST\_ENTRY  
   \+0x0b0 PendingQueueLock : \_EX\_PUSH\_LOCK  
   \+0x0b8 PendingQueue : \_LIST\_ENTRY  
   \+0x0c8 DirectQueueLock : \_EX\_PUSH\_LOCK  
   \+0x0d0 DirectQueue : \_LIST\_ENTRY  
   \+0x0e0 WaitQueueLock : \_EX\_PUSH\_LOCK  
   \+0x0e8 WaitQueue : \_LIST\_ENTRY  
   \+0x0f8 Semaphore : Ptr64 \_KSEMAPHORE  
   \+0x0f8 DummyEvent : Ptr64 \_KEVENT  
   \+0x100 PortAttributes : \_ALPC\_PORT\_ATTRIBUTES  
   \+0x148 ResourceListLock : \_EX\_PUSH\_LOCK  
   \+0x150 ResourceListHead : \_LIST\_ENTRY  
   \+0x160 PortObjectLock : \_EX\_PUSH\_LOCK  
   \+0x168 CompletionList : Ptr64 \_ALPC\_COMPLETION\_LIST  
   \+0x170 CallbackObject : Ptr64 \_CALLBACK\_OBJECT  
   \+0x178 CallbackContext : Ptr64 Void  
   \+0x180 CanceledQueue : \_LIST\_ENTRY  
   \+0x190 SequenceNo : Int4B  
   \+0x194 ReferenceNo : Int4B  
   \+0x198 ReferenceNoWait : Ptr64 \_PALPC\_PORT\_REFERENCE\_WAIT\_BLOCK  
   \+0x1a0 u1 :  <unnamed\-tag>
   \+0x1a8 TargetQueuePort : Ptr64 \_ALPC\_PORT  
   \+0x1b0 TargetSequencePort : Ptr64 \_ALPC\_PORT  
   \+0x1b8 CachedMessage : Ptr64 \_KALPC\_MESSAGE  
   \+0x1c0 MainQueueLength : Uint4B  
   \+0x1c4 LargeMessageQueueLength : Uint4B  
   \+0x1c8 PendingQueueLength : Uint4B  
   \+0x1cc DirectQueueLength : Uint4B  
   \+0x1d0 CanceledQueueLength : Uint4B  
   \+0x1d4 WaitQueueLength : Uint4B

AlpcpReceiveMessagePort retrieves messages from the _ALPC_PORT object in the message queue MainQueue.

The message in the message queue is the nt!_KALPC_MESSAGE object, as shown below you can see that the removed message object +0xf0 position is exactly the 3000008 message entity that was sent.

The structure of _KALPC_MESSAGE is shown below, starting at 0x68 are the MessageAttributes and 0xf0 is the corresponding message entity.

After that, make some settings for that _KALPC_MESSAGE and jump to Label_19.

The AlpcpReceiveMessagePort function finally returns that _KALPC_MESSAGE via a4.

As shown below, the returned _KALPC_MESSAGE.

Since ZwAlpcSendWaitReceivePort in PdcpAlpcProcessMessages sets the ReceiveMessageAttributes parameter, which is a4 in this place, it enters the function AlpcpExposeAttributes.

The parameters for the AlpcpExposeAttributes function call are shown below, and it should be noted that a2=0, a3 is the _KALPC_MESSAGE object returned by the previous AlpcpReceiveMessagePort, a4=2000000, a5 is the ReceiveMessageAttributes.

So here AlpcpExposeAttributes goes directly to the position in the red box below after the judgment of a2, a4.

After that, ReceiveMessageAttributes will be set, whose data is originally the data in the _KALPC_MESSAGE object.

As shown below rcx is ReceiveMessageAttributes+8.

The core of this assignment is the ReceiveMessageAttributes+8 position assignment, you can see that here passed in _KALPC_MESSAGE->MessageAttributes->PortContext.

PortContext is set to ReceiveMessageAttributes+8.

The ReceiveMessageAttributes+8 that completes the setup is shown below.

You can see that this ReceiveMessageAttributes is set as follows

The ZwAlpcSendWaitReceivePort call in PdcpAlpcProcessMessages will return, at this time the incoming function PdcProcessMessage of the second parameter MessageAttribute is ReceiveMessageAttributes + 8, the first parameter is ReceiveMessageAttributes + 8. The first parameter is 300008 message entity, as previously analyzed, the message entity +0x4 position at the 6 will lead to enter the PdcFreeClient.

PdcFreeClient will release poi(poi(poi(MessageAttribute)+0x20)+0x20) and poi(poi(MessageAttribute)+0x20) in sequence.

As shown below poi(poi(poi(MessageAttribute)+0x20)+0x20) actually points to poi(MessageAttribute), so what is actually released twice here is poi(MessageAttribute) and poi(poi(MessageAttribute ) + 0x20).

The first poi released (poi(MessageAttribute)+0x20), shown below, has a pool of size 0x50.

This is followed by poi(MessageAttribute).

The size of poi(MessageAttribute) is also 0x50.

Both locations are released when PdcFreeClient returns.

The 3000008 message leads to the call stack on release.

So here we understand the core of the vulnerability, NtRegisterThreadTerminatePort will bind the current var_alpcConnectionHandle to the current thread CreateThread1, and when CreateThread1 exits, the kernel in the kernel will get the thread object through this thread to get the When CreateThread1 exits, the kernel will get the alpc port kernel object corresponding to var_alpcConnectionHandle through the thread object, and send an LPC_TERMINATION_MESSAGE message to the pdc alpc port, and PdcpAlpcProcessMessages will call LPC_TERMINATION_MESSAGE message when it processes the LPC_TERMINATION_MESSAGE message. MESSAGE message, PdcpAlpcProcessMessages will call ZwAlpcSendWaitReceivePort to get the message, because of the ReceiveMessageAttributes parameter passed in the function, so the kernel will generate the corresponding ReceiveMessageAttributes when routing the message. The position of ReceiveMessageAttributes+8 will be set to KALPC_MESSAGE->MessageAttributes->PortContext of the message, and ZwAlpcSendWaitReceivePort returns to the PdcProcessMessage to process the LPC_TERMINATION_MESSAGE message and eventually calls PdcFreeClient to release the KALPC_MESSAGE->MessageAttributes->PortContext that ReceiveMessageAttributes+8 points to. In what way is the released KALPC_MESSAGE->MessageAttributes->PortContext reused? The answer is through the NtCreateEvent spray, here directly on the release address under the write breakpoint, you can see that the NtCreateEvent ultimately call ObpLookupObjectName, and through the ExAlloctePool to complete the reuse of the released pool.

The actual location shown below is as follows, and the ObjectAttributes.ObjectName set in the NtCreateEvent call is copied to this release address in the memove afterwards, while the content of ObjectAttributes.ObjectName at this point was set to point to the maliciously constructed evil message PortContext.

As you can see below at this point poi(MessageAttribute)+0x20 writes the address of our evil message PortContext.

The following is the function call stack when reused

And here in fun_NtCreateEvent are two sets of schemes that occupy reuse. In addition to NtCreateEvent, there is an NtQueryLicenseValue under it.

NtQueryLicenseValue here is also through the first parameter passed to allocate a section of 0x40 pools, just can occupy the released PortContent memory, after the contents of the 7FF72DE671B0 will be written to the section of the pools, its 0x20 position is exactly Evil message portcontent, but in the actual utilization, this function basically do not need to use, exp code, even if it will directly call the patch off, will not affect the actual utilization of the effect.

After NtCreateEvent completes the reuse of the released PortContext, exp uses this var_alpcConnectionHandle to call NtAlpcSendWaitReceivePort, as shown below for the message received by the kernel routed at this point in the PdcProcessMessage call. exp sends message 30002d8, which is also received from the kernel via ZwAlpcSendWaitReceivePort, and because ReceiveMessageAttributes is set, the message 300002d8 here also returns ReceiveMessageAttributes.

Since they are all returned by the kernel's connection port, KALPC_MESSAGE->MessageAttributes->PortContext is the same even though KALPC_MESSAGE->MessageAttributes->PortContext in Mainqueus is different, and PortContext has already been released after processing of the previous The PortContext has been released after the processing of the previous 300008 message and is reused by NtCreateEvent to write the constructed evil message PortContext.

You can see that at this point PortContext+0x20 points to the evil message PortContext, and the location evilmessage+0x1798 holds the address of var_KWorkerHandleaddr + 0xD

And the actual addressing follows poi(poi(poi(poi(poi(MessageAttribute) + 0x20) + 0x20) + 0x6c8). Eventually the location where var_KWorkerHandleaddr + 0xD is saved is found.

It's also the same evil message PortContext we constructed at the beginning.

The corresponding PortContext is 0xffffe30b16a0d850 at the time of the 30000008 message as seen below.

And also at the 30000d28 message PortContext remains 0xffffe30b16a0d850, which is why UAF reuse is guaranteed.

300002d8 message is processed, the pointer at var_KWorkerHandleaddr+0x10 will be subsequently modified by this evil message portContext, the details of which have been analyzed earlier, the specific call stack is as follows, and the modification will eventually be completed in KeSetEvent.

Here each call just completes four bytes of modification, so to complete the modification of the 8 byte length pointer needs to be triggered twice, which is why var_countsForintoLoopWorkerFactory is set to ensure that NtAlpcSendWaitReceivePort is called more than twice.

The first time you modify the four bytes, you can see that at this point the four bytes starting at var_KWorkerHandleaddr+0xd are set in evilmessage.

Send the 3000002d8 message a second time.

You can see that the second time, the four bytes starting at var_KWorkerHandleaddr+0x11 are modified, and the pointer is eventually controlled to be in the range 0x100000000-0x1000002000 by KeSetEvent.

Eventually in the target write address spray in the range of 0x100000000-0x1000002000, through the call to realize the modification of the i/o ring, so as to obtain any address read and write atoms.


summarize

The entire process of utilization is shown below:

1. Call NtAlpcConnectPort to connect to the pdc alpc port service and get a var_alpcConnectionHandle.

2. Call NtRegisterThreadTerminatePort in thread 1 to bind var_alpcConnectionHandle to thread 1's _ETHREAD kernel object.

3.1 Monitor thread 1. When thread 1 exits, the PspExitThread call in the kernel, bound on the _ETHREAD kernel object var_alpcConnectionHandle kernel object will call LpcRequestPort to send an LPC_TERMINATION_ to the pdc port server. MESSAGE message to the pdc port server.

3.2 The pdc server handles related messages through the PdcpAlpcProcessMessages function, in which the reception of kernel-routed alpc messages is realized through ZwAlpcSendWaitReceivePort, which is called with the parameter ReceiveMessageAttributes set, which will result in a ZwAlpcSendWaitReceivePort->NtAlpcSendWaitReceivePort->AlpcpReceiveMessage->AlpcpExposeAttributes call via the AlpcpReceiveMessagePort to get the _KALPC_MESSAGE of the message and set the corresponding ReceiveMessageAttributes, here the position of ReceiveMessageAttributes+8 will be set to _KALPC_MESSAGE. MessageAttributes.PortContext, the value and connection port binding, that is, at this time all the received messages in the _KALPC_MESSAGE.MessageAttributes.PortContext are fixed pointers.

3.3 Call PdcProcessMessage to process the message and eventually release the _KALPC_MESSAGE.MessageAttributes.PortContext pointer saved by ReceiveMessageAttributes+8 in PdcFreeClient.

4. Ensure that ReceiveMessageAttributes->_KALPC_MESSAGE.MessageAttributes.PortContext release, the cycle call NtCreateEvent, here will be its parameters ObjectAttributes. ObjectName is set to 7FF72DE671B0, and the evil message PortContext 7FF72DE66130 is saved at 7FF72DE671B0+0x20, which is eventually called by NtCreateEvent and passed to ObpLookupObjectName by the ExAllocatePool2 occupies the freed ReceiveMessageAttributes->_KALPC_MESSAGE.MessageAttributes.PortContext memory and subsequently passes memory to ObjectAttributes. ObjectName set in 7FF72DE66130 to the ReceiveMessageAttributes->_KALPC_MESSAGE.MessageAttributes.PortContext section of memory +0x20 to reuse and modify.

5.1 In thread 2, after ensuring that NtCreateEvent is occupied and ReceiveMessageAttributes->_KALPC_MESSAGE.MessageAttributes.PortContext+0x20 points to the evil message After PortContext 7FF72DE66130, call NtAlpcSendWaitReceivePort via var_alpcConnectionHandle to send a message 30002d8 to the pdcport server side

5.2 Similar to the previous 3000008 LPC_TERMINATION_MESSAGE message processing, at this time through the ZwAlpcSendWaitReceivePort from the kernel to obtain ReceiveMessageAttributes, ReceiveMessageAttributes + 8 of the MessageAttributes.PortContext, since this pointer is the same for all _KALPC_MESSAGEs under the same connection port, the returned _KALPC_MESSAGE. MessageAttributes.PortContext, where the 0x20 offset has been modified in Part IV evil message PortContext 7FF72DE66130.

5.3 PdcProcessMessage processes message 30002d8, which eventually causes var_KWorkerHandleaddr at poi(poi(poi(poi(poi(poi(ReceiveMessageAttributes+8))+0x20)+0x20)+0x6c8) + 0xd/0x11 at KeSetEvent is set, and after two NtAlpcSendWaitReceivePort calls (each modifying 4 bytes) the pointer at var_KWorkerHandleaddr + 0x10 is modified to a value in the range of 0x100000000-0x1000002000. Here we are grabbing the memory released by the poi(poi(poi(poi(poi(poi(ReceiveMessageAttributes+8))+0x20)+0x20)+0x6c8) red pointer, poi(poi(poi(poi(poi(poi(ReceiveMessageAttributes+8))+0x20)+0x20)+0x6c8), and the replacement is the blue pointer, which is set to the evil message PortContext.

6. The pointer at var_KWorkerHandleaddr+0x10 is modified to a value in the range of 0x100000000-0x1000002000, and by SPRAYing the target write address on an address in that range, using var_KWorkerHandleaddr to call the NtSetInformationWorkerFactory, it will be possible to obtain the ability to write to an arbitrary address at a time, through which the ability to modify the i/o ring/PreviousMode is ultimately provided in two ways of lifting power.


Reference Links

[1]. https://github.com/avalon1610/ALPC/blob/master/ALPC/ntlpcapi.h

[2]. https://whereisk0shl.top/post/break-me-out-of-sandbox-in-old-pipe-cve-2022-22715-windows-dirty-pipe

[3].https://recon.cx/2008/a/thomas_garnier/LPC-ALPC-slides.pdf

[4].https://csandker.io/2022/05/24/Offensive-Windows-IPC-3-ALPC.html

[5].https://i.blackhat.com/Asia-22/Friday-Materials/AS-22-Xu-The-Next-Generation-of-Windows-Exploitation-Attacking-the-Common-Log-File-System.pdf

VULNERABILITIES WINDOWS