programing tip

JPEG of Death 취약점은 어떻게 작동합니까?

itbloger 2020. 8. 29. 09:35
반응형

JPEG of Death 취약점은 어떻게 작동합니까?


Windows XP 및 Windows Server 2003 에서 GDI +에 대한 오래된 공격에 대해 제가 작업중인 프로젝트에서 JPEG of death라고 부르는 내용을 읽었 습니다.

익스플로잇은 다음 링크에 잘 설명되어 있습니다. http://www.infosecwriters.com/text_resources/pdf/JPEG.pdf

기본적으로 JPEG 파일에는 주석 필드 (비어있을 수 있음)가 포함 된 COM 섹션과 COM 크기가 포함 된 2 바이트 값이 포함됩니다. 주석이 없으면 크기는 2입니다. 판독기 (GDI +)는 크기를 읽고 2를 빼고 적절한 크기의 버퍼를 할당하여 힙의 주석을 복사합니다. 공격은 0필드에 값을 두는 것을 포함 합니다. GDI +는을 빼고 2값이 -2 (0xFFFe)으로 부호없는 정수로 변환 0XFFFFFFFE됩니다 memcpy.

샘플 코드 :

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

관찰 malloc(0)세 번째 줄에 힙에 할당되지 않은 메모리에 대한 포인터를 반환해야합니다. 어떻게 0XFFFFFFFE바이트 ( 4GB!!!!)를 쓰면 프로그램이 중단되지 않을까요? 이것은 힙 영역을 넘어서 다른 프로그램과 OS의 공간에 기록됩니까? 그러면 어떻게됩니까?

내가 이해하는 memcpy것처럼 단순히 n대상에서 소스로 문자를 복사 합니다. 이 경우, 소스는 스택에 힙의 대상, 그리고해야 n입니다 4GB.


이 취약점은 확실히 힙 오버 플로우 였습니다.

0XFFFFFFFE 바이트 (4GB !!!!)를 작성하면 프로그램이 충돌하지 않을 수 있습니까?

아마도 그럴 것이지만 어떤 경우에는 크래시가 발생하기 전에 악용 할 시간이 있습니다 (때로는 프로그램을 정상 실행으로 되돌리고 크래시를 피할 수 있습니다).

memcpy ()가 시작되면 복사본이 다른 힙 블록이나 힙 관리 구조의 일부 (예 : 여유 목록, 사용 중 목록 등)를 덮어 씁니다.

어느 시점에서 복사본은 할당되지 않은 페이지를 만나고 쓰기시 AV (액세스 위반)를 트리거합니다. 그런 다음 GDI +는 힙에 새 블록을 할당하려고 시도합니다 ( ntdll! RtlAllocateHeap 참조 ) ...하지만 힙 구조는 이제 모두 엉망이됩니다.

이때 JPEG 이미지를 신중하게 작성하여 제어 된 데이터로 힙 관리 구조를 덮어 쓸 수 있습니다. 시스템이 새 블록을 할당하려고 할 때 아마도 사용 가능한 목록에서 (사용 가능한) 블록의 연결을 해제 할 것입니다.

블록은 (특히) flink (정방향 링크, 목록의 다음 블록) 및 깜박임 (역방향 링크, 목록의 이전 블록) 포인터로 관리됩니다. flink와 blink를 모두 제어하면 쓸 수있는 내용과 쓸 수있는 위치를 제어 할 수있는 WRITE4 (What / Where 조건 쓰기)가있을 수 있습니다.

이 시점에서 함수 포인터 ( SEH [Structured Exception Handlers] 포인터는 2004 년 당시 선택 대상이었습니다)를 덮어 쓰고 코드 실행을 얻을 수 있습니다.

블로그 게시물 힙 손상 : 사례 연구를 참조하십시오 .

참고 : 내가 freelist를 사용하여 악용에 대해 썼지 만 공격자는 다른 힙 메타 데이터를 사용하여 다른 경로를 선택할 수 있지만 ( "힙 메타 데이터"는 시스템에서 힙을 관리하는 데 사용하는 구조이고 flink 및 blink는 힙 메타 데이터의 일부입니다) 링크 해제 악용은 아마도 "가장 쉬운"것입니다. "힙 악용"에 대한 Google 검색은 이에 대한 수많은 연구 결과를 반환합니다.

이것은 힙 영역을 넘어서 다른 프로그램과 OS의 공간에 기록됩니까?

못. 최신 OS는 가상 주소 공간의 개념을 기반으로하므로 각 프로세스에는 32 비트 시스템에서 최대 4GB의 메모리 주소를 지정할 수있는 고유 한 가상 주소 공간이 있습니다 (실제로는 사용자 영역에서 절반 만 얻었습니다. 나머지는 커널 용입니다.)

간단히 말해, 프로세스는 다른 프로세스의 메모리에 액세스 할 수 없습니다 (일부 서비스 / API를 통해 커널에 요청하는 경우를 제외하고 커널은 호출자가 그렇게 할 권한이 있는지 확인합니다).


저는 이번 주말에이 취약점을 테스트하기로 결정했습니다. 그래서 우리는 순수한 추측보다는 무슨 일이 일어나고 있는지에 대한 좋은 아이디어를 얻을 수있었습니다. 취약점은 이제 10 년이되었으므로이 답변에서 악용 부분에 대해 설명하지 않았지만 이에 대해 작성해도 괜찮다고 생각했습니다.

계획

가장 어려운 작업은 2004 년과 마찬가지로 SP1 만있는 Windows XP를 찾는 것이 었습니다. :)

그런 다음 아래와 같이 단일 픽셀로만 구성된 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 (Start Of Image) 마커이고 FF E0는 애플리케이션 마커입니다.

마커 세그먼트의 첫 번째 매개 변수 (SOI와 같은 일부 마커 제외)는 길이 매개 변수를 포함하고 2 바이트 마커를 제외하고 마커 세그먼트의 바이트 수를 인코딩하는 2 바이트 길이 매개 변수입니다.

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
[...]

The length of the COM segment is set to 00 00 to trigger the vulnerability. I also injected 0xFFFC bytes right after the COM marker with a recurring pattern, a 4 bytes number in hex, which will become handy when "exploiting" the vulnerability.

Debugging

Double clicking the image will immediately trigger the bug in the Windows shell (aka "explorer.exe"), somewhere in gdiplus.dll, in a function named GpJpegDecoder::read_jpeg_marker().

This function is called for each marker in the picture, it simply: reads the marker segment size, allocates a buffer whose length is the segment size and copy the content of the segment into this newly allocated buffer.

Here the start of the function :

.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 register points to the segment size and edi is the number of bytes left in the image.

The code then proceeds to read the segment size, starting by the most significant byte (length is a 16-bits value):

.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

And the least significant byte:

.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

Once this is done, the segment size is used to allocate a buffer, following this calculation:

alloc_size = segment_size + 2

This is done by the code below:

.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)

In our case, as the segment size is 0, the allocated size for the buffer is 2 bytes.

The vulnerability is right after the allocation:

.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

The code simply subtracts the segment_size size (segment length is a 2 bytes value) from the whole segment size (0 in our case) and ends up with an integer underflow: 0 - 2 = 0xFFFFFFFE

The code then checks is there are bytes left to parse in the image (which is true), and then jumps to the copy:

.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

The above snippet shows that copy size is 0xFFFFFFFE 32-bits chunks. The source buffer is controlled (content of the picture) and the destination is a buffer on the heap.

Write condition

The copy will trigger an access violation (AV) exception when it reaches the end of the memory page (this could be either from the source pointer or destination pointer). When the AV is triggered, the heap is already in a vulnerable state because the copy has already overwritten all following heap blocks until a non-mapped page was encountered.

What makes this bug exploitable is that 3 SEH (Structured Exception Handler; this is try / except at low level) are catching exceptions on this part of the code. More precisely, the 1st SEH will unwind the stack so it gets back to parse another JPEG marker, thus completely skipping the marker that triggered the exception.

Without an SEH the code would have just crashed the whole program. So the code skips the COM segment and parses another segment. So we get back to GpJpegDecoder::read_jpeg_marker() with a new segment and when the code allocates a new buffer:

.text:70E19A33  lea     eax, [esi+2] ; alloc_size = semgent_size + 2
.text:70E19A36  push    eax             ; dwBytes
.text:70E19A37  call    _GpMalloc@4     ; GpMalloc(x)

The system will unlink a block from the free list. It happens that metadata structures were overwritten by the content of the image; so we control the unlink with controlled metadata. The below code in somewhere in the system (ntdll) in the heap manager:

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

Now we can write what we want, where we want...


Since I don't know the code from GDI, what's below is just speculation.

Well, one thing that pops into mind is one behavior that I've noticed on some OSes (I don't know if Windows XP had this) was when allocating with new / malloc, you can actually allocate more than your RAM, as long as you don't write to that memory.

This is actually a behavior of the linux Kernel.

From www.kernel.org :

Pages in the process linear address space are not necessarily resident in memory. For example, allocations made on behalf of a process are not satisfied immediately as the space is just reserved within the vm_area_struct.

To get into resident memory a page fault must be triggered.

Basically you need to make the memory dirty before it actually gets allocated on the system:

  unsigned int size=-1;
  char* comment = new char[size];

Sometimes it won't actually make a real allocation in RAM (your program will still not use 4 GB). I know I've seen this behavior on a Linux, but I cannot however replicate it now on my Windows 7 installation.

Starting from this behavior the following scenario is possible.

In order to make that memory existing in RAM you need to make it dirty (basically memset or some other write to it):

  memset(comment, 0, size);

However the vulnerability exploits a buffer overflow, not an allocation failure.

In other words, if I'd were to have this:

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

This will lead to a write after buffer, because there's no such thing as a 4 GB segment of continuous memory.

You didn't put anything in p to make the whole 4 GB of memory dirty, and I don't know if memcpy makes memory dirty all at once, or just page by page (I think it's page by page).

Eventually it will end up overwriting the stack frame (Stack Buffer Overflow).

Another more possible vulnerability was if the picture was kept in memory as a byte array (read whole file into buffer), and the sizeof comments was used just to skip ahead non-vital information.

For example

     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

As you mentioned, if the GDI didn't allocates that size, the program will never crash.

참고URL : https://stackoverflow.com/questions/28369097/how-does-the-jpeg-of-death-vulnerability-operate

반응형