This is a friendly warning that your web-browser does not currently protecting your privacy and/or security as well as you might want. Click on this message to see more information about the issue(s) that were detected. November 29th, 2016 Google Chrome Accessibility blink::Node corruption

Google Chrome Accessibility blink::Node corruption

(The fix and CVE number for this issue are unknown)

Synopsis

A specially crafted web-page can trigger an unknown memory corruption vulnerability in Google Chrome Accessibility code. An attacker can cause code to attempt to execute a method of an object using a vftable, when the pointer to that object is not valid, or the object is not of the expected type. Successful exploitation can lead to arbitrary code execution.

Known affected software and attack vectors

  • Chrome 48.0.2540.0 dev-m on Windows (does not seem to be OS specific)

    An attacker would need to get a target user to open a specially crafted webpage. Renderer accessibility must be enabled through the "--force-renderer-accessibility" command-line option. Disabling Java­Script will not prevent an attacker from triggering the vulnerable code path.

Repro.html <iframe id=x></iframe> <script> var u = 0; onload = x.onload = function () { x.src = "Target" + (u++ % 2) + ".html" } </script> Target0.html <form> Target1.html <canvas><object id=a>x

Description

Repeatedly loading two different pages in an iframe can cause the accessibility code to crash. This crash can happen in two different code paths, which are similar and both end up crashing because of a corrupt blink::Node instance.

The first code path calls blink::is­Disabled­Form­Control with the corrupt blink::Node instance as an argument from AXNode­Object::can­Set­Focus­Attribute:

void AXNode­Object::insert­Child(AXObject* child, unsigned index) { if (!child) return; // If the parent is asking for this child's children, then either it's the first time (and clearing is a no-op), // or its visibility has changed. In the latter case, this child may have a stale child cached. // This can prevent aria-hidden changes from working correctly. Hence, whenever a parent is getting children, ensure data is not stale. child->clear­Children(); if (child->accessibility­Is­Ignored()) { // | // `------. // V bool AXObject::accessibility­Is­Ignored() const { update­Cached­Attribute­Values­If­Needed(); // | // `----------. // V void AXObject::update­Cached­Attribute­Values­If­Needed() const { if (is­Detached()) return; AXObject­Cache­Impl& cache = ax­Object­Cache(); if (cache.modification­Count() == m_­last­Modification­Count) return; m_­last­Modification­Count = cache.modification­Count(); m_­cached­Is­Inert­Or­Aria­Hidden = compute­Is­Inert­Or­Aria­Hidden(); m_­cached­Is­Descendant­Of­Leaf­Node = (leaf­Node­Ancestor() != 0); m_­cached­Is­Descendant­Of­Disabled­Node = (disabled­Ancestor() != 0); m_­cached­Has­Inherited­Presentational­Role = (inherits­Presentational­Role­From() != 0); // | // ,---------------' // V const AXObject* AXNode­Object::inherits­Presentational­Role­From() const { // ARIA states if an item can get focus, it should not be presentational. if (can­Set­Focus­Attribute()) return 0; if (is­Presentational()) return this; // http://www.w3.org/TR/wai-aria/complete#presentation // ARIA spec says that the user agent MUST apply an inherited role of presentation // to any owned elements that do not have an explicit role defined. if (aria­Role­Attribute() != Unknown­Role) return 0; AXObject* parent = parent­Object(); if (!parent) return 0; HTMLElement* element = nullptr; if (node() && node()->is­HTMLElement()) element = to­HTMLElement(node()); if (!parent->has­Inherited­Presentational­Role()) { // | // ,-' // V bool AXObject::has­Inherited­Presentational­Role() const { update­Cached­Attribute­Values­If­Needed(); // | // `----------. // V void AXObject::update­Cached­Attribute­Values­If­Needed() const { if (is­Detached()) return; AXObject­Cache­Impl& cache = ax­Object­Cache(); if (cache.modification­Count() == m_­last­Modification­Count) return; m_­last­Modification­Count = cache.modification­Count(); m_­cached­Is­Inert­Or­Aria­Hidden = compute­Is­Inert­Or­Aria­Hidden(); m_­cached­Is­Descendant­Of­Leaf­Node = (leaf­Node­Ancestor() != 0); m_­cached­Is­Descendant­Of­Disabled­Node = (disabled­Ancestor() != 0); m_­cached­Has­Inherited­Presentational­Role = (inherits­Presentational­Role­From() != 0); // | // ,---------------' // V const AXObject* AXNode­Object::inherits­Presentational­Role­From() const { // ARIA states if an item can get focus, it should not be presentational. if (can­Set­Focus­Attribute()) // | // `----------. // V bool AXNode­Object::can­Set­Focus­Attribute() const { Node* node = this->node(); /*** returns bad value ***/ if (!node) return false; if (is­Web­Area()) return true; // NOTE: It would be more accurate to ask the document whether set­Focused­Node() would // do anything. For example, set­Focused­Node() will do nothing if the current focused // node will not relinquish the focus. if (!node) return false; if (is­Disabled­Form­Control(node)) // | // `---. // V inline bool is­Disabled­Form­Control(const Node* node) /*** node is corrupted ***/ { return node->is­Element­Node() && to­Element(node)->is­Disabled­Form­Control(); /*** Potential crash here ***/

This causes an access violation when blink::is­Disabled­Form­Control attempts to call the is­Disabled­Form­Control method on the corrupt blink::Node instance.

The second code path calls blink::Element::fast­Get­Attribute with the corrupt blink::Node instance as an argument from blink::AXObject::get­Attribute:

const Atomic­String& AXObject::get­Attribute(const Qualified­Name& attribute) const { Node* element­Node = node(); /*** returns bad value ***/ if (!element­Node) return null­Atom; if (!element­Node->is­Element­Node()) /*** Potential crash here ***/ return null­Atom; Element* element = to­Element(element­Node); return element->fast­Get­Attribute(attribute); /*** Potential crash here ***/ // | // `---------------. // V inline const Atomic­String& Element::fast­Get­Attribute(const Qualified­Name& name) const { ASSERT(fast­Attribute­Lookup­Allowed(name)); if (element­Data()) { /*** Potential crash here ***/ if (const Attribute* attribute = element­Data()->attributes().find(name)) /*** Potential crash here ***/ // | // `---------------------------------------------------------------------------. // V template <typename Container, typename Container­Member­Type> inline typename Attribute­Collection­Generic<Container, Container­Member­Type>::iterator Attribute­Collection­Generic<Container, Container­Member­Type>::find(const Qualified­Name& name) const { /*** this is working on corrupted data and almost certain to crash ***/ iterator end = this->end(); for (iterator it = begin(); it != end; ++it) { if (it->name().matches(name)) return it; } return nullptr; }

This can cause an access violation at various locations along the code path, but almost certainly does so if the code reaches the part where it attempts to match the attribute name, as the blink::Attribute­Collection­Generic<...> was taken from a corrupt blink::Node instance and that data is therefore almost certainly completely invalid.

Exploit

Is is unclear to me why the blink::Node instance was corrupted. During analysis, I was having trouble running Google Chrome with Page Heap enabled, which severely limited my ability to reliably crash the application and find out what information on the heap belongs to what object. Then, before I could get my debugging environment fixed, the issue appears to have been fixed, as I was no longer able to reproduce it. Any information on exploitability is therefore based on speculation.

An attacker who is able to trigger the issue reliably, and has some control over the corrupted blink::Node instance that is returned, or heap memory in this area, may be able to control execution flow through the blink::is­Disabled­Form­Control call, as this uses information from the corrupted blink::Node instance as a pointer to a vftable. The disassembled code looks like this:

``C++ blink::is­Disabled­Form­Control: push ebp mov ebp,esp mov ecx,dword ptr [ebp+8] mov eax,dword ptr [ecx+10h] shr eax,3 test al,1 je label_­return_­false mov eax,dword ptr [ecx] // ecx is the corruptedblink::Node` mov eax,dword ptr [eax+1DCh] // eax may be attacker controlled call eax // eip may be attacker controlled test al,al je chrome_­child!blink::is­Disabled­Form­Control+0x22 (5cf71ce2) mov al,1 pop ebp ret label_­return_­false: xor al,al pop ebp ret

Time-line

  • October 2015: This vulnerability was found through fuzzing.
  • November 2016: Details of this vulnerability are released.

(This issue was never reported, as I was struggling with my debugging environment, as described above. At some point after I discovered it, this issue appears to have been fixed, as evidenced by the repro no longer working. However, I have no exact date, nor a fix number to provide here).

© Copyright 2019 by Sky­Lined. Last updated on September 9th, 2019. Creative Commons License This work is licensed under a Creative Commons Attribution-Non‑Commercial 4.0 International License. If you find this web-site useful and would like to make a donation, you can send bitcoin to 183yyxa9s1s1f7JBp­PHPmz­Q346y91Rx5DX.