我一直在阅读有关Windows XP上针对GDI +的旧版漏洞,Windows Server 2003称我正在进行的项目称为死亡JPEG版。
该漏洞利用在以下链接中得到了很好的解释: http://www.infosecwriters.com/text_resources/pdf/JPEG.pdf
基本上,JPEG文件包含一个名为COM的部分,其中包含一个(可能为空)注释字段,以及一个包含COM大小的双字节值。如果没有注释,则大小为2.读取器(GDI +)读取大小,减去两个,并分配适当大小的缓冲区以复制堆中的注释。
攻击涉及在字段中放置0
的值。 GDI +减去2
,导致-2 (0xFFFe)
的值被memcpy
转换为无符号整数0XFFFFFFFE
。
示例代码:
unsigned int size;
size = len - 2;
char *comment = (char *)malloc(size + 1);
memcpy(comment, src, size);
注意第三行上的malloc(0)
应返回指向堆上未分配内存的指针。如何写0XFFFFFFFE
个字节(4GB
!!!!)可能不会导致程序崩溃?这是否超出堆区域并进入其他程序和操作系统的空间?那么会发生什么?
据我了解memcpy
,它只是将n
个字符从目标复制到源。在这种情况下,源应该在堆栈上,堆上的目标,n
是4GB
。
答案 0 :(得分:94)
此漏洞肯定是heap overflow。
如何编写0XFFFFFFFE字节(4 GB !!!!)可能不会导致程序崩溃?
它可能会,但在某些情况下你有时间在崩溃发生之前进行攻击(有时,你可以让程序恢复正常执行并避免崩溃)。
当memcpy()启动时,副本将覆盖其他一些堆块或堆管理结构的某些部分(例如,空闲列表,忙列表等)。
在某些时候,副本将遇到未分配的页面并在写入时触发AV(访问冲突)。然后GDI +将尝试在堆中分配一个新块(参见ntdll!RtlAllocateHeap)...但是堆结构现在都搞砸了。
此时,通过精心设计JPEG图像,您可以使用受控数据覆盖堆管理结构。当系统尝试分配新块时,它可能会取消(空闲)块与空闲列表的链接。
使用(特别是)flink(前向链接;列表中的下一个块)和闪烁(后向链接;列表中的前一个块)指针来管理块。如果您同时控制flink和blink,则可能有一个WRITE4(写入What / Where条件),您可以在其中控制可以写入的内容以及可以写入的位置。
此时你可以覆盖一个函数指针(SEH [Structured Exception Handlers]指针是2004年那个时候的首选目标)并获得代码执行。
请参阅博文 Heap Corruption: A Case Study 。
注意:虽然我写了关于使用freelist的利用,但攻击者可能会选择使用其他堆元数据的另一个路径("堆元数据"是系统用来管理堆的结构; flink和blink是堆元数据的一部分),但取消链接利用可能是最简单的"一。谷歌搜索"堆剥削"将返回关于此的大量研究。
这是否超出堆区域并进入其他区域 程序和操作系统?
从不。现代操作系统基于虚拟地址空间的概念,因此每个进程都有自己的虚拟地址空间,可以在32位系统上寻址高达4千兆字节的内存(实际上,只有一半在用户区中,剩下的就是内核)。
简而言之,一个进程无法访问另一个进程的内存(除非它通过某些服务/ 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之类的标记除外)是一个双字节长度参数,它对标记段中的字节数进行编码,包括长度参数,不包括双字节标记。
我只是在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字节的十六进制数字,当&#34;利用&#34;时,它会变得很方便。漏洞。
<强>调试强>
双击图像将立即触发Windows shell中的错误(又名&#34; explorer.exe&#34;),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时,堆已处于易受攻击状态,因为副本已覆盖所有后续堆块,直到遇到未映射的页面。
使这个bug可被利用的原因是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!!!
现在我们可以写出我们想要的东西,我们想要的地方......
答案 1 :(得分:3)
由于我不知道GDI的代码,下面的内容只是推测。
嗯,有一件事是我在一些操作系统上注意到的一种行为(我不知道Windows XP是否有这种情况)是在使用new / malloc
进行分配时,您实际上可以分配只要你不写入那个内存,就要超过你的RAM。
这实际上是linux Kernel的行为。
来自www.kernel.org:
进程线性地址空间中的页面不一定驻留在内存中。例如,由于空间仅在vm_area_struct中保留,因此不会立即满足代表进程的分配。
要进入常驻内存,必须触发页面错误。
基本上你需要在系统实际分配之前使内存变脏:
unsigned int size=-1;
char* comment = new char[size];
有时它实际上不会在RAM中进行实际分配(您的程序仍然不会使用4 GB)。我知道我在Linux上看到过这种行为,但我现在无法在Windows 7上安装它。
从这种行为开始,可能出现以下情况。
为了使RAM中存在该内存,您需要将其弄脏(基本上是memset或其他一些写入):
memset(comment, 0, size);
但是,漏洞会利用缓冲区溢出,而不是分配失败。
换句话说,如果我有这个:
unsinged int size =- 1;
char* p = new char[size]; // Will not crash here
memcpy(p, some_buffer, size);
这将导致写缓冲区,因为没有4 GB的连续内存段。
你没有在p中放入任何内容使整个4 GB内存变脏,我不知道memcpy
是否一次性内存变脏,或者只是逐页(我认为是页面)按页)。
最终它将最终覆盖堆栈帧(堆栈缓冲区溢出)。
另一个更可能的漏洞是,如果图片作为字节数组保存在内存中(将整个文件读入缓冲区),并且注释的大小仅用于跳过非重要信息。
例如
unsigned int commentsSize = -1;
char* wholePictureBytes; // Has size of file
...
// Time to start processing the output color
char* p = wholePictureButes;
offset = (short) p[COM_OFFSET];
char* dataP = p + offset;
dataP[0] = EvilHackerValue; // Vulnerability here
正如您所提到的,如果GDI没有分配该大小,程序将永远不会崩溃。