这个漏洞明显是一个堆溢出。
写入0XFFFFFFFE字节(4 GB!!!),怎么可能不会导致程序崩溃呢?
通常情况下,它确实会导致程序崩溃,但在一些情况下,您有时间在崩溃发生之前利用这个漏洞(有时,您可以将程序恢复到正常执行并避免崩溃)。
当memcpy()开始时,拷贝操作会覆盖其他一些堆块或堆管理结构的某些部分(例如空闲列表,忙碌列表等)。
在某个点上,拷贝过程将遇到一个未分配的页面,并触发一个写访问冲突错误(Access Violation)。GDI + 然后尝试在堆中分配新的块 (请参见 ntdll!RtlAllocateHeap) ...... 但此时堆结构已经混乱。
此时,通过精心设计的JPEG图像,您可以使用受控数据来覆盖堆管理结构。当系统尝试分配新块时,它可能会从空闲列表中取消链接一个(空闲)块。
块是通过 flink (前向链 ; 列表中的下一个块) 和 blink(后向链; 列表中的前一个块)指针进行管理的。如果您同时控制 flink 和 blink,您可能有一个可能的 WRITE4 (write What/Where condition),其中您可以控制您能写入什么以及您可以写入哪里。
此时,您可以覆盖函数指针 (SEH [Structured Exception Handlers] 指针在2004年时是首选目标) 并获得代码执行权。
详见博客文章:堆破坏:案例研究。
注意:尽管我写了关于使用空闲列表进行利用的内容,但攻击者可能会选择使用其他堆元数据的路径 ("堆元数据" 是系统用于管理堆的结构; flink 和 blink 是堆元数据的一部分),但 unlink 利用可能是 "最简单" 的一种方式。在Google上搜索 "堆利用" 将返回大量关于此方面的研究。
这是否会写入到堆区域之外,进入其他程序和操作系统的空间?
不会。现代操作系统基于虚拟地址空间的概念,因此每个进程都有自己的虚拟地址空间,可以在32位系统上寻址高达4 GB的内存(实际上,在用户领域中你只能使用其中一半,剩下的是给内核用的)。
简而言之,一个进程不能访问另一个进程的内存(除非通过一些服务/ API 向内核请求,但内核会检查调用方是否有权这样做)。
我决定在这个周末测试这个漏洞,以便我们可以得到一个比单纯猜测更好的理解。这个漏洞已经存在了10年,所以我认为写一篇文章介绍它是可以的,尽管我没有在这篇答案中解释利用部分。
计划
最困难的任务是找到只有 Windows XP SP1 的版本,就像在2004年一样 :)
然后,我下载了一个仅由单个像素组成的 JPEG 图像,如下所示(为了简洁起见截断):
File 1x1_pixel.JPG
Address Hex dump ASCII
00000000 FF D8 FF E0|00 10 4A 46|49 46 00 01|01 01 00 60| ÿØÿà JFIF `
00000010 00 60 00 00|FF E1 00 16|45 78 69 66|00 00 49 49| ` ÿá Exif II
00000020 2A 00 08 00|00 00 00 00|00 00 00 00|FF DB 00 43| * ÿÛ C
[...]
JPEG图片由二进制标记(介绍片段)组成。在上面的图像中,FF D8
是SOI(图像开始)标记,而FF E0
,例如,是应用程序标记。
除了一些标记(如SOI)的标记段中的第一个参数是两个字节的长度参数,它编码标记段中的字节数,包括长度参数和不包括两个字节标记。
我只是在SOI之后添加了一个COM标记(0x FFFE
),因为标记没有严格的顺序。
File 1x1_pixel_comment_mod1.JPG
Address Hex dump ASCII
00000000 FF D8 FF FE|00 00 30 30|30 30 30 30|30 31 30 30| ÿØÿþ 0000000100
00000010 30 32 30 30|30 33 30 30|30 34 30 30|30 35 30 30| 0200030004000500
00000020 30 36 30 30|30 37 30 30|30 38 30 30|30 39 30 30| 0600070008000900
00000030 30 61 30 30|30 62 30 30|30 63 30 30|30 64 30 30| 0a000b000c000d00
[...]
COM段的长度设置为
00 00
以触发漏洞。我还在COM标记后注入了0xFFFC字节,使用递归模式和4字节数字的十六进制表示,这将在“利用”漏洞时变得方便。
调试
双击图像将立即在Windows资源管理器(也称为“explorer.exe”)中触发错误,在一个名为
GpJpegDecoder::read_jpeg_marker()
的函数中,位于
gdiplus.dll
中的某个位置。
此函数针对图像中的每个标记调用,它简单地读取标记段大小,分配长度为段大小的缓冲区,并将段的内容复制到此新分配的缓冲区中。
这里是函数的开始部分:
.text:70E199D5 mov ebx, [ebp+arg_0]
.text:70E199D8 push esi
.text:70E199D9 mov esi, [ebx+18h]
.text:70E199DC mov eax, [esi]
.text:70E199DE push edi
.text:70E199DF mov edi, [esi+4]
< p >
eax
寄存器指向段大小,
edi
则是镜像中剩余的字节数。
该代码接着读取段大小,从最高有效位开始(长度为 16 位):
.text:70E199F7 xor ecx, ecx
.text:70E199F9 mov ch, [eax]
.text:70E199FB dec edi
.text:70E199FC inc eax
.text:70E199FD test edi, edi
.text:70E199FF mov [ebp+arg_0], ecx
最不重要的字节:
.text:70E19A15 movzx cx, byte ptr [eax]
.text:70E19A19 add [ebp+arg_0], ecx
.text:70E19A1C mov ecx, [ebp+lpMem]
.text:70E19A1F inc eax
.text:70E19A20 mov [esi], eax
.text:70E19A22 mov eax, [ebp+arg_0]
完成这一步骤后,分段大小被用来分配一个缓冲区,按照以下计算方式进行:
alloc_size = segment_size + 2
下面的代码实现了此功能:
.text:70E19A29 movzx esi, word ptr [ebp+arg_0]
.text:70E19A2D add eax, 2
.text:70E19A30 mov [ecx], ax
.text:70E19A33 lea eax, [esi+2]
.text:70E19A36 push eax
.text:70E19A37 call _GpMalloc@4
在我们的情况下,由于段大小为0,分配给缓冲区的空间大小为2个字节。
漏���就在分配之后:
.text:70E19A37 call _GpMalloc@4
.text:70E19A3C test eax, eax
.text:70E19A3E mov [ebp+lpMem], eax
.text:70E19A41 jz loc_70E19AF1
.text:70E19A47 mov cx, [ebp+arg_4]
.text:70E19A4B mov [eax], cx
.text:70E19A52 lea edx, [esi-2]
.text:70E19A61 mov [ebp+arg_0], edx
这段代码只是从整个段的大小中减去segment_size的大小(segment长度是2字节的值),最终导致了整数下溢:0 - 2 = 0xFFFFFFFE
接下来,代码检查图像中是否有剩余的字节需要解析(是的),然后跳到复制操作:
.text:70E19A69 mov ecx, [eax+4]
.text:70E19A6C cmp ecx, edx
.text:70E19A6E jg short loc_70E19AB4
.text:70E19AB4 mov eax, [ebx+18h]
.text:70E19AB7 mov esi, [eax]
.text:70E19AB9 mov edi, dword ptr [ebp+arg_4]
.text:70E19ABC mov ecx, edx
.text:70E19ABE mov eax, ecx
.text:70E19AC0 shr ecx, 2
.text:70E19AC3 rep movsd
以上代码片段显示复制大小为0xFFFFFFFE 32位块。源缓冲区是可控的(图片内容),目标缓冲区在堆上。
写入条件:
当复制到内存页的末尾时(可能是源指针或目标指针),复制将触发访问冲突(AV)异常。当AV被触发时,堆已经处于易受攻击状态,因为复制已经覆盖了所有后续的堆块,直到遇到非映射页面。
这个bug能够被利用的原因是这部分代码中有3个SEH(结构化异常处理程序;这是低级try/except)。更确切地说,第一个SEH将展开堆栈,使其返回解析另一个JPEG标记,从而完全跳过触发异常的标记。
如果没有SEH,代码将仅崩溃整个程序。所以代码跳过了COM段并解析了另一个段。因此,我们回到
GpJpegDecoder :: read_jpeg_marker()
,并使用一个新的段来分配新的缓冲区。
.text:70E19A33 lea eax, [esi+2] ; alloc_size = semgent_size + 2
.text:70E19A36 push eax ; dwBytes
.text:70E19A37 call _GpMalloc@4 ; GpMalloc(x)
系统将从空闲列表中取消关联一个块。由于元数据结构被图像内容覆盖,我们使用可控元数据来控制取消关联。下面的代码在系统(ntdll)的堆管理器中的某个地方:
CPU Disasm
Address Command Comments
77F52CBF MOV ECX,DWORD PTR DS:[EAX] ; eax points to '0003' ; ecx = 0x33303030
77F52CC1 MOV DWORD PTR SS:[EBP-0B0],ECX ; save ecx
77F52CC7 MOV EAX,DWORD PTR DS:[EAX+4] ; [eax+4] points to '0004' ; eax = 0x34303030
77F52CCA MOV DWORD PTR SS:[EBP-0B4],EAX
77F52CD0 MOV DWORD PTR DS:[EAX],ECX ; write 0x33303030 to 0x34303030!!!
现在我们可以在任何地方书写我们想要的内容...
malloc
分配的大小只有2个字节,而不是0xFFFFFFFE
。这个巨大的大小仅用于复制大小,而不是分配大小。 - Neitsa