此漏洞肯定是堆溢出。
怎么写0XFFFFFFFE字节(4 GB !!!!)可能不会使程序崩溃?
可能会,但是在某些情况下,您有时间在崩溃发生之前加以利用(有时,您可以使程序恢复到正常执行状态,并避免崩溃)。
当memcpy()启动时,副本将覆盖其他一些堆块或堆管理结构的某些部分(例如,空闲列表,繁忙列表等)。
在某个时候,副本将遇到未分配的页面,并在写入时触发AV(访问冲突)。然后,GDI +将尝试在堆中分配一个新块(请参阅ntdll!RtlAllocateHeap)……但是,堆结构现在都被弄乱了。
到那时,通过精心制作JPEG图像,可以用受控数据覆盖堆管理结构。当系统尝试分配新块时,它可能会从空闲列表中取消链接(空闲)块。
使用(特别是)flink(向前链接;列表中的下一个块)和闪烁(向后链接;列表中的前一个块)指针管理块。如果同时控制闪烁和闪烁,则可能有一个WRITE4(写What / Where条件),可以在其中控制可写内容和可写位置。
到那时,您可以覆盖函数指针(SEH [Structured Exception Handlers]指针在2004年当时是首选对象)并获得代码执行。
参见博客文章堆腐败:案例研究。
注意:尽管我使用自由列表写过关于利用的文章,但攻击者可能会使用其他堆元数据来选择其他路径(“堆元数据”是系统用来管理堆的结构; flink和blink是堆元数据的一部分),但是断开链接的利用可能是“最简单的”利用。谷歌搜索“堆利用”将返回有关此的大量研究。
这是否会超出堆区域并写入其他程序和OS的空间?
决不。现代操作系统基于虚拟地址空间的概念,因此每个进程都具有自己的虚拟地址空间,该地址空间可在32位系统上寻址多达4 GB的内存(在实践中,您只有一半的内存是在用户土地上使用的,其余的用于内核)。
简而言之,一个进程无法访问另一个进程的内存(除非它通过某些服务/ API向内核请求它,但是内核将检查调用者是否有权这样做)。
我决定在本周末测试此漏洞,因此我们可以对正在发生的事情有所了解,而不是单纯的猜测。该漏洞现在已有10年了,因此我认为可以写此漏洞,尽管我没有在此答案中解释漏洞利用部分。
规划
最困难的任务是找到只有SP1的Windows XP,就像在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除外))是一个两字节长度的参数,该参数编码标记段中的字节数,包括长度参数,但不包括两字节标记。
我只是FFFE
在SOI之后添加了COM标记(0x ),因为标记没有严格的顺序。
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”)gdiplus.dll
中名为的函数中的错误GpJpegDecoder::read_jpeg_marker()
。
对图片中的每个标记都调用此函数,方法很简单:读取标记段的大小,分配一个长度为段大小的缓冲区,然后将段的内容复制到此新分配的缓冲区中。
这是功能的开始:
.text:70E199D5 mov ebx, [ebp+arg_0] ; ebx = *this (GpJpegDecoder instance)
.text:70E199D8 push esi
.text:70E199D9 mov esi, [ebx+18h]
.text:70E199DC mov eax, [esi] ; eax = pointer to segment size
.text:70E199DE push edi
.text:70E199DF mov edi, [esi+4] ; edi = bytes left to process in the image
eax
寄存器指向段大小,edi
是图像中剩余的字节数。
然后,代码继续读取段大小,从最高有效字节开始(长度为16位值):
.text:70E199F7 xor ecx, ecx ; segment_size = 0
.text:70E199F9 mov ch, [eax] ; get most significant byte from size --> CH == 00
.text:70E199FB dec edi ; bytes_to_process --
.text:70E199FC inc eax ; pointer++
.text:70E199FD test edi, edi
.text:70E199FF mov [ebp+arg_0], ecx ; save segment_size
最低有效字节:
.text:70E19A15 movzx cx, byte ptr [eax] ; get least significant byte from size --> CX == 0
.text:70E19A19 add [ebp+arg_0], ecx ; save segment_size
.text:70E19A1C mov ecx, [ebp+lpMem]
.text:70E19A1F inc eax ; pointer ++
.text:70E19A20 mov [esi], eax
.text:70E19A22 mov eax, [ebp+arg_0] ; eax = segment_size
完成此操作后,将根据以下计算使用段大小来分配缓冲区:
alloc_size = segment_size + 2
这是通过以下代码完成的:
.text:70E19A29 movzx esi, word ptr [ebp+arg_0] ; esi = segment size (cast from 16-bit to 32-bit)
.text:70E19A2D add eax, 2
.text:70E19A30 mov [ecx], ax
.text:70E19A33 lea eax, [esi+2] ; alloc_size = segment_size + 2
.text:70E19A36 push eax ; dwBytes
.text:70E19A37 call _GpMalloc@4 ; GpMalloc(x)
在本例中,由于段大小为0,因此为缓冲区分配的大小为2个字节。
该漏洞在分配之后就可以使用:
.text:70E19A37 call _GpMalloc@4 ; GpMalloc(x)
.text:70E19A3C test eax, eax
.text:70E19A3E mov [ebp+lpMem], eax ; save pointer to allocation
.text:70E19A41 jz loc_70E19AF1
.text:70E19A47 mov cx, [ebp+arg_4] ; low marker byte (0xFE)
.text:70E19A4B mov [eax], cx ; save in alloc (offset 0)
;[...]
.text:70E19A52 lea edx, [esi-2] ; edx = segment_size - 2 = 0 - 2 = 0xFFFFFFFE!!!
;[...]
.text:70E19A61 mov [ebp+arg_0], edx
该代码仅从整个段大小(在本例中为0)中减去segment_size大小(段长度为2个字节的值),并以整数下溢结束:0-2 = 0xFFFFFFFE
然后,代码检查图像中是否还有要解析的字节(这是正确的),然后跳转到副本:
.text:70E19A69 mov ecx, [eax+4] ; ecx = bytes left to parse (0x133)
.text:70E19A6C cmp ecx, edx ; edx = 0xFFFFFFFE
.text:70E19A6E jg short loc_70E19AB4 ; take jump to copy
;[...]
.text:70E19AB4 mov eax, [ebx+18h]
.text:70E19AB7 mov esi, [eax] ; esi = source = points to segment content ("0000000100020003...")
.text:70E19AB9 mov edi, dword ptr [ebp+arg_4] ; edi = destination buffer
.text:70E19ABC mov ecx, edx ; ecx = copy size = segment content size = 0xFFFFFFFE
.text:70E19ABE mov eax, ecx
.text:70E19AC0 shr ecx, 2 ; size / 4
.text:70E19AC3 rep movsd ; copy segment content by 32-bit chunks
上面的代码片段显示副本大小为0xFFFFFFFE 32位块。源缓冲区是受控制的(图片的内容),目标缓冲区是堆上的缓冲区。
写条件
当副本到达内存页面的末尾时(该副本可能来自源指针或目标指针),它将触发访问冲突(AV)异常。触发AV时,堆已经处于易受攻击的状态,因为该副本已经覆盖了随后的所有堆块,直到遇到未映射的页面为止。
使此漏洞可利用的原因是3 SEH(结构化异常处理程序;这是try /除外,级别较低)正在捕获此部分代码中的异常。更准确地说,第一个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!!!
现在我们可以写我们想要的东西,我们想要的地方...