参考: https://paper.seebug.org/692/#1
原文作者的分析环境是:Windows 10 x64 + Microsoft Edge 42.17074.1002.0
我的分析环境是:Windows 10 X86_1803_march + Microsoft Edge 42.17134.1.0
poc地址 : https://github.com/bo13oy/ChakraCore
1. 环境准备
1.1 验证环境
创建好虚拟机,拷贝好poc.html 与 poc.js 。 首先用edge 打开 poc.html ,看一看是否会发生异常(不停的刷新,最后出现“此页无法加载”)。出现异常说明我们的环境是正确的,可以正常触发漏洞。
1.2 为edge 开启页堆:
(powershell 下执行)
PS C:\Users\Mr.wang> gflags.exe -I MicrosoftEdgeCP.exe +hpa +ust
1.3 启动调试
由于edge 是uwp 包, 不能使用一般的附加形式进行调试,需要在命令行下 windbg 使用参数启动调试。
可以看我的另外一篇博客: https://blog.csdn.net/m0_37921080/article/details/83097979
调用方式如下:
windbg.exe -plmPackage <PLMPackageName> -plmApp <ApplicationId> [<parameters>]
主要是第一个参数 Package fullname 的获得。下面是使用powershell 命令的方法获取packagefullname, 即:Microsoft.MicrosoftEdge_42.17134.1.0_neutral__8wekyb3d8bbwe
PS C:\Users\Mr.wang> Get-AppxPackage *MicrosoftEdge
Name : Microsoft.MicrosoftEdge
Publisher : CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US
Architecture : Neutral
ResourceId :
Version : 42.17134.1.0
PackageFullName : Microsoft.MicrosoftEdge_42.17134.1.0_neutral__8wekyb3d8bbwe
InstallLocation : C:\Windows\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe
IsFramework : False
PackageFamilyName : Microsoft.MicrosoftEdge_8wekyb3d8bbwe
PublisherId : 8wekyb3d8bbwe
IsResourcePackage : False
IsBundle : False
IsDevelopmentMode : False
IsPartiallyStaged : False
SignatureKind : System
Status : Ok
第二个参数Appid 是:MicrosoftEdge
第三个参数是edge 要打开的url, 即我们的poc.html
最终通过下面的命令调用 windbg 进行调试:
PS C:\Users\Mr.wang> windbg -plmPackage Microsoft.MicrosoftEdge_42.17134.1.0_neutral__8wekyb3d8bbwe -plmApp MicrosoftEd
ge file:///C:/Users/Mr.wang/Desktop/ChakraCore-master/poc/poc.html
2. 开始分析
2.1 定位到异常函数
输入g, 执行几次之后,捕捉到下面的异常:
(1a50.1a40): C++ EH exception - code e06d7363 (first chance)
(1a50.1a40): C++ EH exception - code e06d7363 (first chance)
(1a50.1a40): C++ EH exception - code e06d7363 (first chance)
(1a50.1a40): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=1567e064 ebx=0000fefa ecx=18010130 edx=5aa91858 esi=000fefa0 edi=1567e010
eip=5a7f9b1e esp=0a1bc580 ebp=0a1bc5a4 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010216
chakra!Js::DynamicProfileInfo::RecordCallSiteInfo+0x3e:
5a7f9b1e 66837c060200 cmp word ptr [esi+eax+2],0 ds:0023:1577d006=????
查看一下栈调用历史:
1:060> kb
# ChildEBP RetAddr Args to Child
00 0a1bc5a4 5a62f1a5 18010130 0000fefa 5aa91858 chakra!Js::DynamicProfileInfo::RecordCallSiteInfo+0x3e
01 0a1bc5e8 5a947097 0000fefa 000009e9 01000002 chakra!Js::ProfilingHelpers::ProfiledNewScObjArray+0x83
02 0a1bc610 5a85089f 18675c75 0a1bc720 18010130 chakra!Js::InterpreterStackFrame::OP_NewScObjArray_Impl<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> >,0>+0x58
03 0a1bc630 5a6daa2a 18675c74 0a1bc64c 18010130 chakra!Js::InterpreterStackFrame::ProcessUnprofiledMediumLayoutPrefix+0x1743ef
04 0a1bc650 5a6da896 86f8926b 00000000 0a1bc720 chakra!Js::InterpreterStackFrame::ProcessUnprofiled+0x10a
05 0a1bc688 5a6d9779 86f89227 18010130 18675c58 chakra!Js::InterpreterStackFrame::Process+0x146
06 0a1bc6c4 5a6db3fe 18675c58 18010130 0a1bc720 chakra!Js::InterpreterStackFrame::OP_TryCatch+0x49
07 0a1bc6e0 5a6da896 86f893fb 06ec7500 0a1bc720 chakra!Js::InterpreterStackFrame::ProcessUnprofiled+0xade
08 0a1bc718 5a6dfb01 18490010 18675c7a 00000001 chakra!Js::InterpreterStackFrame::Process+0x146
09 0a1bcd08 5a7cedf9 0a1bcd38 00000000 10000001 chakra!Js::InterpreterStackFrame::InterpreterHelper+0x3b1
0a 0a1bcd34 16df0fda 0a1bcd48 0a1bcd88 5a7fbebd chakra!Js::InterpreterStackFrame::InterpreterThunk+0x39
可以看到崩溃 是在 chakra!Js::DynamicProfileInfo::RecordCallSiteInfo+0x3e函数里。
那如何找到这个函数呢?
首先在目标系统的文件夹里搜索 chakra ,找到 Chakra.dll 。然后我们用ida进行逆向,但是目前没有符号文件,目标系统又开启了aslr,该如何找到函数的位置呢?
windbg 里面看一下已加载的 模块:
1:060> lm
start end module name
00050000 00082000 MicrosoftEdgeCP (deferred)
0a2c0000 0a42c000 ieapfltr (deferred)
0f230000 0f550000 EdgeContent (pdb symbols) C:\ProgramData\dbg\sym\EdgeContent.pdb\BF90D585B4B1360DF8C56FF212CBA0C41\EdgeContent.pdb
57870000 578d4000 verifier (deferred)
58b40000 58b9e000 ieproxy (deferred)
5a2c0000 5a2ce000 msimtf (deferred)
5a4a0000 5a4c6000 smartscreenps (deferred)
5a4f0000 5a513000 srpapi (deferred)
5a520000 5a555000 MLANG (deferred)
5a560000 5abf1000 chakra (pdb symbols) C:\ProgramData\dbg\sym\chakra.pdb\450E4A112167FABD27683D5B77B8E6131\chakra.pdb
5ac00000 5c113000 edgehtml (pdb symbols) C:\ProgramData\dbg\sym\edgehtml.pdb\A1484365ED972195659A8B67C8368C7E1\edgehtml.pdb
5c120000 5c176000 edgeIso (pdb symbols) C:\ProgramData\dbg\sym\edgeIso.pdb\9A2D5C4F398CD6148451A4A88A6D940F1\edgeIso.pdb
ctrl+f 找到 chakra, 发现还有pdb 文件。从这里我们可以看到两个重要信息,
一个是 pdb文件的位置
一个是 chakra 的加载位置。
找到pdb文件,拷贝出来,用ida加载一下,这样分析chakra.dll 的时候就有符号了。
异常触发地址是 0x5a7f 9b1e , 模块基地址是0x5a56 0000, 所以异常触发相对位置是 0x299b1e。
ida 里面看模块的默认基地址是0x1000 0000。 所以异常位置应该是 0x1029 9b1e。
.text:10299B1B mov [ebp+var_10], esi ; 崩溃点
.text:10299B1E cmp word ptr [esi+eax+2], 0
.text:10299B24 jl loc_10299D4D
果然如此。
2.2 静态分析
下面我们来看,ida逆向出来的代码:
.text:10299AE0 ; Attributes: bp-based frame
.text:10299AE0
.text:10299AE0 ; public: void __thiscall Js::DynamicProfileInfo::RecordCallSiteInfo(class Js::FunctionBody *, unsigned short, class Js::FunctionInfo *, class Js::JavascriptFunction *, unsigned int, bool, unsigned int)
.text:10299AE0 ?RecordCallSiteInfo@DynamicProfileInfo@Js@@QAEXPAVFunctionBody@2@GPAVFunctionInfo@2@PAVJavascriptFunction@2@I_NI@Z proc near
.text:10299AE0 ; CODE XREF: Js::InterpreterStackFrame::OP_ProfileCallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<1>>>>(Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<1>>> const *,Js::RecyclableObject *,uint,ushort,uint,Js::AuxArray<uint> const *)+4Dp
.text:10299AE0 ; Js::InterpreterStackFrame::OP_ProfileCallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1>>>>(Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1>>> const *,Js::RecyclableObject *,uint,ushort,uint,Js::AuxArray<uint> const *)+4Cp ...
.text:10299AE0
.text:10299AE0 var_18 = dword ptr -18h
.text:10299AE0 var_10 = dword ptr -10h
.text:10299AE0 var_C = dword ptr -0Ch
.text:10299AE0 var_8 = dword ptr -8
.text:10299AE0 var_4 = dword ptr -4
.text:10299AE0 arg_0 = dword ptr 8
.text:10299AE0 arg_4 = dword ptr 0Ch
.text:10299AE0 arg_8 = dword ptr 10h
.text:10299AE0 arg_C = dword ptr 14h
.text:10299AE0 arg_10 = dword ptr 18h
.text:10299AE0 arg_14 = byte ptr 1Ch
.text:10299AE0 arg_18 = dword ptr 20h
.text:10299AE0
.text:10299AE0 push ebp
.text:10299AE1 mov ebp, esp
.text:10299AE3 mov edx, [ebp+arg_8] ; 将 输入参数 arg_8 放到 edx 里面。
.text:10299AE6 sub esp, 18h ; 空出一片栈空间作为函数内局部变量。
.text:10299AE9 push ebx
.text:10299AEA mov ebx, [ebp+arg_4] ; ebx = arg_4
.text:10299AED push esi
.text:10299AEE push edi
.text:10299AEF mov edi, ecx
.text:10299AF1 mov ecx, [ebp+arg_0] ; ecx = arg_0
.text:10299AF4 test edx, edx ; 如果 arg_8 参数为0, 则直接跳过下面一段
.text:10299AF6 jz short loc_10299B12
.text:10299AF8 cmp ecx, [edx+4]
.text:10299AFB jnz short loc_10299B12
.text:10299AFD cmp bx, 20h
.text:10299B01 jnb short loc_10299B12
.text:10299B03 mov ecx, [edi+44h]
.text:10299B06 movzx eax, bx
.text:10299B09 bts ecx, eax
.text:10299B0C mov [edi+44h], ecx
.text:10299B0F mov ecx, [ebp+arg_0]
.text:10299B12
.text:10299B12 loc_10299B12: ; CODE XREF: Js::DynamicProfileInfo::RecordCallSiteInfo(Js::FunctionBody *,ushort,Js::FunctionInfo *,Js::JavascriptFunction *,uint,bool,uint)+16j
.text:10299B12 ; Js::DynamicProfileInfo::RecordCallSiteInfo(Js::FunctionBody *,ushort,Js::FunctionInfo *,Js::JavascriptFunction *,uint,bool,uint)+1Bj ...
.text:10299B12 mov eax, [edi+4]
.text:10299B15 movzx esi, bx
.text:10299B18 shl esi, 4
.text:10299B1B mov [ebp+var_10], esi
.text:10299B1E cmp word ptr [esi+eax+2], 0 ; 崩溃点
.text:10299B24 jl loc_10299D4D
上面是逆向出来的汇编代码,简单的看了一下。因为有符号文件,所以我用F5 转换成 c语言,更好分析代码的逻辑。
void __thiscall Js::DynamicProfileInfo::RecordCallSiteInfo(Js::DynamicProfileInfo *this, struct Js::FunctionBody *a2, unsigned __int16 a3, struct Js::FunctionBody **a4, struct Js::JavascriptFunction *a5, unsigned int a6, bool a7, unsigned int a8)
{
Js::DynamicProfileInfo *new_this; // edi@1
struct Js::FunctionBody *v9; // ecx@1
int v10; // ecx@4
int v11; // eax@5
int v12; // esi@5
int v13; // ebx@7
Js::DynamicProfileInfo *v14; // ecx@8
int v15; // eax@10
int v16; // eax@13
struct Js::FunctionBody *v17; // ecx@13
int v18; // ecx@14
int v19; // eax@14
int v20; // edx@29
int v21; // edx@30
int v22; // esi@30
int v23; // [sp+Ch] [bp-18h]@10
int v24; // [sp+14h] [bp-10h]@5
unsigned int v25; // [sp+18h] [bp-Ch]@6
unsigned int v26; // [sp+1Ch] [bp-8h]@7
struct Js::FunctionBody *v27; // [sp+20h] [bp-4h]@9
Js::DynamicProfileInfo *v28; // [sp+20h] [bp-4h]@22
new_this = this;
v9 = a2;
if ( a4 && a2 == a4[1] && a3 < 0x20u ) // 判断条件: a3< 0x20 ; a4 存在, 并且 a4 与a2 指向同一个结构
{
v10 = *((_DWORD *)new_this + 17); // v10 是结构体里的一个 属性
_bittestandset(&v10, a3); // 这可能是个 测试并修改的函数,吧a3 的值放到了 结构体属性里
*((_DWORD *)new_this + 17) = v10; // 把修改后的v10 重新放回结构体里
v9 = a2;
}
v11 = *((_DWORD *)new_this + 1); // v11 里存放结构体的某个属性
v12 = 16 * a3; // a3 <=> callsiteid
v24 = v12;
if ( *(_WORD *)(v12 + v11 + 2) < 0 ) // 在这里发生了崩溃,无法读取对应的地址
上面的注释是我 自己的一些分析。 通过动态调试,我们也可以知道,主要就是 最后的哪个if 语句里, 读取 v12+v11+2 地址处的内容时,发生了越界读取。
而这里的 v12 = v16 * a3 。 a3 是一个函数的输入参数。看到这,当时很奇怪。
然后,了解到,edge 内核 chakra 已经开源了。所以下面我们结合源码来看。
2.3 源码分析
首先,下载源码:https://github.com/Microsoft/ChakraCore
这是 chakra 源码。
注意要下 V 1.10.0 版本的,
releases 那里是之前发布的不同版本的内核, 你可以找到v 1.10.0 的。
下载源码,解压, 用visual studio 打开 build 文件夹里的, chakra.core.sln 解决方案文件。然后就可以进行源码的阅读了。
动动你的小脑瓜,找到Js::DynamicProfileInfo 类,然后找到这个类的 RecordCallSiteInfo 成员函数。
下面是关键代码:
void DynamicProfileInfo::RecordCallSiteInfo(FunctionBody* functionBody, ProfileId callSiteId, FunctionInfo* calleeFunctionInfo, JavascriptFunction* calleeFunction, uint actualArgCount, bool isConstructorCall, InlineCacheIndex ldFldInlineCacheId)
{
AutoCriticalSection cs(&this->callSiteInfoCS);
......
bool doInline = true;
// This is a hard limit as we only use 4 bits to encode the actual count in the InlineeCallInfo
if (actualArgCount > Js::InlineeCallInfo::MaxInlineeArgoutCount)
{
doInline = false;
}
// Mark the callsite bit where caller and callee is same function
if (calleeFunctionInfo && functionBody == calleeFunctionInfo->GetFunctionProxy() && callSiteId < 32)
{
this->m_recursiveInlineInfo = this->m_recursiveInlineInfo | (1 << callSiteId);
}
if (!callSiteInfo[callSiteId].isPolymorphic)
我们可以将这段源码与上面逆向出来的c语言代码进行对比。可以看到,崩溃点就是最后一个if语句:
if (!callSiteInfo[callSiteId].isPolymorphic)
看到那个 callsiteid 作为索引值进行查找,我们立马就应该想到,会有这样的语句,[ id * size],因为数组元素是有自己的大小的 。
那么逆向c代码里的, v12 = 16 * a3。 a3 想必就是那个id 了, 16 则应该是数组元素的大小。
然后是 .isPolymorphic , 这应当是对应数组元素的某个属性, 相对偏移对应 8。
剩下的就是 v11 了,可以推测,这应当是数组的首地址。
然后我们看一下各处地址,可以发现数组首地址是可以访问的,但是 callsiteid 对应的数组元素位置却不可以访问。所以可以初步推断是 callsiteid 的问题。
但要确定是callsiteid 的问题,我们还要知道 数组的长度,所以下面看一下 callSiteInfo 的创建过程。
2.4 创建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 数组的大小。
{ (uint)offsetof(DynamicProfileInfo, ldLenInfo), functionBody->GetProfiledLdLenCount() * sizeof(LdLenInfo) },
{ (uint)offsetof(DynamicProfileInfo, ldElemInfo), functionBody->GetProfiledLdElemCount() * sizeof(LdElemInfo) },
{ (uint)offsetof(DynamicProfileInfo, stElemInfo), functionBody->GetProfiledStElemCount() * sizeof(StElemInfo) },
{ (uint)offsetof(DynamicProfileInfo, arrayCallSiteInfo), functionBody->GetProfiledArrayCallSiteCount() * sizeof(ArrayCallSiteInfo) },
{ (uint)offsetof(DynamicProfileInfo, fldInfo), functionBody->GetProfiledFldCount() * sizeof(FldInfo) },
{ (uint)offsetof(DynamicProfileInfo, divideTypeInfo), functionBody->GetProfiledDivOrRemCount() * sizeof(ValueType) },
{ (uint)offsetof(DynamicProfileInfo, switchTypeInfo), functionBody->GetProfiledSwitchCount() * sizeof(ValueType)},
{ (uint)offsetof(DynamicProfileInfo, slotInfo), functionBody->GetProfiledSlotCount() * sizeof(ValueType) },
{ (uint)offsetof(DynamicProfileInfo, parameterInfo), functionBody->GetProfiledInParamsCount() * sizeof(ValueType) },
{ (uint)offsetof(DynamicProfileInfo, returnTypeInfo), functionBody->GetProfiledReturnTypeCount() * sizeof(ValueType) },
{ (uint)offsetof(DynamicProfileInfo, loopImplicitCallFlags), (EnableImplicitCallFlags(functionBody) ? (functionBody->GetLoopCount() * sizeof(ImplicitCallFlags)) : 0) },
{ (uint)offsetof(DynamicProfileInfo, loopFlags), functionBody->GetLoopCount() ? BVFixed::GetAllocSize(functionBody->GetLoopCount() * LoopFlags::COUNT) : 0 }
};
for (uint i = 0; i < _countof(batch); i++)
{
totalAlloc += batch[i].size;
}
DynamicProfileInfo* info = nullptr;
// In the profile storage case (-only), always allocate a non-leaf profile
// In the regular profile case, we need to allocate it as non-leaf only if it's
// a profile being used in the in-memory cache. This is because in that case, the profile
// also allocates dynamicProfileFunctionInfo, which it uses to match functions across
// script contexts. In the normal case, since we don't allocate that structure, we
// can be a leaf allocation.
if (persistsAcrossScriptContexts)
{
info = RecyclerNewPlusZ(recycler, totalAlloc, DynamicProfileInfo, functionBody);
// 这里是申请了一块内存,大小是 totalAlloc+ sizeof(DynamicProfileinfo)
......
}
else
{
......
{
info = RecyclerNewPlusLeafZ(recycler, totalAlloc, DynamicProfileInfo, functionBody);
}
}
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;
}
}
Assert(current - reinterpret_cast<BYTE*>(info) - sizeof(DynamicProfileInfo) == totalAlloc);
info->Initialize(functionBody);
return info;
}
上面的代码里申请了一快内存空间,空间开始处放的是 DynamicProfileInfo 对象,这个对象里面有许多数组指针。然后空间的剩余部分就是用来存放这些数组。 而 callsiteinfo 就是这些数组里的第一个。所以如果 callsiteinfo 数组被越界访问,有可能导致异常。
(上面的源码大家可以结合着 dynamicproinfo 的定义来看)。
我们可以通过给New函数下断点的方式,来查看每次创建对象时,为callsiteinfo 数组分配的内存空间有多大。如果,callsiteid 明显大于分配的内存空间,可以认为就是由callsiteid 过大照成的。
2.5 参数顺藤摸瓜
首先在windbg 里通过 kb 栈回溯,找到之前的函数:
2:074> kb
# ChildEBP RetAddr Args to Child
00 0901c4c4 63d1f1a5 17ea0130 0000fefa 64181858 chakra!Js::DynamicProfileInfo::RecordCallSiteInfo+0x3e
01 0901c508 64037097 0000fefa 000009e9 01000002 chakra!Js::ProfilingHelpers::ProfiledNewScObjArray+0x83
可以看到是 Js::ProfilingHelpers::ProfiledNewScObjArray 函数,