Как работает уязвимость JPEG of Death?

94

Я читал о более старом эксплойте против GDI + в Windows XP и Windows Server 2003, который называется JPEG смерти для проекта, над которым я работаю.

Эксплойт подробно описан в следующей ссылке: http://www.infosecwriters.com/text_resources/pdf/JPEG.pdf

По сути, файл JPEG содержит раздел под названием COM, содержащий (возможно, пустое) поле комментария и двухбайтовое значение, содержащее размер COM. Если комментариев нет, размер равен 2. Программа чтения (GDI +) считывает размер, вычитает два и выделяет буфер подходящего размера для копирования комментариев в куче. Атака включает размещение значения 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!!!!) может не привести к сбою программы? Записывается ли это за пределы области кучи и в пространство других программ и ОС? Что происходит потом?

Насколько я понимаю memcpy, он просто копирует nсимволы из места назначения в источник. В этом случае источник должен быть в стеке, адресат в куче, и nесть 4GB.

Рафа
источник
malloc будет выделять память из кучи. Я думаю, что эксплойт был выполнен до memcpy и после выделения памяти
iedoc
просто в качестве примечания: не memcpy увеличивает значение до целого числа без знака (4 байта), а скорее вычитание.
rev
1
Обновил мой предыдущий ответ живым примером. Размер malloced составляет всего 2 байта, а не 0xFFFFFFFE. Этот огромный размер используется только для размера копии, а не для размера выделения.
Neitsa

Ответы:

96

Эта уязвимость определенно была переполнением кучи .

Как может запись байтов 0XFFFFFFFE (4 ГБ !!!!) не привести к сбою программы?

Вероятно, так и будет, но в некоторых случаях у вас есть время для использования уязвимости до того, как произойдет сбой (иногда вы можете вернуть программу к нормальному выполнению и избежать сбоя).

При запуске memcpy () копия перезапишет либо некоторые другие блоки кучи, либо некоторые части структуры управления кучей (например, список свободных, список занятости и т. Д.).

В какой-то момент копия обнаружит невыделенную страницу и вызовет AV (нарушение доступа) при записи. Затем GDI + попытается выделить новый блок в куче (см. Ntdll! RtlAllocateHeap ) ... но теперь все структуры кучи испорчены.

На этом этапе, тщательно обработав свое изображение JPEG, вы можете перезаписать структуры управления кучей контролируемыми данными. Когда система пытается выделить новый блок, она, вероятно, отсоединит (свободный) блок от свободного списка.

Блок управляется (в частности) с помощью указателей flink (прямая ссылка; следующий блок в списке) и мигания (обратная ссылка; предыдущий блок в списке). Если вы контролируете и мигание, и мигание, у вас может быть возможность WRITE4 (условие записи What / Where), где вы контролируете, что вы можете писать и где вы можете писать.

На этом этапе вы можете перезаписать указатель функции (указатели SEH [Structured Exception Handlers] были предпочтительной целью в то время, еще в 2004 году) и получить выполнение кода.

См. Сообщение в блоге « Коррупция кучи: пример из практики» .

Примечание: хотя я писал об эксплуатации с использованием списка фрилансеров, злоумышленник может выбрать другой путь, используя другие метаданные кучи («метаданные кучи» - это структуры, используемые системой для управления кучей; мигание и мигание являются частью метаданных кучи), но использование unlink, вероятно, является самым "легким". Поиск в Google по запросу "использование кучи" даст многочисленные исследования по этому поводу.

Записывается ли это за пределы области кучи и в пространство других программ и ОС?

Никогда. Современные ОС основаны на концепции виртуального адресного пространства, поэтому каждый процесс имеет свое собственное виртуальное адресное пространство, которое позволяет адресовать до 4 гигабайт памяти в 32-разрядной системе (на практике у вас только половина ее в пользовательском пространстве, остальное для ядра).

Короче говоря, процесс не может получить доступ к памяти другого процесса (кроме случаев, когда он запрашивает об этом у ядра через какую-то службу / API, но ядро ​​проверяет, имеет ли вызывающий право на это право).


Я решил протестировать эту уязвимость в эти выходные, чтобы мы могли лучше понять, что происходит, а не просто домыслы. Уязвимости сейчас 10 лет, поэтому я подумал, что можно написать об этом, хотя я не объяснил часть эксплуатации в этом ответе.

Планирование

Самой сложной задачей было найти Windows XP только с SP1, как это было в 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) - это параметр двухбайтовой длины, который кодирует количество байтов в сегменте маркера, включая параметр длины и исключая двухбайтовый маркер.

Я просто добавил COM-маркер (0x FFFE) сразу после SOI, поскольку маркеры не имеют строгого порядка.

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чтобы вызвать уязвимость. Я также ввел байты 0xFFFC сразу после маркера COM с повторяющимся шаблоном, числом 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 = размер_сегмента + 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

Код просто вычитает размер segment_size (длина сегмента составляет 2 байта) из всего размера сегмента (0 в нашем случае) и заканчивается целочисленным недополнением: 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 / за исключением низкого уровня) перехватывают исключения в этой части кода. Точнее, 1-й 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!!!

Теперь мы можем писать что хотим и где хотим ...

Neitsa
источник
3

Поскольку я не знаю кода из GDI, то, что ниже, - всего лишь предположение.

Что ж, одна вещь, которая приходит в голову, - это одно поведение, которое я заметил в некоторых ОС (я не знаю, было ли это в Windows XP), было при выделении с помощью new / malloc, вы действительно можете выделить больше, чем ваша оперативная память, если вы не пишете в эту память.

На самом деле это поведение ядра Linux.

С www.kernel.org:

Страницы в линейном адресном пространстве процесса не обязательно находятся в памяти. Например, выделения, сделанные от имени процесса, не выполняются немедленно, поскольку пространство просто зарезервировано в vm_area_struct.

Чтобы попасть в резидентную память, должна сработать ошибка страницы.

В основном вам нужно загрязнить память, прежде чем она будет фактически выделена в системе:

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

Иногда на самом деле не происходит реального выделения в ОЗУ (ваша программа все равно не будет использовать 4 ГБ). Я знаю, что видел такое поведение в Linux, но, тем не менее, я не могу воспроизвести его сейчас в моей установке Windows 7.

Исходя из этого поведения возможен следующий сценарий.

Чтобы сделать эту память существующей в ОЗУ, вам нужно сделать ее грязной (в основном memset или другую запись в нее):

  memset(comment, 0, size);

Однако уязвимость использует переполнение буфера, а не сбой выделения.

Другими словами, если бы у меня было это:

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

Это приведет к записи за буфером, потому что не существует такого понятия, как сегмент непрерывной памяти размером 4 ГБ.

Вы ничего не вставляли в p, чтобы испачкать все 4 ГБ памяти, и я не знаю, 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 не выделяет этот размер, программа никогда не выйдет из строя.

MichaelCMS
источник
4
Это могло быть с 64-битной системой, где 4 ГБ не имеет большого значения (если говорить о пространстве адресов). Но в 32-битной системе (они тоже кажутся уязвимыми) вы не можете зарезервировать 4 ГБ адресного пространства, потому что это все, что есть! Так что malloc(-1U)он обязательно потерпит неудачу, вернется NULLи memcpy()рухнет.
Родриго
9
Я не думаю, что эта строка верна: «В конце концов, она будет записана в адрес другого процесса». Обычно один процесс не может получить доступ к памяти другого. См. Преимущества MMU .
chue x
@MMU Преимущества да, вы правы. Я должен был сказать, что это выйдет за обычные границы кучи и начнет перезапись кадра стека. Я отредактирую свой ответ, спасибо, что указали на него.
MichaelCMS