死亡的JPEG如何运行?

我一直在阅读关于针对Windows XP和Windows Server 2003上的GDI +的老版本攻击,这些攻击称为我正在进行的项目的死亡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字符到源。 在这种情况下,源应该在栈上,堆上的目的地, n4GB

这个漏洞肯定是堆溢出 。

如何写0XFFFFFFFE字节(4 GB的!!!!)可能不会崩溃的程序?

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

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

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

在这一点上,通过精心制作JPEG图像,您可以用受控数据覆盖堆pipe理结构。 当系统尝试分配新块时,它可能会从空闲列表中取消链接(空闲)块。

块(特别是)通过flink(前向链接;列表中的下一个块)和闪烁(向后链接;列表中的前一个块)指针进行pipe理。 如果你同时控制flink和blink,你可能会有一个WRITE4(写什么/哪里条件),你可以控制你写什么和写什么。

在那时你可以覆盖一个函数指针(在2004年的那个时候, SEH [结构化exception处理程序]指针是select的目标)并且获得代码执行。

查看博客文章堆腐败:案例研究

注意:虽然我写了使用freelist的攻击,但攻击者可能会使用其他堆元数据select另一个path(“堆元数据”是系统用于pipe理堆的结构; flink和blink是堆元数据的一部分),但unlink开发可能是“最简单”的一个。 谷歌search“堆开发”将返回许多研究关于这一点。

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

决不。 现代操作系统是基于虚拟地址空间的概念,因此每个进程都有自己的虚拟地址空间,可以在32位系统上寻址高达4GB的内存(实际上,只有一半的用户空间,其余的是内核)。

简而言之,一个进程无法访问另一个进程的内存(除非通过一些服务/ API向内核请求内核,但内核将检查调用者是否有权这样做)。


我决定在本周末testing这个漏洞,所以我们可以清楚地知道发生了什么,而不是纯粹的猜测。 这个漏洞现在已经有10年了,所以我觉得可以写一下,虽然我没有在这个答案中解释开发部分。

规划

最困难的任务是find一个只有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字节的hex数字,这将在“利用”漏洞时变得方便。

debugging

双击该图像将立即在名为GpJpegDecoder::read_jpeg_marker()的函数中触发Windows shell(又名“explorer.exe”)中的gdiplus.dll ,在gdiplus.dll中的某处。

这个函数是为图片中的每个标记调用的,它只是:读取标记段的大小,分配一个长度为段大小的缓冲区,并将段的内容复制到这个新分配的缓冲区中。

这里是函数的开始:

 .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

然后代码检查是否有字节留在图像中parsing(这是真的),然后跳转到副本:

 .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)exception。 当AV被触发时,堆已经处于易受攻击的状态,因为该副本已经覆盖了所有后面的堆块,直到遇到非映射页面。

是什么让这个bug被利用的是3 SEH(结构化exception处理程序;这是尝试/除了在低级别)捕捉这部分代码的例外。 更确切地说,第一个SEH将展开堆栈,以便返回parsing另一个JPEG标记,从而完全跳过触发exception的标记。

如果没有SEH,代码会崩溃整个程序。 所以代码跳过COM段并parsing另一个段。 所以我们回到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)在堆pipe理器中:

 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!!! 

现在我们可以写我们想要的东西,我们想要的东西…

由于我不知道GDI的代码,下面是猜测。

那么,有一件事是我注意到的一些行为,我注意到一些操作系统(我不知道是否Windows XP有这个)是分配新/ 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没有分配这个大小,程序永远不会崩溃。