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:
<script>
var elem = document.createElement('xxx');
var attr1 = document.createAttribute('yyy');
var attr2 = document.createAttribute('zzz');
var obj = {};
obj.valueOf = function() {
elem.clearAttributes();
return 0x1337;
};
attr1.nodeValue = obj;
attr2.nodeValue = 123;
elem.setAttributeNode(attr1);
elem.setAttributeNode(attr2);
elem.removeAttributeNode(attr1);
</script>
Execution process of the PoC:
- Create 1 HTML Element object (elem) and 2 HTML Attribute objects (attr1 and attr2)
- Assign values to the two Attribute objects nodeValue, where attr1’s nodeValue points to an Object which valueOf function is overloaded
- Set the two Attribute objects attr1 and attr2 to Element object elem
- Call elem.removeAttributeNode(attr1) to remove the attr1 from elem
- 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
- 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:
- Why does removeAttributeNode() trigger the valueOf callback in script level?
- Why does null pointer dereference exception still occur on DOM objects which are protected by isolated heap and delayed free mechanism?
- 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():
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).
3.When the index of the attribute object is found, the attribute object is retrieved through CBase::GetObjectAt:
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!
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):
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:
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:
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:
3.CBase::GetIntoBSTRAt triggers the valueOf callback in the script:
4.The memory layout of the elem object after calling clearAttributes() in the valueOf callback:
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:
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:
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:
According to the static analysis of the CBase::DeleteAt function, for the case where the index is -1, an exception will be thrown:
After returning, the CAttrArray object pointer points to the memory set as NULL, which finally triggers the null pointer dereference exception:
Here is a picture to explain the whole process:
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:
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:
<script>
var elem = document.createElement('xxx');
var attr1 = document.createAttribute('yyy');
//var attr2 = document.createAttribute('zzz');
var obj = {};
obj.valueOf = function() {
elem.clearAttributes();
return 0x1337;
};
attr1.nodeValue = obj;
//attr2.nodeValue = 123;
elem.setAttributeNode(attr1);
//elem.setAttributeNode(attr2);
elem.setAttribute('zzz', Array(0x10000).join('A'));
elem.removeAttributeNode(attr1);
</script>
The memory layout of elem before removeAttributeNode():
After clearAttributes(), the VARIANT array of CAttryArray is copied forward, and the BSTR is released immediately:
In the first DeleteAt operation, the BSTR memory of the ‘zzz’ attribute with index=2 is accessed again, which results in UAF:
Here we can get a 0x20010 bytes memory hole after the valueOf callback clearAttributes().
Then there are two questions need to answer:
- What object can be chosen to occupy the empty memory?
- 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:
- Use Scripting.Dictionary.items() to occupy the memory hole and use the hd2.nodeValue dangling pointer to leak the fake ArrayBuffer address
- Leak the metadata of the fake ArrayBuffer, modify ArrayBuffer’s buffer=0, length=0xffffffff and get new fake ArrayBuffer
- 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.
References
[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