There are new articles available, click to refresh the page.
Before yesterdayiamelli0t’s blog

Analysis of DirectComposition Binding and Tracker object vulnerability

15 August 2021 at 00:00

DirectComposition introduction

Microsoft DirectComposition is a Windows component that enables high-performance bitmap composition with transforms, effects, and animations. Application developers can use the DirectComposition API to create visually engaging user interfaces that feature rich and fluid animated transitions from one visual to another.[1]

DirectComposition API provides COM interface via dcomp.dll, calls win32kbase.sys through win32u.dll export function, and finally sends data to client program dwm.exe (Desktop Window Manager) through ALPC to complete the graphics rendering operation:

win32u.dll (Windows 10 1909) provides the following export functions to handle DirectComposition API:

The three functions related to trigger vulnerability are: NtDCompositionCreateChannel,NtDCompositionProcessChannelBatchBuffer and NtDCompositionCommitChannel:
(1) NtDCompositionCreateChannel creates a channel to communicate with the kernel:

typedef NTSTATUS(*pNtDCompositionCreateChannel)(
	OUT PHANDLE hChannel,
	IN OUT PSIZE_T pSectionSize,
	OUT PVOID* pMappedAddress

(2) NtDCompositionProcessChannelBatchBuffer batches multiple commands:

typedef NTSTATUS(*pNtDCompositionProcessChannelBatchBuffer)(
    IN HANDLE hChannel,
    IN DWORD dwArgStart,
    OUT PDWORD pOutArg1,
    OUT PDWORD pOutArg2

The batched commands are stored in the pMappedAddress memory returned by NtDCompositionCreateChannel. The command list is as follows:


The commands related to trigger vulnerability are: CreateResource, SetResourceBufferProperty, ReleaseResource. The data structure of different commands is different:

(3) NtDCompositionCommitChannel serializes batch commands and sends them to dwm.exe for rendering through ALPC:

typedef NTSTATUS(*pNtDCompositionCommitChannel)(
	IN HANDLE hChannel,
	IN DWORD flag,

CInteractionTrackerBindingManagerMarshaler::SetBufferProperty process analysis

First use CreateResource command to create CInteractionTrackerBindingManagerMarshaler resource (ResourceType = 0x59, hereinafter referred to as “Binding”) and CInteractionTrackerMarshaler resource (ResourceType = 0x58, hereinafter referred to as “Tracker”).
Then call the SetResourceBufferProperty command to set the Tracker object to the Binding object’s BufferProperty.
This process is handled by the function CInteractionTrackerBindingManagerMarshaler::SetBufferProperty, which main process is as follows:

The key steps are as follows:
(1) Check the input buffer subcmd == 0 && bufsize == 0xc
(2) Get Tracker objects tracker1 and tracker2 from channel-> resource_list (+0x38) according to the resourceId in the input buffer
(3) Check whether the types of tracker1 and tracker2 are CInteractionTrackerMarshaler (0x58)
(4) If binding->entry_count (+0x50) > 0, find the matched TrackerEntry from binding->tracker_list (+0x38) according to the handleID of tracker1 and tracker2, then update TrackerEntry->entry_id to the new_entry_id from the input buffer
(5) Otherwise, create a new TrackerEntry structure. If tracker1->binding == NULL || tracker2->binding == NULL, update their binding objects

After SetBufferProperty, a reference relationship between the binding object and the tracker object is as follows:

When use ReleaseResource command to release the Tracker object, the CInteractionTrackerMarshaler::ReleaseAllReferencescalled function is called. ReleaseAllReferences checks whether the tracker object has a binding object internally:

If it has:
(1) Call RemoveTrackerBindings. In RemoveTrackerBindings, TrackerEntry.entry_id is set to 0 if the resourceID in tracker_list is equal to the resourceID of the freed tracker. Then call CleanUpListItemsPendingDeletion to delete the TrackerEntry which entry_id=0 in tracker_list:

(2) Call ReleaseResource to set refcnt of the binding object minus 1.

(3) tracker->binding (+0x190) = 0

According to the above process, the input command buffer to construct a normal SetBufferProperty process is as follows:

After SetBufferProperty, the memory layout of binding1 and tacker1, tracker2 objects is as follows:

After ReleaseResource tracker2, the memory layout of binding1, tacker1, and tracker2 objects is as follows:


Retrospective the process of CInteractionTrackerBindingManagerMarshaler::SetBufferProperty, when new_entry_id != 0, a new TrackerEntry structure will be created:

If tracker1 and tracker2 have been bound to binding1 already, after binding tracker1 and tracker2 to binding2, a new TrackerEntry structure will be created for binding2. Since tracker->binding != NULL at this time, tracker->binding will still save binding1 pointer and will not be updated to binding2 pointer. When the tracker is released, binding2->entry_list will retain the tracker’s dangling pointer.

Construct an input command buffer which can trigger the vulnerability as follows:

Memory layout after ReleaseResource tracker1:

It can be seen that after ReleaseResource tracker1, binding2->track_list[0] saves the dangling pointer of tracker1.


According to analyze the branch of ‘the new_entry_id != 0’, the root cause of CVE-2020-1381 is when creating TrackerEntry, it didn’t check the tracker object which has been bound to the binding object. The patch adds a check for tracker->binding when creating TrackerEntry:

CVE-2021-26900 is a bypass of the CVE-2020-1381 patch. The key point to bypass the patch is if the condition of tracker->binding==NULL can be constructed after the tracker is bound to the binding object.
The way to bypass is in the ‘update TrackerEntry’ branch:

When TrackerEntry->entry_id == 0, RemoveBindingManagerReferenceFromTrackerIfNecessary function is called. It checks if entry_id==0 internally, then call SetBindingManagerMarshaler to set tracker->binding=NULL:

Therefore, by setting entry_id=0 manually, the status of tracker->binding == NULL can be obtained, which can be used to bypass the CVE-2020-1381 patch.

Construct an input command buffer which can trigger the vulnerability as follows:

After setting entry_id=0 manually, the memory layout of binding1 and tracker1:

At this time, binding1->TrackerEntry still saves the pointer of tracker1, but tracker1->binding = NULL. Memory layout after ReleaseResource tracker1: avatar

It can be seen that after ReleaseResource tracker1, binding1->track_list[0] saves the dangling pointer of tracker1.


Retrospective the method in CVE-2021-26900 which sets entry_id=0 manually to get tracker->binding == NULL status to bypass the CVE-2020-1381 patch and the process of CInteractionTrackerMarshaler::ReleaseAllReferences:ReleaseAllReferences which checks that if the tracker object has a binding object, and then deletes the corresponding TrackerEntry.

So when entry_id is set to 0 manually, tracker->binding will be set to NULL. When the tracker object is released via ReleaseResource command, the TrackerEntry saved by the binding object will not be deleted, then a dangling pointer of the tracker object will be obtained again.

Construct an input command buffer which can trigger the vulnerability as follows:

Memory layout after ReleaseResource tracker1:

It can be seen that after ReleaseResource tracker1, binding1->track_list[0] saves the dangling pointer of tracker1.


CVE-2021-33739 is different from the vulnerability in win32kbase.sys introduced in previous sections. It is a UAF vulnerability in dwmcore.dll of the dwm.exe process. The root cause is in ReleaseResource phase of CloseChannel. In CInteractionTrackerBindingManager::RemoveTrackerBinding function call, when the element Binding->hashmap(+0x40) is deleted, the hashmap is accessed directly without checking whether the Binding object is released, which causes the UAF vulnerability.

Construct an input command buffer which can trigger the vulnerability as follows:

According to the previous analysis, in the normal scenario of CInteractionTrackerBindingManagerMarshaler::SetBufferProperty function call, the Binding object should be bound with two different Tracker objects. However, if it is bound with the same Tracker object:

(1) Binding phase:
CInteractionTrackerBindingManager::ProcessSetTrackerBindingMode function is called to process binding opreation, which calls CInteractionTrackerBindingManager::AddOrUpdateTrackerBindings function internally to update Tracker->Binding (+0x278). When Tracker has already be bound with the current Binding object, the binding operation will not be repeated:

Therefore, if the same Tracker object is bound already, the Binding object will not be bound again, then the refcnt of the Binding object will only be increased by 1 finally: avatar

(2) Release phase:
After PreRender is finished, CComposition::CloseChannel will be called to close the Channel and release the Resource in the Resource HandleTable. The Binding object will be released firstly, at this time Binding->refcnt = 1:

Then the Tracker object will be released. CInteractionTrackerBindingManager::RemoveTrackerBindings will be called to release Tracker->Binding:

Three steps are included:
(1) Get the Tracker object from the TrackerEntry
(2) Erase the corresponding Tracker pointer from Binding->hashmap (+0x40)
(3) Remove Tracker->Binding (Binding->refcnt –) from the Tracker object

The key problem is: After completing the cleanup of the first Tracker object in TrackerEntry, the Binding object may be released already. When the second Tracker object is prepared to be cleared, because the Binding object has been released, the validity of the Binding object does not be checked before the Binding->hashmap is accessed again, which result in an access vialation exception:

Exploitation: Another way to occupy freed memory

For the kernel object UAF exploitation, according to the publicly available exploit samples[5], the Palette object is used to occupy the freed memory:

Use CInteractionTrackerBindingManagerMarshaler::EmitBoundTrackerMarshalerUpdateCommands function to access the placeholder objects:

The red box here is the virtual function CInteractionTrackerMarshaler::EmitUpdateCommands of tracker1, tracker2 object (vtable + 0x50). Because the freed Tracker object has been reused by Palette, the program execution flow hijacking is achieved by forging a virtual table and writing other function pointers to fake Tracker vtable+0x50.
The sample selects nt!SeSetAccessStateGenericMapping:

With the 16-byte write capability of nt!SeSetAccessStateGenericMapping, it modifies _KTHREAD->PreviousMode = 0 to inject shellcode into Winlogon process to complete the privilege escalation.

Another way to occupy freed memory

The exploitation of Palette object is relatively common, is there some object with user-mode controllable memory size in the DirectComposition component can be exploited?

The Binding object and Tracker object we discussed before are belonged to the Resource of DirectComposition. DirectComposition contains many Resource objects, which are created by DirectComposition::CApplicationChannel::CreateInternalResource:

Each Resource has a BufferProperty, which is set by SetResourceBufferProperty command. So our object is to find one Resource which can be used to allocate a user-mode controllable memory size through the SetResourceBufferProperty command. Through searching, I found CTableTransferEffectMarshaler::SetBufferProperty. The command format is as follows:

When subcmd==0, the bufferProperty is stored at CTableTransferEffectMarshaler+0x58. The size of the bufferProperty is set by the user-mode input bufferSize, and the content is copied from the user-mode input buffer:

Modify the original sample and use the propertyBuffer of CTableTransferEffectMarshaler to occupy freed memory:

By debugging, we can see that the propertyBuffer of CTableTransferEffectMarshaler occupies the freed memory successfully:

Finally, successful exploitation screenshot:


[1] https://docs.microsoft.com/en-us/windows/win32/directcomp/directcomposition-portal
[2] https://www.zerodayinitiative.com/blog/2021/5/3/cve-2021-26900-privilege-escalation-via-a-use-after-free-vulnerability-in-win32k
[3] https://github.com/thezdi/PoC/blob/master/CVE-2021-26900/CVE-2021-26900.c
[4] https://ti.dbappsecurity.com.cn/blog/articles/2021/06/09/0day-cve-2021-33739/
[5] https://github.com/Lagal1990/CVE-2021-33739-POC

Analysis of Chromium issue 1196683, 1195777

20 April 2021 at 00:00

On April 12, a code commit[1] in Chromium get people’s attention. This is a bugfix for some vulnerability in Chromium Javascript engine v8. At the same time, the regression test case regress-1196683.js for this bugfix was also submitted. Based on this regression test case, some security researcher published an exploit sample[2]. Due to Chrome release pipeline, the vulnerability wasn’t been fixed in Chrome stable update until April 13[3].

Coincidentally, on April 15, another code commit[4] of some bugfix in v8 has also included one regression test case regress-1195777.js. Based on this test case, the exploit sample was exposed again[5]. Since the latest Chrome stable version does not pull this bugfix commit, the sample can still exploit in render process of latest Chrome. When the vulnerable Chormium browser accesses a malicious link without enabling the sandbox (–no-sandbox), the vulnerability will be triggered and caused remote code execution.

RCA of Issue 1196683

The bugfix for this issue is shown as follows:

This commit fixes the issue of incorrect instruction selection for the ChangeInt32ToInt64 node in the instruction selection phase of v8 TurboFan. Before the commit, instruction selection is according to the input node type of the ChangeInt32ToInt64 node. If the input node type is a signed integer, it selects the instruction X64Movsxlq for sign extension, otherwise it selects X64Movl for zero extension. After the bugfix, X64Movsxlq will be selected for sign extension regardless of the input node type.

First, let’s analyze the root cause of this vulnerability via regress-1196683.js:

(function() {
  const arr = new Uint32Array([2**31]);
  function foo() {
    return (arr[0] ^ 0) + 1;
  assertEquals(-(2**31) + 1, foo());
  assertEquals(-(2**31) + 1, foo());

The foo function that triggers JIT has only one code line. Let’s focus on the optimization process of (arr[0] ^ 0) + 1 in the key phases of TurboFan:

1) TyperPhase

The XOR operator corresponds to node 32, and its two inputs are the constant 0 (node 24) and arr[0] (node 80).

2) SimplifiedLoweringPhase

The original node 32: SpeculativeNumberBitwiseXor is optimized to Word32Xor, and the successor node ChangeInt32ToInt64 is added after Word32Xor. At this time, the input node (Word32Xor) type of ChangeInt32ToInt64 is Signed32.

3) EarlyOptimizationPhase

We can see that the original node 32 (Word32Xor) has been deleted and replaced with node 80 as the input of the node 110 ChangeInt32ToInt64. Now the input node (LoadTypedElement) type of ChangeInt32ToInt64 is Unsigned32.

The v8 code corresponding to the logic is as follows:

template <typename WordNAdapter>
Reduction MachineOperatorReducer::ReduceWordNXor(Node* node) {
  using A = WordNAdapter;
  A a(this);

  typename A::IntNBinopMatcher m(node);
  if (m.right().Is(0)) return Replace(m.left().node());  // x ^ 0 => x
  if (m.IsFoldable()) {  // K ^ K => K  (K stands for arbitrary constants)
    return a.ReplaceIntN(m.left().ResolvedValue() ^ m.right().ResolvedValue());
  if (m.LeftEqualsRight()) return ReplaceInt32(0);  // x ^ x => 0
  if (A::IsWordNXor(m.left()) && m.right().Is(-1)) {
    typename A::IntNBinopMatcher mleft(m.left().node());
    if (mleft.right().Is(-1)) {  // (x ^ -1) ^ -1 => x
      return Replace(mleft.left().node());

  return a.TryMatchWordNRor(node);

As the code shown above, for the case of x ^ 0 => x, the left node is used to replace the current node, which introduces the wrong data type.

4) InstructionSelectionPhase
According to the previous analysis, in instruction selection phase, because the input node (LoadTypedElement) type of ChangeInt32ToInt64 is Unsigned32, the X64Movl instruction is selected to replace the ChangeInt32ToInt64 node finally:

Because the zero extended instruction X64Movl is selected incorrectly, (arr[0] ^ 0) returns the wrong value: 0x0000000080000000.

Finally, using this vulnerability, a variable x with an unexpected value 1 in JIT can be obtained via the following code (the expected value should be 0):

const _arr = new Uint32Array([0x80000000]);

function foo() {
	var x = (_arr[0] ^ 0) + 1;
	x = Math.abs(x);
	x -= 0x7fffffff;
	x = Math.max(x, 0);
	x -= 1;
	if(x==-1) x = 0;
	return x;

RCA of Issue 1195777

The bugfix for this issue is shown as follows:

This commit fixes a integer conversion node generation error which used to convert a 64-bit integer to a 32-bit integer (truncation) in SimplifiedLowering phase. Before the commit, if the output type of current node is Signed32 or Unsigned32, the TruncateInt64ToInt32 node is generated. After the commit, if the output type of current node is Unsigned32, the type of use_info is needed to be checked next. Only when use_info.type_check() == TypeCheckKind::kNone, the TruncateInt64ToInt32 node wiill be generated.

First, let’s analyze the root cause of this vulnerability via regress-1195777.js:

(function() {
  function foo(b) {
    let x = -1;
    if (b) x = 0xFFFFFFFF;
    return -1 < Math.max(0, x, -1);

The key code in foo function which triggers JIT is ‘return -1 < Math.max(0, x, -1)’. Let’s focus on the optimization process of Math.max(0, x, -1) in the key phases of TurboFan:

1) TyperPhase
Math.max(0, x, -1) corresponds to node 56 and node 58. The output of node 58 is used as the input of node 41: SpeculativeNumberLessThan (<) .

2) TypedLoweringPhase

The two constant parameters 0, -1 (node 54 and node 55) in Math.max(0, x, -1) are replaced with constant node 32 and node 14.

3) SimplifiedLoweringPhase

The original NumberMax node 56 and node 58 are replaced by Int64LessThan + Select nodes. The original node 41: SpeculativeNumberLessThan is replaced with Int32LessThan. When processing the input node of SpeculativeNumberLessThan, because the output type of the input node (Select) is Unsigned32, the vulnerability is triggered and the node 76: TruncateInt64ToInt32 is generated incorrectly.

The result of Math.max(0, x, -1) is truncated to Signed32. Therefore, when the x in Math.max(0, x, -1) is Unsigned32, it will be truncated to Signed32 by TruncateInt64ToInt32.

Finally, using this vulnerability, a variable x with an unexpected value 1 in JIT can be obtained via the following code (the expected value should be 0):

function foo(flag){
	let x = -1;
	if (flag){ 
		x = 0xFFFFFFFF;
	x = Math.sign(0 - Math.max(0, x, -1));
	return x;

Exploit analysis

According to the above root cause analysis, we can see that the two vulnerabilities are triggered when TurboFan performs integer data type conversion (expansion, truncation). Using the two vulnerabilities, a variable x with an unexpected value 1 in JIT can be obtained.
According to the samples exploited in the wild, the exploit is following the steps below:

1) Create an Array which length is 1 with the help of variable x which has the error value 1;
2) Obtain an out-of-bounds array with length 0xFFFFFFFF through Array.prototype.shift();

The key code is as shown follows:

var arr = new Array(x);	// wrong: x = 1
arr.shift();			// oob
var cor = [1.8010758439469018e-226, 4.6672617056762661e-62, 1.1945305861211498e+103];
return [arr, cor];

The JIT code of var arr = new Array(x) is:

Rdi is the length of arr, which value is 1. It shift left one bit (rdi+rdi) by pointer compression and stored in JSArray.length property (+0xC).

The JIT code of arr.shift() is:

After arr.shift(), the length of arr is assigned by constant 0xFFFFFFFE directly, the optimization process is shown as follows:


The array length assignment operation is mainly composed of node 152 and node 153. The node 152 caculates Array.length-1. The node 153 saves the calculation result in Array.length (+0xC).

Since the value of x which collected by Ignition is 0, constant folding (0-1=-1) happens here to get the constant 0xFFFFFFFF. After shift left one bit, it is 0xFFFFFFFE, which is stored in Array.length (+0xC). Thus, an out-of-bounds array with a length of 0xFFFFFFFF is obtained.

After the out-of-bounds array is obtained, the next steps are common:
3) Realize addrof/fakeobj with the help of this out-of-bounds array;
4) Fake a JSArray to achieve arbitrary memory read/write primitive wth the help of addrof/fakeobj;

The memory layout of arr and cor in exploit sample is:

(1) Use the vulnerability to obtain an arr with the length of 0xFFFFFFFF (red box)
(2) Use the out-of-bounds arr and cor to achieve addrof/fakeobj (green box)
(3) Use the out-of-bounds arr to modify the length of cor (yellow box)
(4) Use the out-of-bounds cor, leak the map and properties of the cor (blue box), fake a JSArray, and use this fake JSArray to achieve arbitrary memory read/write primitive

5) Execute shellcode with the help of WebAssembly;
Finally, a memory page with RWX attributes is created with the help of WebAssembly. The shellcode is copied to the memory page, and executed in the end.

The exploitation screenshot:


[1] https://chromium-review.googlesource.com/c/v8/v8/+/2820971
[2] https://github.com/r4j0x00/exploits/blob/master/chrome-0day/exploit.js
[3] https://chromereleases.googleblog.com/2021/04/stable-channel-update-for-desktop.html
[4] https://chromium-review.googlesource.com/c/v8/v8/+/2826114
[5] https://github.com/r4j0x00/exploits/blob/master/chrome-0day/exploit.js

Exploiting Windows RPC to bypass CFG mitigation: analysis of CVE-2021-26411 in-the-wild sample

10 April 2021 at 00:00

The general method of browser render process exploit is: after exploiting the vulnerability to obtain user mode arbitrary memory read/write primitive, the vtable of DOM/js object is tampered to hijack the code execution flow. Then VirtualProtect is called by ROP chain to modify the shellcode memory to PAGE_EXECUTE_READWRITE, and the code execution flow is jumped to shellcode by ROP chain finally. After Windows 8.1, Microsoft introduced CFG (Control Flow Guard)[1] mitigation to verify the indirect function call, which mitigates the exploitation of tampering with vtable to get code execution.

However, the confrontation is not end. Some new methods to bypass CFG mitigation have emerged. For example, in chakra/jscript9, the code execution flow is hijacked by tampering with the function return address on the stack; in v8, WebAssembly with executable memory property is used to execute shellcode. In December 2020, Microsoft introduced CET(Control-flow Enforcement Technology)[2] mitigation technology based on Intel Tiger Lake CPU in Windows 10 20H1, which protects the exploitation of tampering with the function return address on the stack. Therefore, how to bypass CFG in a CET mitigation environment has become a new problem for vulnerability exploitation.

When analyzing CVE-2021-26411 in-the-wild sample, we found a new method to bypass CFG mitigation using Windows RPC (Remote Procedure Call)[3]. This method does not rely on the ROP chain. By constructing RPC_MESSAGE, arbitrary code execution can be achieved by calling rpcrt4!NdrServerCall2 manually.

CVE-2021-26411 Retrospect

My blog of “CVE-2021-26411: Internet Explorer mshtml use-after-free” has illustrated the root cause: removeAttributeNode() triggers the attribute object nodeValue’s valueOf callback. During the callback, clearAttributes() is called manually, which causes the BSTR saved in nodeValue to be released in advance. After the valueOf callback returns, the nodeValue object is not checked if existed, which results in UAF.

The bug fix for this vulnerability in Windows March patch is to add an index check before deleting the object in CAttrArray::Destroy function:

For such a UAF vulnerability with a controllable memory size, the idea of exploitation is: use two different types of pointers (BSTR and Dictionary.items) to point to the reuse memory, then the pointer leak and pointer dereference ability is achieved via type confusion:

Windows RPC introduction and exploitation

Windows RPC is used to support the scenario of distributed client/server function calls. Based on Windows RPC, the client can invoke server functions the same as local function call. The basic architecture of Windows RPC is shown as follows:

The client/server program passes the calling parameters or return values to the lower-level Stub function. The Stub function is responsible for encapsulating the data into NDR (Network Data Representation) format. Communications through the runtime library is provided by rpcrt4.dll.

An idl example is given below:


interface HelloRPC
	int add(int x, int y);

When the client calls the add function, the server receives the processing request from rpcrt4.dll and calls rpcrt4!NdrServerCall2:

rpcrt4!NdrServerCall2 has only one parameter PRPC_MESSAGE, which contains important data such as the function index and parameters. The server RPC_MESSAGE structure and main sub data structure are shown as follows (32 bits):

As shown in the aforementioned picture, in RPC_MESSAGE structure, the two important variables of function call are Buffer and RpcInterfaceInformation. The Buffer stores the parameters of the function, and RpcInterfaceInformation points to the RPC_SERVER_INTERFACE structure. The RPC_SERVER_INTERFACE structure saves the server program interface information, in which DispatchTable(+0x2c) saves the interface function pointers of the runtime library and the stub function, and InterpreterInfo(+0x3c) points to the MIDL_SERVER_INFO structure. The MIDL_SERVER_INFO structure saves the server IDL interface information, and the DispatchTable(+0x4) saves the pointer array of the server routine functions.

Here is an example to introduce the structure of RPC_MESSAGE:

According to the idl given above, when the client calls add(0x111, 0x222), the server program breaks at rpcrt4!NdrServerCall2: avatar

It can be seen that the dynamic debugging memory dump is consistent with the RPC_MESSAGE structure analysis, and the add function is stored in MIDL_SERVER_INFO.DispatchTable.

Next, we analyze how rpcrt4!NdrServerCall2 invokes the add function according to RPC_MESSAGE:

The rpcrt4!NdrServerCall2 calls rpcrt4!NdrStubCall2 internally. The rpcrt4!NdrStubCall2 calculates the function pointer address based on MIDL_SERVER_INFO.DispatchTable and RPC_MESSAGE.ProcNum, and passes the function pointer, function parameters and parameter length to rpcrt4!Invoke:

The rpcrt4!Invoke calls the server provided routine function finally:

Based on above analysis, after achieving the arbitrary memory read/write primitive, we can construct an fake RPC_MESSAGE, set the function pointer and function parameters want to invoke, and call rpcrt4!NdrServerCall2 manually to implement any function execution.

Two problems need to be solved next:
1)How to invoke rpcrt4!NdrServerCall2 in javascript
2)When observing the server routine function call in rpcrt4!Invoke:

We can see that this is an indirect function call, and there is a CFG check. Therefore, we need to consider how to bypass the CFG protection here after tampering with the MIDL_SERVER_INFO.DispatchTable function pointer.

Let’s solve the problem 1 firstly: How to invoke rpcrt4!NdrServerCall2 in javascript?
We can replace the DOM object vtable’s function pointer with rpcrt4!NdrServerCall2. Because rpcrt4!NdrServerCall2 is a legal pointer recorded in CFGBitmap, it can pass the CFG check. The sample replacs MSHTML!CAttribute::normalize with rpcrt4!NdrServerCall2, and calls “xyz.normalize()” in javascript to invoke rpcrt4!NdrServerCall2.

Then we solve the problem 2: How to bypass the CFG protection in rpcrt4!NdrServerCall2?
The method in the sample is:
1) Use fake RPC_MESSAGE and rpcrt4!NdrServerCall2 to invoke VirtualProtect, and modify the memory attribute of RPCRT4!__guard_check_icall_fptr to PAGE_EXECUTE_READWRITE
2) Replace the pointer ntdll!LdrpValidateUserCallTarget saved in rpcrt4!__guard_check_icall_fptr with ntdll!KiFastSystemCallRet to kill the CFG check in rpcrt4.dll
3) Restore RPCRT4!__guard_check_icall_fptr memory attribute

function killCfg(addr) {
  var cfgobj = new CFGObject(addr)
  if (!cfgobj.getCFGValue()) 
  var guard_check_icall_fptr_address = cfgobj.getCFGAddress()
  var KiFastSystemCallRet = getProcAddr(ntdll, 'KiFastSystemCallRet')
  var tmpBuffer = createArrayBuffer(4)
  call2(VirtualProtect, [guard_check_icall_fptr_address, 0x1000, 0x40, tmpBuffer])
  write(guard_check_icall_fptr_address, KiFastSystemCallRet, 32)
  call2(VirtualProtect, [guard_check_icall_fptr_address, 0x1000, read(tmpBuffer, 32), tmpBuffer])

After solving the two problems, the fake RPC_MESSAGE can be used to invoke any function pointer including the buffer stores the shellcode, because CFG check in rpcrt4.dll has been killed.
At last, the sample writes the shellcode to the location of msi.dll + 0x5000, and invokes the shellcode through rpcrt4!NdrServerCall2 finally:

var shellcode = new Uint8Array([0xcc])
var msi = call2(LoadLibraryExA, [newStr('msi.dll'), 0, 1]) + 0x5000
var tmpBuffer = createArrayBuffer(4)
call2(VirtualProtect, [msi, shellcode.length, 0x4, tmpBuffer])
writeData(msi, shellcode)
call2(VirtualProtect, [msi, shellcode.length, read(tmpBuffer, 32), tmpBuffer])
call2(msi, [])

The exploitation screenshot:

Some thoughts

A new method to bypass CFG mitigation by exploiting Windows RPC exposed in CVE-2021-26411 in the wild sample. This exploitation technology does not need to construct ROP chain, and achieve arbitrary code execution directly by fake RPC_MESSAGE. This exploitation technology is simple and stable. It is reasonable to believe that it will become a new and effective exploitation technology to bypass CFG mitigation.


[1] https://docs.microsoft.com/en-us/windows/win32/secbp/control-flow-guard
[2] https://windows-internals.com/cet-on-windows/
[3] https://docs.microsoft.com/en-us/windows/win32/rpc/rpc-start-page

CVE-2021-1732: win32kfull xxxCreateWindowEx callback out-of-bounds

25 March 2021 at 00:00

CVE-2021-1732 is a 0-Day vulnerability exploited by the BITTER APT organization in one operation which was disclosed in February this year[1][2][3]. This vulnerability exploits a user mode callback opportunity in win32kfull module to break the normal execution flow and set the error flag of window object (tagWND) extra data, which results in kernel-space out-of-bounds memory access violation.

Root cause analysis

The root cause of CVE-2021-1732 is:
In the process of creating window (CreateWindowEx), when the window object tagWND has extra data (tagWND.cbwndExtra != 0), the function pointer of user32!_xxxClientAllocWindowClassExtraBytes saved in ntdll!_PEB.kernelCallbackTable (offset+0x58) in user mode will be called via the nt!KeUserModeCallback callback mechanism, and the system heap allocator (ntdll!RtlAllocateHeap) is used to allocate the extra data memory in user-space.
By hooking user32!_xxxClientAllocWindowClassExtraBytes function in user mode, and modifying the properties of the window object extra data in the hook function manually, the kernel mode atomic operation of allocating memory for extra data can be broken, then the out-of-bounds read/write ability based on the extra data memory is achieved finally.

The normal flow of the window object creation (CreateWindowEx) process is shown as follows (partial):

From the above figure, we can see that: when the window extra data size (tagWND.cbWndExtra) is not 0, win32kfull!xxxCreateWindowEx calls the user mode function user32!_xxxClientAllocWindowClassExtraBytes via the kernel callback mechanism, requests for the memory of the window extra data in user-space. After allocation, the pointer of allocated memory in user-space will be returned to the tagWND.pExtraBytes property:

Here are two modes of saving tagWND extra data address (tagWND.pExtraBytes):
[Mode 1] In user-space system heap
As the normal process shown in the figure above, the pointer of extra data memory allocated in user-space system heap is saved in tagWND.pExtraBytes directly.
One tagWND memory layout of Mode 1 is shown in the following figure:

[Mode 2] In kernel-space desktop heap
The function ntdll!NtUserConsoleControl allocates extra data memory in kernel-space desktop heap by function DesktopAlloc, calculates the offset of allocated extra data memory address to the kernel desktop heap base address, saves the offset to tagWND.pExtraBytes, and modifies tagWND.extraFlag |= 0x800:

One tagWND memory layout of Mode 2 is shown in the following figure: avatar

So we can hook the function user32!_xxxClientAllocWindowClassExtraBytes in user-space, call NtUserConsoleControl manually in hook function to modify the tagWND extra data storage mode from Mode 1 to Mode 2, call ntdll!NtCallbackReturn before the callback returns:

Then return the user mode controllable offset value to tagWND.pExtraBytes through ntdll!NtCallbackReturn, and realize the controllable offset out-of-bounds read/write ability based on the kernel-space desktop heap base address finally.

The modified process which can trigger the vulnerability is shown as follows:

According to the modified flowchart above, the key steps of triggering this vulnerability are explained as follows:

  1. Modify the user32!_xxxClientAllocWindowClassExtraBytes function pointer in PEB.kernelCallbackTable to a custom hook function.
  2. Create some normal window objects, and leak the user-space memory addresses of these tagWND kernel objects through user32!HMValidateHandle.
  3. Destroy part of the normal window objects created in step 2, and create one new window object named ‘hwndMagic’ with the specified tagWND.cbwndExtra. The hwndMagic can probably reuse the previously released window object memory. Therefore, by searching the previously leaked window object user-space memory addresses with the specified tagWND.cbwndExtra in the custom hook function, the hwndMagic can be found before CreateWindowEx returns.
  4. Call NtUserConsoleControl in the custom hook function to modify the tagWNDMagic.extraFlag with flag 0x800.
  5. Call NtCallbackReturn in the custom hook function to assign a fake offset to tagWNDMagic.pExtraBytes.
  6. Call SetWindowLong to write data to the address of kernel-space desktop heap base address + specified offset, which can result in out-of-bounds memory access violation.

An implementation of the hook function is demonstrated as follows:

void* WINAPI MyxxxClientAllocWindowClassExtraBytes(ULONG* size) {

	do {
		if (MAGIC_CBWNDEXTRA  == *size) {
			HWND hwndMagic = NULL;
			//search from freed NormalClass window mapping desktop heap
			for (int i = 2; i < 50; ++i) {
				ULONG_PTR cbWndExtra = *(ULONG_PTR*)(g_pWnd[i] + _WND_CBWNDEXTRA_OFFSET);
				if (MAGIC_CBWNDEXTRA == cbWndExtra) {
					hwndMagic = (HWND)*(ULONG_PTR*)(g_pWnd[i]);
					printf("[+] bingo! find &hwndMagic = 0x%llx in callback :) \n", g_pWnd[i]);
			if (!hwndMagic) {
				printf("[-] Not found hwndMagic, memory layout unsuccessfully :( \n");

			// 1. set hwndMagic extraFlag |= 0x800
			CONSOLEWINDOWOWNER consoleOwner = { 0 };
			consoleOwner.hwnd = hwndMagic;
			consoleOwner.ProcessId = 1;
			consoleOwner.ThreadId = 2;
			NtUserConsoleControl(6, &consoleOwner, sizeof(consoleOwner));

			// 2. set hwndMagic pExtraBytes fake offset
			struct {
				ULONG_PTR retvalue;
				ULONG_PTR unused1;
				ULONG_PTR unused2;
			} result = { 0 };		
			//offset = 0xffffff00, access memory = heap base + 0xffffff00, trigger BSOD	
			result.retvalue = 0xffffff00;			
			NtCallbackReturn(&result, sizeof(result), 0);
	} while (false);

	return _xxxClientAllocWindowClassExtraBytes(size);

BSOD snapshot:

Exploit analysis

From Root cause anaysis, we can see that:
“An opportunity to read/write data in the address which calculated by the kernel-space desktop heap base address + specified offset” can be obtained via this vulnerability.

For the kernel mode exploitation, the attack target is to obtain system token generally. A common method is shown as follows:

  1. Exploit the vulnerability to obtain a arbitrary memory read/write primitive in kernel-space.
  2. Leak the address of some kernel object, find the system process through the EPROCESS chain.
  3. Copy the system process token to the attack process token to complete the privilege escalation job.

The obstacle is step 1: How to exploit “An opportunity to read/write data in the address which calculated by the kernel-space desktop heap base address + specified offset” to obtain the arbitrary memory read/write primitive in kernel-space.

One solution is shown in the following figure:

  1. The offset of tagWNDMagic extra data (wndMagic_extra_bytes) is controllable via the vulnerability, so we can use SetWindowLong to modify the data in specified address calculated by desktop heap base address + controllable offset.
  2. Use the vulnerability ability to modify tagWNDMagic.pExtraBytes to the offset of tagWND0 (the offset of tagWND0 is obtained by tagWND0+0x8), call SetWindowLong to modify tagWND0.cbWndExtra = 0x0fffffff to obtain a tampered tagWND0.pExtraBytes which can achieve read/write out-of-bounds.
  3. Calculate the offset from tagWND0.pExtraBytes to tagWND1, call SetWindowLongPtr to replace the spMenu of tagWND1 with a fake spMenu by the tampered tagWND0.pExtraBytes, realize the arbitrary memory read ability with the help of fake spMenu and function GetMenuBarInfo.
    The logic of GetMenuBarInfo to read the data in specified address is shown as follows, the 16 bytes data is stored into MENUBARINFO.rcBar structure: avatar

  4. Use the tampered tagWND0.pExtraBytes to modify tagWND1.pExtraBytes with specified address, and use the SetWindowLongPtr of tagWND1 to obtain the arbitrary memory write ability.
  5. After obtaining the arbitrary memory read/write primitive, we need to leak a kernel object address in desktop heap to find EPROCESS. Fortunately, when setting the fake spMenu for tagWND1 in step 3, the return value of SetWindowLongPtr is the kernel address of original spMenu, which can be used directly.
  6. Finally, find the system process by traversing the EPROCESS chain, and copy the system process token to the attack process to complete the privilege escalation job. This method is relatively common, so will not be described in detail.

The final privilege escalation demonstration:

Patch analysis



[1] https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-1732
[2] https://ti.dbappsecurity.com.cn/blog/index.php/2021/02/10/windows-kernel-zero-day-exploit-is-used-by-bitter-apt-in-targeted-attack-cn/
[3] https://www.virustotal.com/gui/file/914b6125f6e39168805fdf57be61cf20dd11acd708d7db7fa37ff75bf1abfc29/detection
[4] https://en.wikipedia.org/wiki/Privilege_escalation

CVE-2021-26411: Internet Explorer mshtml use-after-free

12 March 2021 at 00:00

In January of this year, Google and Microsoft respectively published blogs revealing attacks on security researchers by an APT group from NK[1][2]. A vulnerability in Internet Explorer used in this attack was fixed as CVE-2021-26411 in Microsoft’s Patch Tuesday this month[3]. The vulnerability is triggered when users of the affected version of Internet Explorer access a malicious link constructed by attackers, causing remote code execution.

Root cause analysis

The POC which can trigger the vulnerability is shown below:

var elem = document.createElement('xxx'); 
var attr1 = document.createAttribute('yyy'); 
var attr2 = document.createAttribute('zzz'); 

var obj = {};
obj.valueOf = function() {
	return 0x1337;

attr1.nodeValue = obj;
attr2.nodeValue = 123;

Execution process of the PoC:

  1. Create 1 HTML Element object (elem) and 2 HTML Attribute objects (attr1 and attr2)
  2. Assign values to the two Attribute objects nodeValue, where attr1’s nodeValue points to an Object which valueOf function is overloaded
  3. Set the two Attribute objects attr1 and attr2 to Element object elem
  4. Call elem.removeAttributeNode(attr1) to remove the attr1 from elem
  5. The removeAttributeNode method triggers a callback to the valueOf function, during which clearAttributes() is called to clear all the attribute objects (attr1 and attr2) of the elem object
  6. When valueOf callback returns, IE Tab process crashes at null pointer dereference:


Based on the above PoC flow analysis, it can be inferred that the reason of the crash is the Double Free issue caused by the clearAttributes() function in valueOf callback. After clearing all the attribute objects of the element object, it returns to the interpreter (breaking the atomic deletion operation at the interpreter level) and frees the attribute object again which results in Double Free.
But there are still some details need to be worked out:

  1. Why does removeAttributeNode() trigger the valueOf callback in script level?
  2. Why does null pointer dereference exception still occur on DOM objects which are protected by isolated heap and delayed free mechanism?
  3. How to exploit this null pointer dereference exception?

Question 1 is discussed via static analysis firstly:

1.The function removeAttributeNode() in mshtml.dll is handled by MSHTML!CElement::ie9_removeAttributeNode function. It calls MSHTML!CElement::ie9_removeAttributeNodeInternal internally, which is the main implementation of the removeAttributeNode(): avatar

2.MSHTML!CElement::ie9_removeAttributeNodeInternal calls CBase::FindAAIndexNS twice to lookup the indexes of attribute object and attribute nodeValue object in CAttrArray’s VARINAT array (+0x8). avatar

3.When the index of the attribute object is found, the attribute object is retrieved through CBase::GetObjectAt: avatar

4.When the nodeValue index of the attribute object is found, it calls CBase::GetIntoBSTRAt function to convert the nodeValue into BSTR and stores the BSTR value to CAttribute.nodeValue (+0x30). At this time, the valueOf callback will be triggered! avatar

5.Then it calls CBase::DeleteAt twice to delete the attribute object and the attribute nodeValuev object (here need to pay attention to the existence of one CBase::FindAAIndexNS call between the twice DeleteAt to find the attr1.nodeValue index again): avatar

6.CBase::DeleteAt checks the index of the object which needs to be deleted. If it is not equal with -1, it calls CAttrArray::Destroy to perform cleanup work: avatar

7.CAttrArray::Destroy calls CImplAry::Delete to modify the CAttrArray counter (+0x4) and reorder the corresponding VARIANT array (+0x8) , then calls CAttrValue::Free to release the attribute object in the end: avatar

Next we observe the callback process of question 1 and analyze question 2 via dynamic debugging:
1.The memory layout of the elem object before entering the removeAttributeNode function:

2.The result of two CBase::FindAAIndexNS function calls after entering MSHTML!CElement::ie9_removeAttributeNodeInternal: avatar

3.CBase::GetIntoBSTRAt triggers the valueOf callback in the script: avatar

4.The memory layout of the elem object after calling clearAttributes() in the valueOf callback: avatar

Comparing step 1, you can see that after clearAttributes(), the elem.CAttrArray.count (+0x4) is decremented to 1 and a memory copy operation in CAttrArray VARIANT array (+0x8) happens: the attr2 of index 4 is copied to the previous index in order (shift), which corresponding to the logic of CImplAry::Delete: avatar

5.When the callback returns, in the the first CBase::DeleteAt() call:
It checks the index of the VARIANT object which waited to be deleted firstly. Here is the attr1 index value 2 which is found in the first CBase::FindAAIndexNS search: avatar
After passing the index check, CAttrArray::Destroy is called to start cleaning up.

6.When the callback returns, in the the second CBase::DeleteAt() call:
Recalling the static analysis part, one CBase::FindAAIndexNS between the two CBase::DeleteAt is performed to find the attr1.nodeValue index again. Under normal circumstances, CBase::FindAAIndexNS is expected to return 1. However, because the callback breaks the atomic operation at the interpreter level and releases all the attributes of elem in advance, the unexpected -1 is returned here: avatar

According to the static analysis of the CBase::DeleteAt function, for the case where the index is -1, an exception will be thrown: avatar

After returning, the CAttrArray object pointer points to the memory set as NULL, which finally triggers the null pointer dereference exception: avatar

Here is a picture to explain the whole process: avatar

From null pointer dereference to read/write primitive

Finally, let’s address question 3:
How to exploit this null pointer dereference exception?

As we know, null pointer dereference exception in user mode is difficult to exploit, but this vulnerability has its particularity. Through the previous analysis, we know that the null pointer dereference exception occurs in the second DeleteAt operation, but the first DeleteAt operation has an incorrect assumption already: the VARIANT array (+0x8) saved in CAttrArray has been reassigned in the callback:

At this time, the first DeleteAt operation with index = 2 will release the attr2 object by mistake: avatar

So here is actually a UAF issue hidden by the null pointer dereference exception.

The next step needs to think about is how to exploit this UAF. Considering that the DOM element object is protected by isolated heap and delayed free mechanism, it should select an object that can directly allocate memory by the system heap allocator, such as an extremely long BSTR.

The revised PoC is shown as follows:

var elem = document.createElement('xxx'); 
var attr1 = document.createAttribute('yyy'); 
//var attr2 = document.createAttribute('zzz'); 

var obj = {};
obj.valueOf = function() {
	return 0x1337;

attr1.nodeValue = obj;
//attr2.nodeValue = 123;

elem.setAttribute('zzz', Array(0x10000).join('A'));


The memory layout of elem before removeAttributeNode(): avatar

After clearAttributes(), the VARIANT array of CAttryArray is copied forward, and the BSTR is released immediately: avatar

In the first DeleteAt operation, the BSTR memory of the ‘zzz’ attribute with index=2 is accessed again, which results in UAF: avatar

Here we can get a 0x20010 bytes memory hole after the valueOf callback clearAttributes().
Then there are two questions need to answer:

  1. What object can be chosen to occupy the empty memory?
  2. How to bypass the null pointer dereference exception caused by ‘index=-1’ check in the second DeleteAt after the empty memory is occupied successfully?

Referring to the exp codes published by ENKI[4], we can use an ArrayBuffer object with a size of 0x20010 bytes to occupy the empty memory, and re-set the attribute ‘yyy’ to the elem before callback return to bypass of the null pointer dereference exception:

Finally, after ‘elem.removeAttributeNode(attr1)’ returns, a dangling pointer ‘hd2.nodeValue’ with a size of 0x20010 bytes is obtained. There are many path for the next exploitation. The main ideas from the original exp code are:

  1. Use Scripting.Dictionary.items() to occupy the memory hole and use the hd2.nodeValue dangling pointer to leak the fake ArrayBuffer address
  2. Leak the metadata of the fake ArrayBuffer, modify ArrayBuffer’s buffer=0, length=0xffffffff and get new fake ArrayBuffer
  3. Create a DataView which references the new fake ArrayBuffer to achieve arbitrary memory read and write primitive:


Patch analysis


Some thoughts

Microsoft has removed the IE browser vulnerability from the vulnerability bounty program, and began to release the new Edge browser based on the Chromium. However, in recent years, APT actors exploit IE browser vulnerability are still active. From the vbscript engine in 2018, the jscript engine in 2019 to the jscript9 engine in 2020, attackers are constantly looking for new attack surfaces. The disclosure of CVE-2021-26411 brought the security issues of the mshtml engine which had been found with a large number of UAF issues back to the public’s perspective again . We believe that the attacks against Internet Explorer are not stopped.


[1] https://blog.google/threat-analysis-group/new-campaign-targeting-security-researchers/
[2] https://www.microsoft.com/security/blog/2021/01/28/zinc-attacks-against-security-researchers/
[3] https://msrc.microsoft.com/update-guide/en-us/vulnerability/CVE-2021-26411
[4] https://enki.co.kr/blog/2021/02/04/ie_0day.html

  • There are no more articles