作者:bo13oy of Qihoo 360 Vulcan Team
作者博客:360 Technology Security Blog
1. 漏洞描述
漏洞編號:無
影響版本:Chakra <= 1.10.0
此漏洞是我去年11月份發現的,在今年的七月微軟的發布版本中被修掉。該漏洞成因在於:Interpreter在執行OP_NewScObjArray
操作碼指令時處理不當,在OP_NewScObjArray_Impl
函數內有一個結構體之間的強制轉換,導致了類型混淆,成功利用該漏洞可導致遠程代碼執行。
2. 測試環境
Windows 10 x64 + Microsoft Edge 42.17074.1002.0
3. 漏洞分析
3.1 漏洞基本信息
開啟頁堆保護后(gflags.exe -I MicrosoftEdgeCP.exe +hpa +ust)
,用Microsoft Edge瀏覽器加載poc.html時的異常信息如下:
0:017> r
rax=00000033438faf18 rbx=00000000000fefa0 rcx=000001aee1be3020
rdx=00007ff88f40c698 rsi=000001a6c2bcd960 rdi=000001aee1be3020
rip=00007ff88ee5d224 rsp=00000033438faea0 rbp=000000000000fefa
r8=000001aee1ce2050 r9=00007ff88f40c698 r10=000001a6c2b38568
r11=000001aee33001b0 r12=000001aee1be3020 r13=000001aec87103c0
r14=000001aee1be3b29 r15=00000000000009e9
iopl=0 nv up ei pl nz na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
chakra!Js::DynamicProfileInfo::RecordCallSiteInfo+0x54:
00007ff88ee5d224 450fb74802 movzx r9d,word ptr [r8+2] ds:000001aee1ce2052=????
0:017> kb
RetAddr : Args to Child : Call Site
00 00007ff88ed5ca46 : 000001aee1be3020 000001aee33001b0 000001a6c2b3fefa 00007ff88f40c698 : chakra!Js::DynamicProfileInfo::RecordCallSiteInfo+0x54
01 00007ff88ed5b061 : 000001aec8708580 00000033438faff0 000001aec87103c0 000001a6c2b3fefa : chakra!Js::ProfilingHelpers::ProfiledNewScObjArray+0xa6
02 00007ff88efe8518 : 00000033438fb280 000001aee3af7c85 00000033438fb0b0 000001aee3af7c84 : chakra!Js::InterpreterStackFrame::OP_NewScObjArray_Impl<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> >,0>+0x81
03 00007ff88ee25e1b : 00000033438fb280 000001aee3af7c84 00000033438fb0b0 0000000000000000 : chakra!Windows::Data::Text::IUnicodeCharactersStatics::vcall'{144}'+0x1f618
04 00007ff88ee25c55 : 00000033438fb280 0000000000000000 0000000000000001 00007ff88ed5c496 : chakra!Js::InterpreterStackFrame::ProcessUnprofiled+0x9b
05 00007ff88ee24704 : 00000033438fb280 00000033438fb280 00000033438fb280 0000000000000001 : chakra!Js::InterpreterStackFrame::Process+0x175
06 00007ff88ee26cdb : 00000033438fb280 000001aee3af7c68 000001aee3af7c68 0000000000000000 : chakra!Js::InterpreterStackFrame::OP_TryCatch+0x64
07 00007ff88ee25c55 : 00000033438fb280 0000000000000000 0000000000000000 0000000000000000 : chakra!Js::InterpreterStackFrame::ProcessUnprofiled+0xf5b
08 00007ff88ee1913d : 00000033438fb280 00000033438fb280 00000033438fbc80 000001a6c2b28760 : chakra!Js::InterpreterStackFrame::Process+0x175
09 00007ff88ee189de : 000001aec87103c0 00000033438fbe60 000001aee1c70fba 00000033438fbe78 : chakra!Js::InterpreterStackFrame::InterpreterHelper+0x49d
0a 000001aee1c70fba : 00000033438fbeb0 0000000000000001 00000033438fbea0 00000033438fc288 : chakra!Js::InterpreterStackFrame::InterpreterThunk+0x4e
....
從上面的棧幀可知是調用RecordCallSiteInfo函數時內部發生了訪問異常。
3.2 漏洞成因
3.2.1 pc對應的代碼
源代碼如下:
void DynamicProfileInfo::RecordCallSiteInfo(FunctionBody* functionBody, ProfileId callSiteId, FunctionInfo* calleeFunctionInfo, JavascriptFunction* calleeFunction, uint actualArgCount, bool isConstructorCall, InlineCacheIndex ldFldInlineCacheId)
{
...
if (!callSiteInfo[callSiteId].isPolymorphic) //out of bound read
{
Js::SourceId oldSourceId = callSiteInfo[callSiteId].u.functionData.sourceId;
if (oldSourceId == InvalidSourceId)
{
return;
}
...
反彙編代碼如下:
void **__fastcall Js::DynamicProfileInfo::RecordCallSiteInfo(Js::DynamicProfileInfo *this, struct Js::FunctionBody *a2, unsigned __int16 a3, struct Js::FunctionInfo *a4, struct Js::JavascriptFunction *a5, unsigned __int16 a6, bool a7, unsigned int a8)
{
...
unsigned __int16 v12; // bp@1
signed __int64 v13; // rbx@3
signed __int64 v14; // r8@3
__int16 v15; // r9@3
...
v9 = a2;
v10 = this;
v11 = a4;
v12 = a3; // a3 <=> callSiteId
...
v13 =0x10 * a3;
v14 = v13 + *((_QWORD *)this + 1);//<=>callSiteInfo
v15 = *(_WORD *)(v14 + 2);//out of bound read
if ( v15 >= 0 )
{
v16 = *(_DWORD *)(v14 + 8);
LODWORD(v17) = -4;
if ( v16 == -4 )
return result;
...
根據 3.1 步驟的漏洞異常信息時,rbp=0x000000000000fefa
,及其上面的源代碼與彙編代碼的對比發現,callSiteId = a3 = v12 = bp = 0xfefa
。
0:017> dq rcx + 0x8
000001aee1be3028 000001aee1be30b0 0000000000000000
000001aee1be3038 0000000000000000 0000000000000000
000001aee1be3048 0000000000000000 0000000000000000
000001aee1be3058 000001aee1be3140 0000000000000000
000001aee1be3068 000001aee1bf313f 0000000000000000
000001aee1be3078 0000000000000000 0000000000000000
000001aee1be3088 0000000000000000 0000000000000009
000001aee1be3098 0000000000000000 ffffffff00000000
此時:
callSiteInfo = * ((QWORD * )this + 1) => poi(rcx + 0x8) = 0x000001aee1be30b0
r8 = (callSiteInfo + callSiteId * 0x10) = 0x000001aee1be30b0 + 0xfefa * 0x10 = 0x000001aee1ce2050
0:017> dd 000001aee1be30b0
000001aee1be30b0 00000009 00000000 ffffffff 00000000
000001aee1be30c0 00000009 00000000 ffffffff 00000000
000001aee1be30d0 00000009 00000000 ffffffff 00000000
000001aee1be30e0 00000009 00000000 ffffffff 00000000
000001aee1be30f0 00000009 00000000 ffffffff 00000000
000001aee1be3100 00000009 00000000 ffffffff 00000000
000001aee1be3110 00000009 00000000 ffffffff 00000000
000001aee1be3120 00000009 00000000 ffffffff 00000000
通過dd 0x000001aee1be30b0 命令查看callSiteInfo內容是可以訪問的,初步判定崩潰是因callSiteId越界導致的內存越界讀。
3.2.2 創建callSiteInfo對象的代碼
源代碼如下:
DynamicProfileInfo* DynamicProfileInfo::New(Recycler* recycler, FunctionBody* functionBody, bool persistsAcrossScriptContexts)
{
size_t totalAlloc = 0;
Allocation batch[] =
{
{ (uint)offsetof(DynamicProfileInfo, callSiteInfo), functionBody->GetProfiledCallSiteCount() * sizeof(CallSiteInfo) },// 計算分配callSiteInfo對象的內存大小
...
};
for (uint i = 0; i < _countof(batch); i++)
{
totalAlloc += batch[i].size;//數組元素內存分配之和
}
info = RecyclerNewPlusZ(recycler, totalAlloc, DynamicProfileInfo, functionBody);// 總的內存分配長度為:totalAlloc + sizeof(DynamicProfileInfo)
BYTE* current = (BYTE*)info + sizeof(DynamicProfileInfo);
}
}
for (uint i = 0; i < _countof(batch); i++)
{
if (batch[i].size > 0)
{
Field(BYTE*)* field = (Field(BYTE*)*)(((BYTE*)info + batch[i].offset));
*field = current;
current += batch[i].size;
}
}
info->Initialize(functionBody);
return info;
反彙編代碼如下:
char *__fastcall Js::DynamicProfileInfo::New(struct Memory::Recycler *a1, struct Js::FunctionBody *a2)
{
...
size_t v26; // rdx@16
char *v27; // rbx@18
...
else
{
v26 = v25 + 144;
if ( v25 + 144 < v25 )
v26 = -1i64;
v27 = Memory::Recycler::AllocLeafZero(v12, v26);//v26 = rdx => totalAlloc + sizeof(DynamicProfileInfo) = 內存分配長度
}
*((_WORD *)v27 + 56) = ValueType::Uninitialized;
v27[114] = 0;
v27[136] = 1;
LABEL_20:
v28 = (signed __int64)(v27 + 144);
v29 = 12i64;
v30 = &v38;
do
{
v31 = *((_QWORD *)v30 + 1);
if ( v31 )
{
*(_QWORD *)&v27[*v30] = v28;
v28 += v31;
}
v30 += 4;
--v29;
}
while ( v29 );
Js::DynamicProfileInfo::Initialize((Js::DynamicProfileInfo *)v27, (struct Js::FunctionBody *const )retaddr);
return v27;
}
用命令 ba chakra!Js::DynamicProfileInfo::New 在DynamicProfileInfo::New函數位置打個斷點,動態調試之後,可得如下信息:
0:017> r
rax=00000000000100e7 rbx=0000015bce310348 rcx=00000153adba97b0
rdx=0000000000010177 rsi=0000000000000000 rdi=0000015bce3101b0
rip=00007ff88ef9a7c3 rsp=0000004016ffba50 rbp=0000004016ffbb50
r8=0000000000000004 r9=0000000000000001 r10=00000153adba97b0
r11=0000004016ffbc70 r12=0000004016ffc0a8 r13=0000000000000001
r14=0000015bb37203c0 r15=0000000000000001
iopl=0 nv up ei pl nz na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
chakra!Js::DynamicProfileInfo::New+0x213:
00007ff88ef9a7c3 e8c830f0ff call chakra!Memory::Recycler::AllocLeafZero (00007ff8`8ee9d890)
此時可知:
(v26 = rdx = totalAlloc + sizeof(DynamicProfileInfo) = 0x10177) < (0xfefa0 = callSiteId * 0x10 = 0xfefa * 0x10),
內存分配長度為 0x10177 遠遠小於 使用時的 0xfefa0,確定漏洞是因callSiteId越界導致的內存越界讀。
3.2.3 追溯callSiteId參數的來源
根據 3.1 步驟的 kb 棧回溯命令可知,callSiteId是上層調用OP_NewScObjArray_Impl
函數進來的,OP_NewScObjArray_Impl
的源代碼如下:
template <class T, bool Profiled>
void InterpreterStackFrame::OP_NewScObjArray_Impl(const unaligned T* playout, const Js::AuxArray<uint32> *spreadIndices)
{
// Always profile this operation when auto-profiling so that array type changes are tracked
if (!Profiled && !isAutoProfiling)
Assert(!Profiled);
{
OP_NewScObject_Impl<T, Profiled, false>(playout, Js::Constants::NoInlineCacheIndex, spreadIndices);
return;
}
Arguments args(CallInfo(CallFlags_New, playout->ArgCount), m_outParams);
uint32 spreadSize = 0;
if (spreadIndices != nullptr){...}
else
{
SetReg(
(RegSlot)playout->Return,
ProfilingHelpers::ProfiledNewScObjArray(
GetReg(playout->Function),
args,
function,
static_cast<const unaligned OpLayoutDynamicProfile2<T> *>(playout)->profileId,//profileId <==> callSiteId
static_cast<const unaligned OpLayoutDynamicProfile2<T> *>(playout)->profileId2));
}
PopOut(playout->ArgCount);
}
根據上面的紅色部分,我們可知,callSiteId來自playout參數的profileId的成員變量。
3.2.4 追溯playout參數的來源
根據 3.1 步驟的 kb 棧回溯命令可知,playout是上層調用ProcessUnprofiled函數進來的,ProcessUnprofiled的源代碼調用棧追溯路徑如下:
InterpreterLoop.inl:
...
case INTERPRETER_OPCODE::MediumLayoutPrefix:
{
Var yieldValue = nullptr;
ip = PROCESS_OPCODE_FN_NAME(MediumLayoutPrefix)(ip, yieldValue);==>
CHECK_YIELD_VALUE();
CHECK_SWITCH_PROFILE_MODE();
break;
}
...
const byte* Js::InterpreterStackFrame::PROCESS_OPCODE_FN_NAME(MediumLayoutPrefix)(const byte* ip, Var& yieldValue)
{
INTERPRETER_OPCODE op = READ_OP(ip);
switch (op)
{
#ifndef INTERPRETER_ASMJS
case INTERPRETER_OPCODE::Yield:
m_reader.Reg2_Medium(ip);
yieldValue = GetReg(GetFunctionBody()->GetYieldRegister());
break;
#endif
#define DEF2_WMS(x, op, func) PROCESS_##x##_COMMON(op, func, _Medium)
#define DEF3_WMS(x, op, func, y) PROCESS_##x##_COMMON(op, func, y, _Medium)==>
#define DEF4_WMS(x, op, func, y, t) PROCESS_##x##_COMMON(op, func, y, _Medium, t)
#include "InterpreterHandler.inl"
default:
// Help the C++ optimizer by declaring that the cases we
// have above are sufficient
AssertMsg(false, "dispatch to bad opcode");
__assume(false);
}
return ip;
}
InterpreterHandler.inl:
...
DEF3_WMS(CALL, NewScObject, OP_NewScObject, CallI)
DEF3_WMS(CUSTOM_L_R0, NewScObjectNoCtorFull, OP_NewScObjectNoCtorFull, Reg2)
EXDEF2_WMS(A1toA1Mem, LdCustomSpreadIteratorList, JavascriptOperators::OP_LdCustomSpreadIteratorList)
EXDEF3_WMS(CALL, NewScObjectSpread, OP_NewScObjectSpread, CallIExtended)
DEF3_WMS(CALL, NewScObjArray, OP_NewScObjArray, CallI)==>
...
InterpreterStackFrame.cpp:
#define PROCESS_CALL_COMMON(name, func, layout, suffix) /
case OpCode::name: /
{ /
PROCESS_READ_LAYOUT(name, layout, suffix); //==>
func(playout); /
break; /
}
...
#define PROCESS_READ_LAYOUT(name, layout, suffix) /
CompileAssert(OpCodeInfo<OpCode::name>::Layout == OpLayoutType::layout); /
const unaligned OpLayout##layout##suffix * playout = m_reader.layout##suffix(ip); //==>
Assert((playout != nullptr) == (Js::OpLayoutType::##layout != Js::OpLayoutType::Empty)); // Make sure playout is used
...
LayoutTypes.h:
...
LAYOUT_TYPE (StartCall)
LAYOUT_TYPE_PROFILED2_WMS (CallI)==>
LAYOUT_TYPE_PROFILED_WMS (CallIFlags)
...
OpLayouts.h:
...
#define LAYOUT_TYPE_WMS(layout) /
typedef OpLayoutT_##layout OpLayout##layout##_Large; /
typedef OpLayoutT_##layout OpLayout##layout##_Medium; //==>
typedef OpLayoutT_##layout OpLayout##layout##_Small;
...
template <typename SizePolicy>
struct OpLayoutT_CallI // Return = Function(ArgCount)
{
typename SizePolicy::ArgSlotType ArgCount;
typename SizePolicy::RegSlotSType Return;
typename SizePolicy::RegSlotType Function;
};
...
ByteCodeReader.cpp:
template<typename LayoutType>
const unaligned LayoutType * ByteCodeReader::GetLayout(const byte*& ip)
{
size_t layoutSize = sizeof(LayoutType);//LayoutType=Js::OpLayoutT_CallI<Js::LayoutSizePolicy<MediumLayout>> => layoutSize = 0x5
AssertMsg((layoutSize > 0) && (layoutSize < 100), "Ensure valid layout size");
const byte * layoutData = ip;
ip += layoutSize;
m_currentLocation = ip;
Assert(m_currentLocation <= m_endLocation);
return reinterpret_cast(layoutData);
}
根據上面的源代碼追溯和動態調試,可得playout參數的類型為OpLayoutT_CallI結構體,指向的是bytecode的內容,長度為0x5個字節。OpLayoutT_CallI內存結構如下:
name: |ArgSlotType|RegSlotSType|RegSlotType|
size: | 1 byte | 2 byte | 2 byte |
value: | c5 | fe 00 | fe 00 |
此時再看OP_NewScObjArray_Impl
函數獲取callSiteId參數的代碼如下:
static_cast<const unaligned OpLayoutDynamicProfile2 *>(playout)->profileId,//profileId <==> callSiteId
發現playout參數被強制轉換成OpLayoutDynamicProfile2結構體,並提取其成員變量profileId當成callSiteId向下傳遞了,OpLayoutDynamicProfile2的結構體代碼如下:
OpLayouts.h:
...
typedef uint16 ProfileId;
...
// Dynamic profile layout wrapper
template <typename LayoutType>
struct OpLayoutDynamicProfile : public LayoutType
{
ProfileId profileId;
};
template <typename LayoutType>
struct OpLayoutDynamicProfile2 : public LayoutType
{
ProfileId profileId;
ProfileId profileId2;
};
...
該結構體的長度為sizeof(OpLayoutDynamicProfile2) = 0x9個字節, OpLayoutDynamicProfile2 內存結構如下:
name: |ArgSlotType|RegSlotSType|RegSlotType|profileId|profileId2|
size: | 1 byte | 2 byte | 2 byte | 2 byte | 2 byte |
value: | c5 | fe 00 | fe 00 | fa fe | e9 09 |
此時可知OpLayoutT_Call類型被混淆成OpLayoutDynamicProfile2使用,callSiteId = profileId = 0xfefa參數變量是由於在對象混淆情況下,越界讀了後面2個字節的bytecode指令操作碼,callSiteId參數被傳到RecordCallSiteInfo函數之後,產生了越界讀異常現象。
4. 漏洞利用
1) 首先跳到訪問異常點附近看看,RecordCallSiteInfo在函數訪問異常點之後,還有些什麼樣的操作,重點關注程序後續流程中有沒有寫的操作。RecordCallSiteInfo函數關鍵點代碼如下:
if (!callSiteInfo[callSiteId].isPolymorphic) // out of bound read
{
...
if (doInline && IsPolymorphicCallSite(functionId, sourceId, oldFunctionId, oldSourceId))
{
CreatePolymorphicDynamicProfileCallSiteInfo(functionBody, callSiteId, functionId, oldFunctionId, sourceId, oldSourceId);==>
}
...
void DynamicProfileInfo::CreatePolymorphicDynamicProfileCallSiteInfo(FunctionBody *funcBody, ProfileId callSiteId, Js::LocalFunctionId functionId, Js::LocalFunctionId oldFunctionId, Js::SourceId sourceId, Js::SourceId oldSourceId)
{
PolymorphicCallSiteInfo *localPolyCallSiteInfo = RecyclerNewStructZ(funcBody->GetScriptContext()->GetRecycler(), PolymorphicCallSiteInfo);
Assert(maxPolymorphicInliningSize >= 2);
localPolyCallSiteInfo->functionIds[0] = oldFunctionId;
localPolyCallSiteInfo->functionIds[1] = functionId;
localPolyCallSiteInfo->sourceIds[0] = oldSourceId;
localPolyCallSiteInfo->sourceIds[1] = sourceId;
localPolyCallSiteInfo->next = funcBody->GetPolymorphicCallSiteInfoHead();
for (int i = 2; i < maxPolymorphicInliningSize; i++)
{
localPolyCallSiteInfo->functionIds[i] = CallSiteNoInfo;
}
callSiteInfo[callSiteId].isPolymorphic = true;//out of bound write boolean
callSiteInfo[callSiteId].u.polymorphicCallSiteInfo = localPolyCallSiteInfo;//out of bound write pointer
funcBody->SetPolymorphicCallSiteInfoHead(localPolyCallSiteInfo);
}
...
假設在完全可以控制堆噴數據的情況下,那麼上面藍色部分的判斷可以過掉,在隨後的紅色部分就有寫的操作。
2) 漏洞分配的內存長度為0x10177,越界讀的內存偏移為0xfefa0 = callSiteId * 0x10 = 0xfefa * 0x10,由於callSiteId = 0xfefa是通過越界讀2個字節的bytecode指令操作碼得到的,所以這個越界讀的偏移不是任意可以控制的。
3) Microsoft Edge 堆隔離,及其內存分配機制。Edge的堆分為:
- 小堆 (0 < size <= 0x300):每隔0x10為一個堆桶(步長為0x10),對齊方式:0x10 實現方式:size => 堆桶的 map 映射。 例如:0x10、0x20、0x30… 一共(0x300 / 0x10 = 0x30個堆桶)
- 中堆 (0x300 < size <= 0x2000):每隔0x100為一個堆桶(步長為0x100),對齊方式:0x100 實現方式:size => 堆桶的 map 映射。例如:0x400、0x500、0x600…一共(0x2000-0x300 / 0x100 = 0x1D個堆桶)
- 大堆 (size > 0x2000):對齊方式:0x10 實現方式:堆桶之間的鏈表串連。
由於 0x10177 > 0x2000 的內存大小在大堆範疇,所以由大堆來分配內存。
綜合 1),2),3),及其深入分析之後,要能夠精準控制內存的堆噴,越界寫一些內存關鍵數據(如:長度、數據存儲指針等),選用array進行堆噴可以滿足要求,本利用中選擇越界修改array的長度來實現漏洞利用。堆噴之後的內存結構如下:
name: | vulnmem | fill_mem | pre_trigger_arr | trigger_arr | fill_leak_arr |
desc: | 0x10180 | spray_mem | int array | int array | object array |
完整的漏洞利用步驟如下:
a. 觸發漏洞之後,pre_trigger_arr的長度被修改為一個指針,此時pre_trigger_arr可越界寫,但不能越界讀。
b. 通過pre_trigger_arr越界寫,修改trigger_arr的長度,此時trigger_arr可越界讀寫。
c. 通過trigger_arr越界讀,可泄露fill_leak_arr中的任意一個元素對象的地址。
d. 通過pre_trigger_arr越界寫,修改trigger_arr的數據存儲指針為DataView對象地址偏移,把DataView數據頭偽造成trigger_arr的元素數據。
e. 通過trigger_arr正常的寫,修改DataView的arrayBuffer的數據指針。
f. 通過DataView正常讀取,可達到任意地址讀寫的目的。
5. 漏洞演示
a. 通過任意地址讀寫,泄露chakra.dll的基地址。
b. 通過調用GetProcAddress函數,泄露msvcrt.dll中malloc函數的地址。
c. 通過調用GetProcAddress函數,泄露kernel32.dll中WinExec函數的地址。
转载请注明:IAMCOOL » Microsoft Edge Chakra OP_NewScObjArray Type Confusion 遠程代碼執行漏洞分析與利用