小白分析漏洞之Microsoft Edge Chakra OP_NewScObjArray Type Confusion 远程代码执行

参考: 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 函数,

猜你喜欢

转载自blog.csdn.net/m0_37921080/article/details/83180644
今日推荐