原文:https://www.pentestpartners.com/security-blog/time-travel-debugging-finding-windows-gdi-flaws/

简介


2018年10月,Microsoft公布了49个安全漏洞的补丁程序。在这些漏洞中,既有内核级权限提升漏洞,也有可能导致远程代码执行的漏洞,例如MSXML。在这篇文章中,我们将对一个WMF越界读取漏洞进行深入分析,并尝试确定其可利用性。

这个安全漏洞是我们提交给微软的,目前已经得到了修复,对应漏洞编号为CVE-2018-8472。该漏洞的分析过程是在Windows 10 x64上进行的,使用的是测试套件是32位的。

Markus Gaasedelen在Timeless Debugging of Complex Software一文中,使用工具是Mozilla的rr,与此类似,在本文中我们将使用Windows Debugger Preview的时空旅行调试(TTD)功能来分析复杂漏洞的根本原因。

借助于时空旅行调试技术,我们可以通过前进和回滚的方式来跟踪和分析漏洞,这样的话,我们不仅能够找出导致代码崩溃的所有用户输入,同时,还能深入了解漏洞本身的相关情况。虽然之前已经对EMF文件格式进行了研究,但是通过使用winafl对WMF文件进行模糊测试,同样也能挖掘出相应的漏洞。使用特制的WMF文件调用gdiplus!GpImage::LoadImageW函数将引发崩溃:

(388.1928): Access violation - code c0000005 (!!! second chance !!!)
eax=00000012 ebx=00000000 ecx=00000001 edx=d0d0d0d0 esi=08632000 edi=086241c0
eip=74270b37 esp=00eff124 ebp=00eff14c iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010202
ucrtbase!memcpy+0x507:
74270b37 8b16            mov     edx,dword ptr [esi]  ds:002b:08632000=????????

从上面可以看出,当我们试图复制esi (08632000) 所指向的内存内容,并将其移动到edx寄存器时,崩溃发生了。就这里来说,esi所指向的好像是一个未映射的内存地址:

0:000> dc esi L10
08632000  ???????? ???????? ???????? ????????  ????????????????
08632010  ???????? ???????? ???????? ????????  ????????????????
08632020  ???????? ???????? ???????? ????????  ????????????????
08632030  ???????? ???????? ???????? ????????  ????????????????

图1:运行精心制作的WMF文件时执行的指令。

下一步是查看堆栈跟踪数据,从而了解我们是如何终止这个memcpy函数的:

0:000> kv
 # ChildEBP RetAddr  Args to Child              
00 00eff128 76d6e086 08624154 08631f94 00000072 ucrtbase!memcpy+0x507 (FPO: [3,0,2])
01 00eff14c 76d6dfd9 00000051 08621d20 00000000 gdi32full!MRBDIB::vInit+0x7d (FPO: [Non-Fpo])
02 00eff200 76d6da5f ffffff00 00001400 00001400 gdi32full!MF_AnyDIBits+0x167 (FPO: [Non-Fpo])
03 00eff334 74743ca3 75211255 00000000 ffffff00 gdi32full!StretchDIBitsImpl+0xef (FPO: [Non-Fpo])
04 00eff374 76da86ec 75211255 00000000 ffffff00 GDI32!StretchDIBits+0x43 (FPO: [Non-Fpo])
05 00eff494 76d69164 75211255 0862dff0 0861be96 gdi32full!PlayMetaFileRecord+0x3f3ec
06 00eff544 76d9749d 00000000 00000000 00eff568 gdi32full!CommonEnumMetaFile+0x3a5 (FPO: [Non-Fpo])
07 00eff554 74745072 75211255 d0261074 0049414e gdi32full!PlayMetaFile+0x1d (FPO: [Non-Fpo])
08 00eff568 71ac9eb1 75211255 d0261074 d0261074 GDI32!PlayMetaFileStub+0x22 (FPO: [Non-Fpo])
09 00eff5fc 71ac9980 09c39e18 000001e4 00000000 gdiplus!GetEmfFromWmfData+0x4f5 (FPO: [Non-Fpo])
0a 00eff624 71a9bd6a 09c33f3c 09c33fd0 00000000 gdiplus!GetEmfFromWmf+0x69 (FPO: [Non-Fpo])
0b 00eff770 71a8030c 09c33f3c 09c33fd0 00eff794 gdiplus!GetHeaderAndMetafile+0x1b970
0c 00eff79c 71a690f4 09c37fc8 00000001 71b59ec4 gdiplus!GpMetafile::InitStream+0x4c (FPO: [Non-Fpo])
0d 00eff7c0 71a77280 085a3fd0 00000000 09c31ff0 gdiplus!GpMetafile::GpMetafile+0xc2 (FPO: [Non-Fpo])
0e 00eff7e4 71a771e1 09c31ff0 00000000 0859cfeb gdiplus!GpImage::LoadImageW+0x36 (FPO: [Non-Fpo])
0f 00eff800 00311107 085a3fd0 09c31ff4 085a3fd0 gdiplus!GdipLoadImageFromFile+0x51 (FPO: [Non-Fpo])
--- redacted ---

有趣的是,我们可以看到memcpy是从MRBDIB::vInit()函数中调用的,而MRBDIB::vInit()函数又是从StretchDIBits、PlayMetaFileRecord、CommonEnumMetaFile和其他一些函数中调用的。

此外,让我们看看esi寄存器的值,以及为其分配的内存量:

0:000> !heap -p -a esi
    address 08632000 found in
    _DPH_HEAP_ROOT @ 5781000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                 a2b0a90:          8631f78               84 -          8631000             2000
          unknown!noop
    6afca8d0 verifier!AVrfDebugPageHeapAllocate+0x00000240
    773b4b16 ntdll!RtlDebugAllocateHeap+0x0000003c
    7730e3e6 ntdll!RtlpAllocateHeap+0x000000f6
    7730cfb7 ntdll!RtlpAllocateHeapInternal+0x000002b7
    7730ccee ntdll!RtlAllocateHeap+0x0000003e
    76af9f10 KERNELBASE!LocalAlloc+0x00000080
    76da8806 gdi32full!PlayMetaFileRecord+0x0003f506
    == redacted ==

我们可以看到,为其分配的内存空间为0x84字节,我们将在稍后进行精确定位,并确定该值的来源。

崩溃代码最小化与Windbg TTD功能简介


WMF是一种非常复杂的文件格式(并且现已弃用),所以,这里要将测试用例最小化!

在这个过程中,我们可以借助于Axel Souchet的afl-tmin工具,它带有winafl工具。我们可以通过以下命令来最小化crasher:

afl-tmin.exe -D C:\DRIO\bin32 -i C:\Users\symeon\Desktop\GDI\crasher_84.wmf -o C:\Users\symeon\Desktop\GDI\crasher_MIN.wmf -- -covtype edge -coverage_module GDI32.dll -target_method fuzzit -nargs 2 -- C:\Users\symeon\Desktop\GDI\GdiRefactor.exe @@

图2:最小化原始崩溃文件。

比较原始测试用例和最小化测试用例之间的差异,我们可以发现,最小化后的用例用起来更为顺手。大家要注意观察该工具是如何将我们不感兴趣的字节改为空字节(0x30),从而仅保留导致崩溃的关键字节的!这样就能够有效地帮助我们确定用户可以控制哪些字节,以及修改它们的方法,这些方面的内容,我们将在后面详细加以阐述。

图3:原始测试用例和最小化后的测试用例之间的比对。

通过最小化的测试用例,可以启动Windbg Preview并记录跟踪信息。为了记录跟踪信息,Windbg Preview需要用到管理员权限。为此,可以在使用Windbg运行时,选择File-> Start Debugging-> Launch Executable Advance,并确保启用“Record process with Time Travel Debugging”功能。

图4:Windg Preview捕获的新跟踪信息

使用测试套件(harness)和crasher启动跟踪,并继续执行,之后会看到:

图5:使用crasher启动测试套件(harness)时将导致memcpy崩溃

如果读者还没有看过微软的相关视频介绍的话,我们强烈建议您花点时间先看一遍。这里,我们只做简单介绍:

g-和!tt 00将让我们回滚到跟踪的初始状态(其中,后者使用的是百分比格式,例如!tt 50表示回滚到跟踪的中间时刻)。

p-用于回滚一个命令,可以与p-10结合使用,这样就能回滚n个命令了。

时空旅行最棒的地方在于,一旦启用了跟踪记录功能,所有内存/堆分配/偏移信息都会保留下来。这样一来,我们就可以迅速考察函数的参数以及指向我们感兴趣数据的内存地址了。

确定漏洞的根本原因


让我们再次输出堆栈的跟踪数据,这里将从跟踪数据的底部开始,检查与每条跟踪数据相关的函数调用,并输出其参数。

0:000> kv 6
 # ChildEBP RetAddr  Args to Child              
00 0078f260 757ee086 19a17108 19a3af94 00000072 ucrtbase!memcpy+0x507 (FPO: [3,0,2])
01 0078f284 757edfd9 00000051 19a14d20 00003030 gdi32full!MRBDIB::vInit+0x7d (FPO: [Non-Fpo])
02 0078f338 757eda5f 00003030 00003030 00003030 gdi32full!MF_AnyDIBits+0x167 (FPO: [Non-Fpo])
03 0078f46c 76e13ca3 9f211284 00003030 00003030 gdi32full!StretchDIBitsImpl+0xef (FPO: [Non-Fpo])
04 0078f4ac 758286ec 9f211284 00003030 00003030 GDI32!StretchDIBits+0x43 (FPO: [Non-Fpo])
05 0078f5cc 757e9164 9f211284 19a2cf38 19a0cee6 gdi32full!PlayMetaFileRecord+0x3f3ec

首先,让我们看看PlayMetaFileRecord的调用位置。

现在,让我们借助windbg的时空旅行调试(TTD)功能,带我们回到过去!

为此,一种方法是使用以下LINQ查询,并输出在跟踪过程中PlayMetaFileRecord的所有TTD调用:

0:000> dx -r1 @$cursession.TTD.Calls("gdi32full!PlayMetaFileRecord")

图6:跟踪PlayMetaFileRecord的调用位置

太棒了,上面的查询总共找到了三个调用。点击最后一个输出结果,即选中最后一个调用,将得到以下信息:

0:000> dx -r1 @$cursession.TTD.Calls("gdi32full!PlayMetaFileRecord")[2]
@$cursession.TTD.Calls("gdi32full!PlayMetaFileRecord")[2]                
    EventType        : Call
    ThreadId         : 0x1c3c
    UniqueThreadId   : 0x2
    TimeStart        : 2113:3DB [Time Travel]
    TimeEnd          : 2113:34C [Time Travel]
    Function         : UnknownOrMissingSymbols
    FunctionAddress  : 0x757e9300
    ReturnAddress    : 0x757e9164
    ReturnValue      : 0x3000000000
    Parameters

再次单击TimeStart属性中的Time Travel,我们将穿越到该调用发生的时刻(图4):

0:000> dx @$cursession.TTD.Calls("gdi32full!PlayMetaFileRecord")[2].TimeStart.SeekTo()
Setting position: 2113:3DB
@$cursession.TTD.Calls("gdi32full!PlayMetaFileRecord")[2].TimeStart.SeekTo()
(1fc0.1c3c): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: 2113:3DB
eax=19a0cee6 ebx=00000000 ecx=19a10f10 edx=00000084 esi=00000000 edi=9f211284
eip=757e9300 esp=0078f5d0 ebp=0078f67c iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
gdi32full!PlayMetaFileRecord:
757e9300 8bff            mov     edi,edi

输入p-命令,回滚一步,回到该调用被执行前那一刻的状态,以便查看相关的参数:

0:000> p-
Time Travel Position: 2113:3DA
eax=19a0cee6 ebx=00000000 ecx=19a10f10 edx=00000084 esi=00000000 edi=9f211284
eip=757e915f esp=0078f5d4 ebp=0078f67c iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
gdi32full!CommonEnumMetaFile+0x3a0:
757e915f e89c010000      call    gdi32full!PlayMetaFileRecord (757e9300)

另一种方法是使用Unassemble backwards命令(使用前面堆栈跟踪#05中的返回地址)(见图3):

0:000> ub 757e9164
gdi32full!CommonEnumMetaFile+0x38f:
757e914e 1485            adc     al,85h
757e9150 db0f            fisttp  dword ptr [edi]
757e9152 853de80300ff    test    dword ptr ds:[0FF0003E8h],edi
757e9158 75cc            jne     gdi32full!CommonEnumMetaFile+0x367 (757e9126)
757e915a 50              push    eax
757e915b ff75c4          push    dword ptr [ebp-3Ch]
757e915e 57              push    edi
757e915f e89c010000      call    gdi32full!PlayMetaFileRecord (757e9300)

然后使用g-命令从当前故障位置再次返回:

0:000> g- 757e915f
Time Travel Position: 2113:3DA
eax=19a0cee6 ebx=00000000 ecx=19a10f10 edx=00000084 esi=00000000 edi=9f211284
eip=757e915f esp=0078f5d4 ebp=0078f67c iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
gdi32full!CommonEnumMetaFile+0x3a0:
757e915f e89c010000      call    gdi32full!PlayMetaFileRecord (757e9300)

查看Microsoft的文档,可以发现关于PlayMetaFileRecord函数的如下介绍:

“PlayMetaFileRecord函数能够通过执行该记录中包含的图形设备接口(GDI)函数来播放Windows格式的元文件记录。”

图7:GDI32 PlayMetaFileRecord API文档。

下一步是在进入函数之前,输出并检查参数:

图8:转储PlayMetaFileRecord参数对应的内存内容。

请注意,LPMETARECORD看起来很眼熟,事实上,如果我们使用十六进制编辑器打开crasher的话,我们将看到以下内容:

图9:WMF文件中big-endian格式的LPMETARECORD

下面,让我们来考察StretchDIBits函数:

0:000> ub 758286ec
gdi32full!PlayMetaFileRecord+0x3f3d3:
758286d3 0fbf4316        movsx   eax,word ptr [ebx+16h]
758286d7 50              push    eax
758286d8 0fbf4318        movsx   eax,word ptr [ebx+18h]
758286dc 50              push    eax
758286dd 0fbf431a        movsx   eax,word ptr [ebx+1Ah]
758286e1 50              push    eax
758286e2 ff742444        push    dword ptr [esp+44h]
758286e6 ff1588d08975    call    dword ptr [gdi32full!_imp__StretchDIBits (7589d088)]

让我们再来看一下StretchDIBits函数:

图10:StretchDIBits函数

该函数需要总共13个参数,让我们具体看一下:

0:000> g 758286e6
ModLoad: 73610000 73689000   C:\WINDOWS\system32\uxtheme.dll
Time Travel Position: 2152:264
eax=00003030 ebx=19a3af78 ecx=30303030 edx=19a3af94 esi=19a3af94 edi=19a3affa
eip=758286e6 esp=0078f4b4 ebp=0078f5cc iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
gdi32full!PlayMetaFileRecord+0x3f3e6:
758286e6 ff1588d08975    call    dword ptr [gdi32full!_imp__StretchDIBits (7589d088)] ds:002b:7589d088={GDI32!StretchDIBits (76e13c60)}
And printing again the parameters right before the call:
0:000> dds esp LD
0078f4b4  9f211284 <== hdc
0078f4b8  00003030 <== xDest
0078f4bc  00003030 <== yDest
0078f4c0  00003030 <== DestWidth
0078f4c4  00003030 <== DestHeight
0078f4c8  00003030 <== xSrc
0078f4cc  00003030 <== ySrc
0078f4d0  00003030 <== SrcWidth
0078f4d4  00003030 <== SrcHeight
0078f4d8  19a3affa <== *lpBits
0078f4dc  19a3af94 <== *lpbmi
0078f4e0  00000001 <== iUsage
0078f4e4  30303030 <== rop

在所有这些值中,我们对*lpbmi的值特别感兴趣,因为它是指向BITMAPINFO结构的指针。

BITMAPINFO结构定义如下所示 bitmapinfoheader:

bmiHeader也是BITMAPINFOHEADER结构的成员,其中包含有关DIB的尺寸和颜色格式方面的信息。

BITMAPINFOHEADER结构定义如下:

图11:BITMAPINFOHEADER结构。

我们来看看lpbmi对应的内存内容:

0:000> dc 19a3af94
19a3af94  00000066 30303030 30303030 00200000  f...00000000.. .
19a3afa4  00000003 30303030 30303030 30303030  ....000000000000
19a3afb4  00000000 30303030 30303030 30303030  ....000000000000
19a3afc4  30303030 30303030 30303030 30303030  0000000000000000
19a3afd4  30303030 30303030 30303030 30303030  0000000000000000
19a3afe4  30303030 30303030 30303030 30303030  0000000000000000
19a3aff4  30303030 30303030 d0d0d0d0 ????????  00000000....????
19a3b004  ???????? ???????? ???????? ????????  ????????????????

小结


在本文中,我们为读者详细介绍了Windbg TTD的功能,以及如何利用该功能确定安全漏洞的根源所在,在下一篇文章中,我们将继续为读者奉献更多精彩内容,敬请期待。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖